Added mvt parser lib. Added mapillary vector tiles. Refactored resources cache.

This commit is contained in:
Alexey Kulish 2017-05-16 21:50:41 +03:00
parent 3e69f02629
commit ba63945038
40 changed files with 7937 additions and 356 deletions

Binary file not shown.

View file

@ -0,0 +1,50 @@
package com.wdtinc.mapbox_vector_tile.adapt.jts;
import com.vividsolutions.jts.geom.*;
/**
* Filter {@link Polygon} and {@link MultiPolygon} by area or
* {@link LineString} and {@link MultiLineString} by length.
*
* @see IGeometryFilter
*/
public final class GeomMinSizeFilter implements IGeometryFilter {
/** Minimum area */
private final double minArea;
/** Minimum length */
private final double minLength;
/**
* @param minArea minimum area required for a {@link Polygon} or {@link MultiPolygon}
* @param minLength minimum length required for a {@link LineString} or {@link MultiLineString}
*/
public GeomMinSizeFilter(double minArea, double minLength) {
if(minArea < 0.0d) {
throw new IllegalArgumentException("minArea must be >= 0");
}
if(minLength < 0.0d) {
throw new IllegalArgumentException("minLength must be >= 0");
}
this.minArea = minArea;
this.minLength = minLength;
}
@Override
public boolean accept(Geometry geometry) {
boolean accept = true;
if((geometry instanceof Polygon || geometry instanceof MultiPolygon)
&& geometry.getArea() < minArea) {
accept = false;
} else if((geometry instanceof LineString || geometry instanceof MultiLineString)
&& geometry.getLength() < minLength) {
accept = false;
}
return accept;
}
}

View file

@ -0,0 +1,15 @@
package com.wdtinc.mapbox_vector_tile.adapt.jts;
import com.vividsolutions.jts.geom.Geometry;
public interface IGeometryFilter {
/**
* Return true if the value should be accepted (pass), or false if the value should be rejected (fail).
*
* @param geometry input to test
* @return true if the value should be accepted (pass), or false if the value should be rejected (fail)
* @see Geometry
*/
boolean accept(Geometry geometry);
}

View file

@ -0,0 +1,26 @@
package com.wdtinc.mapbox_vector_tile.adapt.jts;
import net.osmand.binary.VectorTile;
import java.util.List;
/**
* Process MVT tags and feature id, convert to user data object. The returned user data
* object may be null.
*/
public interface ITagConverter {
/**
* Convert MVT user data to JTS user data object or null.
*
* @param id feature id, may be {@code null}
* @param tags MVT feature tags, may be invalid
* @param keysList layer key list
* @param valuesList layer value list
* @return user data object or null
*/
Object toUserData(Long id,
List<Integer> tags,
List<String> keysList,
List<VectorTile.Tile.Value> valuesList);
}

View file

@ -0,0 +1,23 @@
package com.wdtinc.mapbox_vector_tile.adapt.jts;
import com.wdtinc.mapbox_vector_tile.build.MvtLayerProps;
import net.osmand.binary.VectorTile;
/**
* Processes a user data object, converts to MVT feature tags.
*/
public interface IUserDataConverter {
/**
* <p>Convert user data to MVT tags. The supplied user data may be null. Implementation
* should update layerProps and optionally set the feature id.</p>
*
* <p>SIDE EFFECT: The implementation may add tags to featureBuilder, modify layerProps, modify userData.</p>
*
* @param userData user object may contain values in any format; may be null
* @param layerProps properties global to the layer the feature belongs to
* @param featureBuilder may be modified to contain additional tags
*/
void addTags(Object userData, MvtLayerProps layerProps, VectorTile.Tile.Feature.Builder featureBuilder);
}

View file

@ -0,0 +1,655 @@
package com.wdtinc.mapbox_vector_tile.adapt.jts;
import com.vividsolutions.jts.algorithm.CGAlgorithms;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.CoordinateArrays;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryCollection;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.MultiLineString;
import com.vividsolutions.jts.geom.MultiPoint;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;
import com.vividsolutions.jts.geom.TopologyException;
import com.vividsolutions.jts.geom.util.AffineTransformation;
import com.vividsolutions.jts.simplify.TopologyPreservingSimplifier;
import com.wdtinc.mapbox_vector_tile.build.MvtLayerParams;
import com.wdtinc.mapbox_vector_tile.build.MvtLayerProps;
import com.wdtinc.mapbox_vector_tile.encoding.GeomCmd;
import com.wdtinc.mapbox_vector_tile.encoding.GeomCmdHdr;
import com.wdtinc.mapbox_vector_tile.encoding.MvtUtil;
import com.wdtinc.mapbox_vector_tile.encoding.ZigZag;
import com.wdtinc.mapbox_vector_tile.util.Vec2d;
import net.osmand.binary.VectorTile;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Stack;
/**
* Adapt JTS {@link Geometry} to 'Mapbox Vector Tile' objects.
*/
public final class JtsAdapter {
/**
* Create geometry clipped and then converted to MVT 'extent' coordinates. Result
* contains both clipped geometry (intersection) and transformed geometry for encoding to MVT.
*
* @param g original 'source' geometry
* @param tileEnvelope world coordinate bounds for tile
* @param geomFactory creates a geometry for the tile envelope
* @param mvtLayerParams specifies vector tile properties
* @param filter geometry values that fail filter after transforms are removed
* @return tile geometry result
* @see TileGeomResult
*/
public static TileGeomResult createTileGeom(Geometry g,
Envelope tileEnvelope,
GeometryFactory geomFactory,
MvtLayerParams mvtLayerParams,
IGeometryFilter filter) {
return createTileGeom(flatFeatureList(g), tileEnvelope, geomFactory,
mvtLayerParams, filter);
}
/**
* Create geometry clipped and then converted to MVT 'extent' coordinates. Result
* contains both clipped geometry (intersection) and transformed geometry for encoding to MVT.
*
* @param g original 'source' geometry, passed through {@link #flatFeatureList(Geometry)}
* @param tileEnvelope world coordinate bounds for tile
* @param geomFactory creates a geometry for the tile envelope
* @param mvtLayerParams specifies vector tile properties
* @param filter geometry values that fail filter after transforms are removed
* @return tile geometry result
* @see TileGeomResult
*/
public static TileGeomResult createTileGeom(List<Geometry> g,
Envelope tileEnvelope,
GeometryFactory geomFactory,
MvtLayerParams mvtLayerParams,
IGeometryFilter filter) {
final Geometry tileEnvelopeGeom = geomFactory.toGeometry(tileEnvelope);
final AffineTransformation t = new AffineTransformation();
final double xDiff = tileEnvelope.getWidth();
final double yDiff = tileEnvelope.getHeight();
final double xOffset = -tileEnvelope.getMinX();
final double yOffset = -tileEnvelope.getMinY();
// Transform Setup: Shift to 0 as minimum value
t.translate(xOffset, yOffset);
// Transform Setup: Scale X and Y to tile extent values, flip Y values
t.scale(1d / (xDiff / (double) mvtLayerParams.extent),
-1d / (yDiff / (double) mvtLayerParams.extent));
// Transform Setup: Bump Y values to positive quadrant
t.translate(0d, (double) mvtLayerParams.extent);
// The area contained in BOTH the 'original geometry', g, AND the 'tile envelope geometry' is the 'tile geometry'
final List<Geometry> intersectedGeoms = flatIntersection(tileEnvelopeGeom, g);
final List<Geometry> transformedGeoms = new ArrayList<>(intersectedGeoms.size());
// Transform intersected geometry
Geometry nextTransformGeom;
Object nextUserData;
for(Geometry nextInterGeom : intersectedGeoms) {
nextUserData = nextInterGeom.getUserData();
nextTransformGeom = t.transform(nextInterGeom);
// Floating --> Integer, still contained within doubles
nextTransformGeom.apply(RoundingFilter.INSTANCE);
// TODO: Refactor line simplification
nextTransformGeom = TopologyPreservingSimplifier.simplify(nextTransformGeom, .1d); // Can't use 0d, specify value < .5d
nextTransformGeom.setUserData(nextUserData);
// Apply filter on transformed geometry
if(filter.accept(nextTransformGeom)) {
transformedGeoms.add(nextTransformGeom);
}
}
return new TileGeomResult(intersectedGeoms, transformedGeoms);
}
/**
* @param envelope non-list geometry defines bounding area
* @param data geometry passed to {@link #flatFeatureList(Geometry)}
* @return list of geometry from {@code data} intersecting with {@code envelope}.
* @see #flatIntersection(Geometry, List)
*/
private static List<Geometry> flatIntersection(Geometry envelope, Geometry data) {
return flatIntersection(envelope, flatFeatureList(data));
}
/**
* JTS 1.14 does not support intersection on a {@link GeometryCollection}. This function works around this
* by performing intersection on a flat list of geometry. The resulting list is pre-filtered for invalid
* or empty geometry (outside of bounds). Invalid geometry are logged as errors.
*
* @param envelope non-list geometry defines bounding area
* @param dataGeoms geometry pre-passed through {@link #flatFeatureList(Geometry)}
* @return list of geometry from {@code data} intersecting with {@code envelope}.
*/
private static List<Geometry> flatIntersection(Geometry envelope, List<Geometry> dataGeoms) {
final List<Geometry> intersectedGeoms = new ArrayList<>(dataGeoms.size());
Geometry nextIntersected;
for(Geometry nextGeom : dataGeoms) {
try {
// AABB intersection culling
if(envelope.getEnvelopeInternal().intersects(nextGeom.getEnvelopeInternal())) {
nextIntersected = envelope.intersection(nextGeom);
if(!nextIntersected.isEmpty()) {
nextIntersected.setUserData(nextGeom.getUserData());
intersectedGeoms.add(nextIntersected);
}
}
} catch (TopologyException e) {
//LoggerFactory.getLogger(JtsAdapter.class).error(e.getMessage(), e);
}
}
return intersectedGeoms;
}
/**
* Get the MVT type mapping for the provided JTS Geometry.
*
* @param geometry JTS Geometry to get MVT type for
* @return MVT type for the given JTS Geometry, may return
* {@link com.wdtinc.mapbox_vector_tile.VectorTile.Tile.GeomType#UNKNOWN}
*/
public static VectorTile.Tile.GeomType toGeomType(Geometry geometry) {
VectorTile.Tile.GeomType result = VectorTile.Tile.GeomType.UNKNOWN;
if(geometry instanceof Point
|| geometry instanceof MultiPoint) {
result = VectorTile.Tile.GeomType.POINT;
} else if(geometry instanceof LineString
|| geometry instanceof MultiLineString) {
result = VectorTile.Tile.GeomType.LINESTRING;
} else if(geometry instanceof Polygon
|| geometry instanceof MultiPolygon) {
result = VectorTile.Tile.GeomType.POLYGON;
}
return result;
}
/**
* <p>Recursively convert a {@link Geometry}, which may be an instance of {@link GeometryCollection} with mixed
* element types, into a flat list containing only the following {@link Geometry} types:</p>
* <ul>
* <li>{@link Point}</li>
* <li>{@link LineString}</li>
* <li>{@link Polygon}</li>
* <li>{@link MultiPoint}</li>
* <li>{@link MultiLineString}</li>
* <li>{@link MultiPolygon}</li>
* </ul>
* <p>WARNING: Any other Geometry types that were not mentioned in the list above will be discarded!</p>
* <p>Useful for converting a generic geometry into a list of simple MVT-feature-ready geometries.</p>
*
* @param geom geometry to flatten
* @return list of MVT-feature-ready geometries
*/
public static List<Geometry> flatFeatureList(Geometry geom) {
final List<Geometry> singleGeoms = new ArrayList<>();
final Stack<Geometry> geomStack = new Stack<>();
Geometry nextGeom;
int nextGeomCount;
geomStack.push(geom);
while(!geomStack.isEmpty()) {
nextGeom = geomStack.pop();
if(nextGeom instanceof Point
|| nextGeom instanceof MultiPoint
|| nextGeom instanceof LineString
|| nextGeom instanceof MultiLineString
|| nextGeom instanceof Polygon
|| nextGeom instanceof MultiPolygon) {
singleGeoms.add(nextGeom);
} else if(nextGeom instanceof GeometryCollection) {
// Push all child geometries
nextGeomCount = nextGeom.getNumGeometries();
for(int i = 0; i < nextGeomCount; ++i) {
geomStack.push(nextGeom.getGeometryN(i));
}
}
}
return singleGeoms;
}
/**
* <p>Convert JTS {@link Geometry} to a list of vector tile features.
* The Geometry should be in MVT coordinates.</p>
*
* <p>Each geometry will have its own ID.</p>
*
* @param geometry JTS geometry to convert
* @param layerProps layer properties for tagging features
* @param userDataConverter convert {@link Geometry#userData} to MVT feature tags
* @see #flatFeatureList(Geometry)
* @see #createTileGeom(Geometry, Envelope, GeometryFactory, MvtLayerParams, IGeometryFilter)
*/
public static List<VectorTile.Tile.Feature> toFeatures(Geometry geometry,
MvtLayerProps layerProps,
IUserDataConverter userDataConverter) {
return toFeatures(flatFeatureList(geometry), layerProps, userDataConverter);
}
/**
* <p>Convert a flat list of JTS {@link Geometry} to a list of vector tile features.
* The Geometry should be in MVT coordinates.</p>
*
* <p>Each geometry will have its own ID.</p>
*
* @param flatGeoms flat list of JTS geometry to convert
* @param layerProps layer properties for tagging features
* @param userDataConverter convert {@link Geometry#userData} to MVT feature tags
* @see #flatFeatureList(Geometry)
* @see #createTileGeom(Geometry, Envelope, GeometryFactory, MvtLayerParams, IGeometryFilter)
*/
public static List<VectorTile.Tile.Feature> toFeatures(Collection<Geometry> flatGeoms,
MvtLayerProps layerProps,
IUserDataConverter userDataConverter) {
// Guard: empty geometry
if(flatGeoms.isEmpty()) {
return Collections.emptyList();
}
final List<VectorTile.Tile.Feature> features = new ArrayList<>();
final Vec2d cursor = new Vec2d();
VectorTile.Tile.Feature nextFeature;
for(Geometry nextGeom : flatGeoms) {
cursor.set(0d, 0d);
nextFeature = toFeature(nextGeom, cursor, layerProps, userDataConverter);
if(nextFeature != null) {
features.add(nextFeature);
}
}
return features;
}
/**
* Create and return a feature from a geometry. Returns null on failure.
*
* @param geom flat geometry via {@link #flatFeatureList(Geometry)} that can be translated to a feature
* @param cursor vector tile cursor position
* @param layerProps layer properties for tagging features
* @return new tile feature instance, or null on failure
*/
private static VectorTile.Tile.Feature toFeature(Geometry geom,
Vec2d cursor,
MvtLayerProps layerProps,
IUserDataConverter userDataConverter) {
// Guard: UNKNOWN Geometry
final VectorTile.Tile.GeomType mvtGeomType = JtsAdapter.toGeomType(geom);
if(mvtGeomType == VectorTile.Tile.GeomType.UNKNOWN) {
return null;
}
final VectorTile.Tile.Feature.Builder featureBuilder = VectorTile.Tile.Feature.newBuilder();
final boolean mvtClosePath = MvtUtil.shouldClosePath(mvtGeomType);
final List<Integer> mvtGeom = new ArrayList<>();
featureBuilder.setType(mvtGeomType);
if(geom instanceof Point || geom instanceof MultiPoint) {
// Encode as MVT point or multipoint
mvtGeom.addAll(ptsToGeomCmds(geom, cursor));
} else if(geom instanceof LineString || geom instanceof MultiLineString) {
// Encode as MVT linestring or multi-linestring
for (int i = 0; i < geom.getNumGeometries(); ++i) {
mvtGeom.addAll(linesToGeomCmds(geom.getGeometryN(i), mvtClosePath, cursor, 1));
}
} else if(geom instanceof MultiPolygon || geom instanceof Polygon) {
// Encode as MVT polygon or multi-polygon
for(int i = 0; i < geom.getNumGeometries(); ++i) {
final Polygon nextPoly = (Polygon) geom.getGeometryN(i);
final List<Integer> nextPolyGeom = new ArrayList<>();
boolean valid = true;
// Add exterior ring
final LineString exteriorRing = nextPoly.getExteriorRing();
// Area must be non-zero
final double exteriorArea = CGAlgorithms.signedArea(exteriorRing.getCoordinates());
if(((int) Math.round(exteriorArea)) == 0) {
continue;
}
// Check CCW Winding (must be positive area)
if(exteriorArea < 0d) {
CoordinateArrays.reverse(exteriorRing.getCoordinates());
}
nextPolyGeom.addAll(linesToGeomCmds(exteriorRing, mvtClosePath, cursor, 2));
// Add interior rings
for(int ringIndex = 0; ringIndex < nextPoly.getNumInteriorRing(); ++ringIndex) {
final LineString nextInteriorRing = nextPoly.getInteriorRingN(ringIndex);
// Area must be non-zero
final double interiorArea = CGAlgorithms.signedArea(nextInteriorRing.getCoordinates());
if(((int)Math.round(interiorArea)) == 0) {
continue;
}
// Check CW Winding (must be negative area)
if(interiorArea > 0d) {
CoordinateArrays.reverse(nextInteriorRing.getCoordinates());
}
// Interior ring area must be < exterior ring area, or entire geometry is invalid
if(Math.abs(exteriorArea) <= Math.abs(interiorArea)) {
valid = false;
break;
}
nextPolyGeom.addAll(linesToGeomCmds(nextInteriorRing, mvtClosePath, cursor, 2));
}
if(valid) {
mvtGeom.addAll(nextPolyGeom);
}
}
}
if(mvtGeom.size() < 1) {
return null;
}
featureBuilder.addAllGeometry(mvtGeom);
// Feature Properties
userDataConverter.addTags(geom.getUserData(), layerProps, featureBuilder);
return featureBuilder.build();
}
/**
* <p>Convert a {@link Point} or {@link MultiPoint} geometry to a list of MVT geometry drawing commands. See
* <a href="https://github.com/mapbox/vector-tile-spec">vector-tile-spec</a>
* for details.</p>
*
* <p>WARNING: The value of the {@code cursor} parameter is modified as a result of calling this method.</p>
*
* @param geom input of type {@link Point} or {@link MultiPoint}. Type is NOT checked and expected to be correct.
* @param cursor modified during processing to contain next MVT cursor position
* @return list of commands
*/
private static List<Integer> ptsToGeomCmds(final Geometry geom, final Vec2d cursor) {
// Guard: empty geometry coordinates
final Coordinate[] geomCoords = geom.getCoordinates();
if(geomCoords.length <= 0) {
Collections.emptyList();
}
/** Tile commands and parameters */
final List<Integer> geomCmds = new ArrayList<>(geomCmdBuffLenPts(geomCoords.length));
/** Holds next MVT coordinate */
final Vec2d mvtPos = new Vec2d();
/** Length of 'MoveTo' draw command */
int moveCmdLen = 0;
// Insert placeholder for 'MoveTo' command header
geomCmds.add(0);
Coordinate nextCoord;
for(int i = 0; i < geomCoords.length; ++i) {
nextCoord = geomCoords[i];
mvtPos.set(nextCoord.x, nextCoord.y);
// Ignore duplicate MVT points
if(i == 0 || !equalAsInts(cursor, mvtPos)) {
++moveCmdLen;
moveCursor(cursor, geomCmds, mvtPos);
}
}
if(moveCmdLen <= GeomCmdHdr.CMD_HDR_LEN_MAX) {
// Write 'MoveTo' command header to first index
geomCmds.set(0, GeomCmdHdr.cmdHdr(GeomCmd.MoveTo, moveCmdLen));
return geomCmds;
} else {
// Invalid geometry, need at least 1 'MoveTo' value to make points
return Collections.emptyList();
}
}
/**
* <p>Convert a {@link LineString} or {@link Polygon} to a list of MVT geometry drawing commands.
* A {@link MultiLineString} or {@link MultiPolygon} can be encoded by calling this method multiple times.</p>
*
* <p>See <a href="https://github.com/mapbox/vector-tile-spec">vector-tile-spec</a> for details.</p>
*
* <p>WARNING: The value of the {@code cursor} parameter is modified as a result of calling this method.</p>
*
* @param geom input of type {@link LineString} or {@link Polygon}. Type is NOT checked and expected to be correct.
* @param closeEnabled whether a 'ClosePath' command should terminate the command list
* @param cursor modified during processing to contain next MVT cursor position
* @param minLineToLen minimum allowed length for LineTo command.
* @return list of commands
*/
private static List<Integer> linesToGeomCmds(
final Geometry geom,
final boolean closeEnabled,
final Vec2d cursor,
final int minLineToLen) {
final Coordinate[] geomCoords = geom.getCoordinates();
// Check geometry for repeated end points
final int repeatEndCoordCount = countCoordRepeatReverse(geomCoords);
final int minExpGeomCoords = geomCoords.length - repeatEndCoordCount;
// Guard/Optimization: Not enough geometry coordinates for a line
if(minExpGeomCoords < 2) {
Collections.emptyList();
}
/** Tile commands and parameters */
final List<Integer> geomCmds = new ArrayList<>(geomCmdBuffLenLines(minExpGeomCoords, closeEnabled));
/** Holds next MVT coordinate */
final Vec2d mvtPos = new Vec2d();
// Initial coordinate
Coordinate nextCoord = geomCoords[0];
mvtPos.set(nextCoord.x, nextCoord.y);
// Encode initial 'MoveTo' command
geomCmds.add(GeomCmdHdr.cmdHdr(GeomCmd.MoveTo, 1));
moveCursor(cursor, geomCmds, mvtPos);
/** Index of 'LineTo' 'command header' */
final int lineToCmdHdrIndex = geomCmds.size();
// Insert placeholder for 'LineTo' command header
geomCmds.add(0);
/** Length of 'LineTo' draw command */
int lineToLength = 0;
for(int i = 1; i < minExpGeomCoords; ++i) {
nextCoord = geomCoords[i];
mvtPos.set(nextCoord.x, nextCoord.y);
// Ignore duplicate MVT points in sequence
if(!equalAsInts(cursor, mvtPos)) {
++lineToLength;
moveCursor(cursor, geomCmds, mvtPos);
}
}
if(lineToLength >= minLineToLen && lineToLength <= GeomCmdHdr.CMD_HDR_LEN_MAX) {
// Write 'LineTo' 'command header'
geomCmds.set(lineToCmdHdrIndex, GeomCmdHdr.cmdHdr(GeomCmd.LineTo, lineToLength));
if(closeEnabled) {
geomCmds.add(GeomCmdHdr.closePathCmdHdr());
}
return geomCmds;
} else {
// Invalid geometry, need at least 1 'LineTo' value to make a Multiline or Polygon
return Collections.emptyList();
}
}
/**
* <p>Count number of coordinates starting from the end of the coordinate array backwards
* that match the first coordinate value.</p>
*
* <p>Useful for ensuring self-closing line strings do not repeat the first coordinate.</p>
*
* @param coords coordinates to check for duplicate points
* @return number of duplicate points at the rear of the list
*/
private static int countCoordRepeatReverse(Coordinate[] coords) {
int repeatCoords = 0;
final Coordinate firstCoord = coords[0];
Coordinate nextCoord;
for(int i = coords.length - 1; i > 0; --i) {
nextCoord = coords[i];
if(equalAsInts2d(firstCoord, nextCoord)) {
++repeatCoords;
} else {
break;
}
}
return repeatCoords;
}
/**
* <p>Appends {@link ZigZag#encode(int)} of delta in x,y from {@code cursor} to {@code mvtPos} into the {@code geomCmds} buffer.</p>
*
* <p>Afterwards, the {@code cursor} values are changed to match the {@code mvtPos} values.</p>
*
* @param cursor MVT cursor position
* @param geomCmds geometry command list
* @param mvtPos next MVT cursor position
*/
private static void moveCursor(Vec2d cursor, List<Integer> geomCmds, Vec2d mvtPos) {
// Delta, then zigzag
geomCmds.add(ZigZag.encode((int)mvtPos.x - (int)cursor.x));
geomCmds.add(ZigZag.encode((int)mvtPos.y - (int)cursor.y));
cursor.set(mvtPos);
}
/**
* Return true if the values of the two {@link Coordinate} are equal when their
* first and second ordinates are cast as ints. Ignores 3rd ordinate.
*
* @param a first coordinate to compare
* @param b second coordinate to compare
* @return true if the values of the two {@link Coordinate} are equal when their
* first and second ordinates are cast as ints
*/
private static boolean equalAsInts2d(Coordinate a, Coordinate b) {
return ((int)a.getOrdinate(0)) == ((int)b.getOrdinate(0))
&& ((int)a.getOrdinate(1)) == ((int)b.getOrdinate(1));
}
/**
* Return true if the values of the two vectors are equal when cast as ints.
*
* @param a first vector to compare
* @param b second vector to compare
* @return true if the values of the two vectors are equal when cast as ints
*/
private static boolean equalAsInts(Vec2d a, Vec2d b) {
return ((int) a.x) == ((int) b.x) && ((int) a.y) == ((int) b.y);
}
/**
* Get required geometry buffer size for a {@link Point} or {@link MultiPoint} geometry.
*
* @param coordCount coordinate count for the geometry
* @return required geometry buffer length
*/
private static int geomCmdBuffLenPts(int coordCount) {
// 1 MoveTo Header, 2 parameters * coordCount
return 1 + (coordCount * 2);
}
/**
* Get required geometry buffer size for a {@link LineString} or {@link Polygon} geometry.
*
* @param coordCount coordinate count for the geometry
* @param closeEnabled whether a 'ClosePath' command should terminate the command list
* @return required geometry buffer length
*/
private static int geomCmdBuffLenLines(int coordCount, boolean closeEnabled) {
// MoveTo Header, LineTo Header, Optional ClosePath Header, 2 parameters * coordCount
return 2 + (closeEnabled ? 1 : 0) + (coordCount * 2);
}
}

View file

@ -0,0 +1,599 @@
package com.wdtinc.mapbox_vector_tile.adapt.jts;
import com.vividsolutions.jts.algorithm.CGAlgorithms;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.CoordinateSequence;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.LinearRing;
import com.vividsolutions.jts.geom.MultiLineString;
import com.vividsolutions.jts.geom.MultiPoint;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;
import com.wdtinc.mapbox_vector_tile.encoding.GeomCmd;
import com.wdtinc.mapbox_vector_tile.encoding.GeomCmdHdr;
import com.wdtinc.mapbox_vector_tile.encoding.ZigZag;
import com.wdtinc.mapbox_vector_tile.util.Vec2d;
import net.osmand.binary.VectorTile;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
/**
* Load Mapbox Vector Tiles (MVT) to JTS {@link Geometry}. Feature tags may be converted
* to user data via {@link ITagConverter}.
*/
public final class MvtReader {
private static final int MIN_LINE_STRING_LEN = 6; // MoveTo,1 + LineTo,1
private static final int MIN_POLYGON_LEN = 9; // MoveTo,1 + LineTo,2 + ClosePath
/**
* Convenience method for loading MVT from file.
* See {@link #loadMvt(InputStream, GeometryFactory, ITagConverter, RingClassifier)}.
* Uses {@link #RING_CLASSIFIER_V2_1} for forming Polygons and MultiPolygons.
*
* @param f MVT file
* @param geomFactory allows for JTS geometry creation
* @param tagConverter converts MVT feature tags to JTS user data object
* @return JTS geometries in using MVT coordinates
* @throws IOException failure reading MVT from path
* @see #loadMvt(InputStream, GeometryFactory, ITagConverter, RingClassifier)
* @see Geometry
* @see Geometry#getUserData()
* @see RingClassifier
*/
public static List<Geometry> loadMvt(File f,
GeometryFactory geomFactory,
ITagConverter tagConverter) throws IOException {
return loadMvt(f, geomFactory, tagConverter, RING_CLASSIFIER_V2_1);
}
/**
* Convenience method for loading MVT from file.
* See {@link #loadMvt(InputStream, GeometryFactory, ITagConverter, RingClassifier)}.
*
* @param f MVT file
* @param geomFactory allows for JTS geometry creation
* @param tagConverter converts MVT feature tags to JTS user data object
* @param ringClassifier determines how rings are parsed into Polygons and MultiPolygons
* @return JTS geometries in using MVT coordinates
* @throws IOException failure reading MVT from path
* @see #loadMvt(InputStream, GeometryFactory, ITagConverter, RingClassifier)
* @see Geometry
* @see Geometry#getUserData()
* @see RingClassifier
*/
public static List<Geometry> loadMvt(File f,
GeometryFactory geomFactory,
ITagConverter tagConverter,
RingClassifier ringClassifier) throws IOException {
final InputStream is = new FileInputStream(f);
return loadMvt(is, geomFactory, tagConverter, ringClassifier);
}
/**
* Load an MVT to JTS geometries using coordinates. Uses {@code tagConverter} to create user data
* from feature properties. Uses {@link #RING_CLASSIFIER_V2_1} for forming Polygons and MultiPolygons.
*
* @param is stream with MVT data
* @param geomFactory allows for JTS geometry creation
* @param tagConverter converts MVT feature tags to JTS user data object.
* @return JTS geometries in using MVT coordinates
* @throws IOException failure reading MVT from stream
* @see Geometry
* @see Geometry#getUserData()
* @see RingClassifier
*/
public static List<Geometry> loadMvt(InputStream is,
GeometryFactory geomFactory,
ITagConverter tagConverter) throws IOException {
return loadMvt(is, geomFactory, tagConverter, RING_CLASSIFIER_V2_1);
}
/**
* Load an MVT to JTS geometries using coordinates. Uses {@code tagConverter} to create user data
* from feature properties.
*
* @param is stream with MVT data
* @param geomFactory allows for JTS geometry creation
* @param tagConverter converts MVT feature tags to JTS user data object.
* @param ringClassifier determines how rings are parsed into Polygons and MultiPolygons
* @return JTS geometries in using MVT coordinates
* @throws IOException failure reading MVT from stream
* @see Geometry
* @see Geometry#getUserData()
* @see RingClassifier
*/
public static List<Geometry> loadMvt(InputStream is,
GeometryFactory geomFactory,
ITagConverter tagConverter,
RingClassifier ringClassifier) throws IOException {
final List<Geometry> tileGeoms = new ArrayList<>();
final VectorTile.Tile mvt = VectorTile.Tile.parseFrom(is);
final Vec2d cursor = new Vec2d();
for (VectorTile.Tile.Layer nextLayer : mvt.getLayersList()) {
final List<String> keysList = nextLayer.getKeysList();
final List<VectorTile.Tile.Value> valuesList = nextLayer.getValuesList();
for (VectorTile.Tile.Feature nextFeature : nextLayer.getFeaturesList()) {
final Long id = nextFeature.hasId() ? nextFeature.getId() : null;
final VectorTile.Tile.GeomType geomType = nextFeature.getType();
if (geomType == VectorTile.Tile.GeomType.UNKNOWN) {
continue;
}
final List<Integer> geomCmds = nextFeature.getGeometryList();
cursor.set(0d, 0d);
final Geometry nextGeom = readGeometry(geomCmds, geomType, geomFactory, cursor, ringClassifier);
if (nextGeom != null) {
tileGeoms.add(nextGeom);
nextGeom.setUserData(tagConverter.toUserData(id, nextFeature.getTagsList(), keysList, valuesList));
}
}
}
return tileGeoms;
}
private static Geometry readGeometry(List<Integer> geomCmds,
VectorTile.Tile.GeomType geomType,
GeometryFactory geomFactory,
Vec2d cursor,
RingClassifier ringClassifier) {
Geometry result = null;
switch (geomType) {
case POINT:
result = readPoints(geomFactory, geomCmds, cursor);
break;
case LINESTRING:
result = readLines(geomFactory, geomCmds, cursor);
break;
case POLYGON:
result = readPolys(geomFactory, geomCmds, cursor, ringClassifier);
break;
default:
//LoggerFactory.getLogger(MvtReader.class).error("readGeometry(): Unhandled geometry type [{}]", geomType);
}
return result;
}
/**
* Create {@link Point} or {@link MultiPoint} from MVT geometry drawing commands.
*
* @param geomFactory creates JTS geometry
* @param geomCmds contains MVT geometry commands
* @param cursor contains current MVT extent position
* @return JTS geometry or null on failure
*/
private static Geometry readPoints(GeometryFactory geomFactory, List<Integer> geomCmds, Vec2d cursor) {
// Guard: must have header
if (geomCmds.isEmpty()) {
return null;
}
/** Geometry command index */
int i = 0;
// Read command header
final int cmdHdr = geomCmds.get(i++);
final int cmdLength = GeomCmdHdr.getCmdLength(cmdHdr);
final GeomCmd cmd = GeomCmdHdr.getCmd(cmdHdr);
// Guard: command type
if (cmd != GeomCmd.MoveTo) {
return null;
}
// Guard: minimum command length
if (cmdLength < 1) {
return null;
}
// Guard: header data unsupported by geometry command buffer
// (require header and at least 1 value * 2 params)
if (cmdLength * GeomCmd.MoveTo.getParamCount() + 1 > geomCmds.size()) {
return null;
}
final CoordinateSequence coordSeq = geomFactory.getCoordinateSequenceFactory().create(cmdLength, 2);
int coordIndex = 0;
Coordinate nextCoord;
while (i < geomCmds.size() - 1) {
cursor.add(
ZigZag.decode(geomCmds.get(i++)),
ZigZag.decode(geomCmds.get(i++))
);
nextCoord = coordSeq.getCoordinate(coordIndex++);
nextCoord.setOrdinate(0, cursor.x);
nextCoord.setOrdinate(1, cursor.y);
}
return coordSeq.size() == 1 ? geomFactory.createPoint(coordSeq) : geomFactory.createMultiPoint(coordSeq);
}
/**
* Create {@link LineString} or {@link MultiLineString} from MVT geometry drawing commands.
*
* @param geomFactory creates JTS geometry
* @param geomCmds contains MVT geometry commands
* @param cursor contains current MVT extent position
* @return JTS geometry or null on failure
*/
private static Geometry readLines(GeometryFactory geomFactory, List<Integer> geomCmds, Vec2d cursor) {
// Guard: must have header
if (geomCmds.isEmpty()) {
return null;
}
/** Geometry command index */
int i = 0;
int cmdHdr;
int cmdLength;
GeomCmd cmd;
List<LineString> geoms = new ArrayList<>(1);
CoordinateSequence nextCoordSeq;
Coordinate nextCoord;
while (i <= geomCmds.size() - MIN_LINE_STRING_LEN) {
// --------------------------------------------
// Expected: MoveTo command of length 1
// --------------------------------------------
// Read command header
cmdHdr = geomCmds.get(i++);
cmdLength = GeomCmdHdr.getCmdLength(cmdHdr);
cmd = GeomCmdHdr.getCmd(cmdHdr);
// Guard: command type and length
if (cmd != GeomCmd.MoveTo || cmdLength != 1) {
break;
}
// Update cursor position with relative move
cursor.add(
ZigZag.decode(geomCmds.get(i++)),
ZigZag.decode(geomCmds.get(i++))
);
// --------------------------------------------
// Expected: LineTo command of length > 0
// --------------------------------------------
// Read command header
cmdHdr = geomCmds.get(i++);
cmdLength = GeomCmdHdr.getCmdLength(cmdHdr);
cmd = GeomCmdHdr.getCmd(cmdHdr);
// Guard: command type and length
if (cmd != GeomCmd.LineTo || cmdLength < 1) {
break;
}
// Guard: header data length unsupported by geometry command buffer
// (require at least (1 value * 2 params) + current_index)
if ((cmdLength * GeomCmd.LineTo.getParamCount()) + i > geomCmds.size()) {
break;
}
nextCoordSeq = geomFactory.getCoordinateSequenceFactory().create(1 + cmdLength, 2);
// Set first point from MoveTo command
nextCoord = nextCoordSeq.getCoordinate(0);
nextCoord.setOrdinate(0, cursor.x);
nextCoord.setOrdinate(1, cursor.y);
// Set remaining points from LineTo command
for (int lineToIndex = 0; lineToIndex < cmdLength; ++lineToIndex) {
// Update cursor position with relative line delta
cursor.add(
ZigZag.decode(geomCmds.get(i++)),
ZigZag.decode(geomCmds.get(i++))
);
nextCoord = nextCoordSeq.getCoordinate(lineToIndex + 1);
nextCoord.setOrdinate(0, cursor.x);
nextCoord.setOrdinate(1, cursor.y);
}
geoms.add(geomFactory.createLineString(nextCoordSeq));
}
return geoms.size() == 1 ? geoms.get(0) : geomFactory.createMultiLineString(geoms.toArray(new LineString[geoms.size()]));
}
/**
* Create {@link Polygon} or {@link MultiPolygon} from MVT geometry drawing commands.
*
* @param geomFactory creates JTS geometry
* @param geomCmds contains MVT geometry commands
* @param cursor contains current MVT extent position
* @param ringClassifier
* @return JTS geometry or null on failure
*/
private static Geometry readPolys(GeometryFactory geomFactory,
List<Integer> geomCmds,
Vec2d cursor,
RingClassifier ringClassifier) {
// Guard: must have header
if (geomCmds.isEmpty()) {
return null;
}
/** Geometry command index */
int i = 0;
int cmdHdr;
int cmdLength;
GeomCmd cmd;
List<LinearRing> rings = new ArrayList<>(1);
CoordinateSequence nextCoordSeq;
Coordinate nextCoord;
while (i <= geomCmds.size() - MIN_POLYGON_LEN) {
// --------------------------------------------
// Expected: MoveTo command of length 1
// --------------------------------------------
// Read command header
cmdHdr = geomCmds.get(i++);
cmdLength = GeomCmdHdr.getCmdLength(cmdHdr);
cmd = GeomCmdHdr.getCmd(cmdHdr);
// Guard: command type and length
if (cmd != GeomCmd.MoveTo || cmdLength != 1) {
break;
}
// Update cursor position with relative move
cursor.add(
ZigZag.decode(geomCmds.get(i++)),
ZigZag.decode(geomCmds.get(i++))
);
// --------------------------------------------
// Expected: LineTo command of length > 1
// --------------------------------------------
// Read command header
cmdHdr = geomCmds.get(i++);
cmdLength = GeomCmdHdr.getCmdLength(cmdHdr);
cmd = GeomCmdHdr.getCmd(cmdHdr);
// Guard: command type and length
if (cmd != GeomCmd.LineTo || cmdLength < 2) {
break;
}
// Guard: header data length unsupported by geometry command buffer
// (require at least (2 values * 2 params) + (current index 'i') + (1 for ClosePath))
if ((cmdLength * GeomCmd.LineTo.getParamCount()) + i + 1 > geomCmds.size()) {
break;
}
nextCoordSeq = geomFactory.getCoordinateSequenceFactory().create(2 + cmdLength, 2);
// Set first point from MoveTo command
nextCoord = nextCoordSeq.getCoordinate(0);
nextCoord.setOrdinate(0, cursor.x);
nextCoord.setOrdinate(1, cursor.y);
// Set remaining points from LineTo command
for (int lineToIndex = 0; lineToIndex < cmdLength; ++lineToIndex) {
// Update cursor position with relative line delta
cursor.add(
ZigZag.decode(geomCmds.get(i++)),
ZigZag.decode(geomCmds.get(i++))
);
nextCoord = nextCoordSeq.getCoordinate(lineToIndex + 1);
nextCoord.setOrdinate(0, cursor.x);
nextCoord.setOrdinate(1, cursor.y);
}
// --------------------------------------------
// Expected: ClosePath command of length 1
// --------------------------------------------
// Read command header
cmdHdr = geomCmds.get(i++);
cmdLength = GeomCmdHdr.getCmdLength(cmdHdr);
cmd = GeomCmdHdr.getCmd(cmdHdr);
if (cmd != GeomCmd.ClosePath || cmdLength != 1) {
break;
}
// Set last point from ClosePath command
nextCoord = nextCoordSeq.getCoordinate(nextCoordSeq.size() - 1);
nextCoord.setOrdinate(0, nextCoordSeq.getOrdinate(0, 0));
nextCoord.setOrdinate(1, nextCoordSeq.getOrdinate(0, 1));
rings.add(geomFactory.createLinearRing(nextCoordSeq));
}
// Classify rings
final List<Polygon> polygons = ringClassifier.classifyRings(rings, geomFactory);
if (polygons.size() < 1) {
return null;
} else if (polygons.size() == 1) {
return polygons.get(0);
} else {
return geomFactory.createMultiPolygon(polygons.toArray(new Polygon[polygons.size()]));
}
}
/**
* Classifies Polygon and MultiPolygon rings.
*/
public interface RingClassifier {
/**
* <p>Classify a list of rings into polygons using surveyor formula.</p>
* <p>
* <p>Zero-area polygons are removed.</p>
*
* @param rings linear rings to classify into polygons
* @param geomFactory creates JTS geometry
* @return polygons from classified rings
*/
List<Polygon> classifyRings(List<LinearRing> rings, GeometryFactory geomFactory);
}
/**
* Area for surveyor formula may be positive or negative for exterior rings. Mimics Mapbox parsers supporting V1.
*/
public static final RingClassifier RING_CLASSIFIER_V1 = new PolyRingClassifierV1();
/**
* Area from surveyor formula must be positive for exterior rings. Obeys V2.1 spec.
*/
public static final RingClassifier RING_CLASSIFIER_V2_1 = new PolyRingClassifierV2_1();
/**
* Area from surveyor formula must be positive for exterior rings. Obeys V2.1 spec.
*
* @see CGAlgorithms#signedArea(Coordinate[])
*/
private static final class PolyRingClassifierV2_1 implements RingClassifier {
@Override
public List<Polygon> classifyRings(List<LinearRing> rings, GeometryFactory geomFactory) {
final List<Polygon> polygons = new ArrayList<>();
final List<LinearRing> holes = new ArrayList<>();
double outerArea = 0d;
LinearRing outerPoly = null;
for (LinearRing r : rings) {
double area = CGAlgorithms.signedArea(r.getCoordinates());
if (!r.isRing()) {
continue; // sanity check, could probably be handled in a isSimple() check
}
if (area == 0d) {
continue; // zero-area
}
if (area > 0d) {
if (outerPoly != null) {
polygons.add(geomFactory.createPolygon(outerPoly, holes.toArray(new LinearRing[holes.size()])));
holes.clear();
}
// Pos --> CCW, Outer
outerPoly = r;
outerArea = area;
} else {
if (Math.abs(outerArea) < Math.abs(area)) {
continue; // Holes must have less area, could probably be handled in a isSimple() check
}
// Neg --> CW, Hole
holes.add(r);
}
}
if (outerPoly != null) {
holes.toArray();
polygons.add(geomFactory.createPolygon(outerPoly, holes.toArray(new LinearRing[holes.size()])));
}
return polygons;
}
}
/**
* Area for surveyor formula may be positive or negative for exterior rings. Mimics Mapbox parsers supporting V1.
*
* @see CGAlgorithms#signedArea(Coordinate[])
*/
private static final class PolyRingClassifierV1 implements RingClassifier {
@Override
public List<Polygon> classifyRings(List<LinearRing> rings, GeometryFactory geomFactory) {
final List<Polygon> polygons = new ArrayList<>();
final List<LinearRing> holes = new ArrayList<>();
double outerArea = 0d;
LinearRing outerPoly = null;
for (LinearRing r : rings) {
double area = CGAlgorithms.signedArea(r.getCoordinates());
if (!r.isRing()) {
continue; // sanity check, could probably be handled in a isSimple() check
}
if (area == 0d) {
continue; // zero-area
}
if (outerPoly == null || (outerArea < 0 == area < 0)) {
if (outerPoly != null) {
polygons.add(geomFactory.createPolygon(outerPoly, holes.toArray(new LinearRing[holes.size()])));
holes.clear();
}
// Pos --> CCW, Outer
outerPoly = r;
outerArea = area;
} else {
if (Math.abs(outerArea) < Math.abs(area)) {
continue; // Holes must have less area, could probably be handled in a isSimple() check
}
// Neg --> CW, Hole
holes.add(r);
}
}
if (outerPoly != null) {
holes.toArray();
polygons.add(geomFactory.createPolygon(outerPoly, holes.toArray(new LinearRing[holes.size()])));
}
return polygons;
}
}
}

View file

@ -0,0 +1,33 @@
package com.wdtinc.mapbox_vector_tile.adapt.jts;
import com.vividsolutions.jts.geom.CoordinateSequence;
import com.vividsolutions.jts.geom.CoordinateSequenceFilter;
/**
* <p>Round each coordinate value to an integer.</p>
*
* <p>Mapbox vector tiles have fixed precision. This filter can be useful for reducing precision to
* the extent of a MVT.</p>
*/
public final class RoundingFilter implements CoordinateSequenceFilter {
public static final RoundingFilter INSTANCE = new RoundingFilter();
private RoundingFilter() {}
@Override
public void filter(CoordinateSequence seq, int i) {
seq.setOrdinate(i, 0, Math.round(seq.getOrdinate(i, 0)));
seq.setOrdinate(i, 1, Math.round(seq.getOrdinate(i, 1)));
}
@Override
public boolean isDone() {
return false;
}
@Override
public boolean isGeometryChanged() {
return true;
}
}

View file

@ -0,0 +1,18 @@
package com.wdtinc.mapbox_vector_tile.adapt.jts;
import net.osmand.binary.VectorTile;
import java.util.List;
/**
* Ignores tags, always returns null.
*
* @see ITagConverter
*/
public final class TagIgnoreConverter implements ITagConverter {
@Override
public Object toUserData(Long id, List<Integer> tags, List<String> keysList,
List<VectorTile.Tile.Value> valuesList) {
return null;
}
}

View file

@ -0,0 +1,99 @@
package com.wdtinc.mapbox_vector_tile.adapt.jts;
import com.wdtinc.mapbox_vector_tile.encoding.MvtValue;
import net.osmand.binary.VectorTile;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* Convert MVT tags list to a {@link Map} of {@link String} to {@link Object}. Tags indices that are out
* of range of the key or value list are ignored.
*
* @see ITagConverter
*/
public final class TagKeyValueMapConverter implements ITagConverter {
/** If true, return null user data when tags are empty */
private final boolean nullIfEmpty;
/** If true, add id to user data object */
private final boolean addId;
/** The {@link Map} key for the feature id. */
private final String idKey;
/**
* Always created user data object, even with empty tags. Ignore feature ids.
*/
public TagKeyValueMapConverter() {
this(false);
}
/**
* Ignore feature ids.
*
* @param nullIfEmpty if true, return null user data when tags are empty
*/
public TagKeyValueMapConverter(boolean nullIfEmpty) {
this.nullIfEmpty = nullIfEmpty;
this.addId = false;
this.idKey = null;
}
/**
* Store feature ids using idKey. Id value may be null if not present.
*
* @param nullIfEmpty if true, return null user data when tags are empty
* @param idKey key name to use for feature id value
*/
public TagKeyValueMapConverter(boolean nullIfEmpty, String idKey) {
if (idKey == null) {
throw new NullPointerException();
}
this.nullIfEmpty = nullIfEmpty;
this.addId = true;
this.idKey = idKey;
}
@Override
public Object toUserData(Long id, List<Integer> tags, List<String> keysList,
List<VectorTile.Tile.Value> valuesList) {
// Guard: empty
if(nullIfEmpty && tags.size() < 1 && (!addId || id == null)) {
return null;
}
final Map<String, Object> userData = new HashMap<>(((tags.size() + 1) / 2));
// Add feature properties
int keyIndex;
int valIndex;
boolean valid;
for(int i = 0; i < tags.size() - 1; i += 2) {
keyIndex = tags.get(i);
valIndex = tags.get(i + 1);
valid = keyIndex >= 0 && keyIndex < keysList.size()
&& valIndex >= 0 && valIndex < valuesList.size();
if(valid) {
userData.put(keysList.get(keyIndex), MvtValue.toObject(valuesList.get(valIndex)));
}
}
// Add ID, value may be null
if(addId) {
userData.put(idKey, id);
}
return userData;
}
}

View file

@ -0,0 +1,35 @@
package com.wdtinc.mapbox_vector_tile.adapt.jts;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.wdtinc.mapbox_vector_tile.build.MvtLayerParams;
import java.util.List;
/**
* Processing result containing intersection geometry and MVT geometry.
*
* @see JtsAdapter#createTileGeom(Geometry, Envelope, GeometryFactory, MvtLayerParams, IGeometryFilter)
*/
public final class TileGeomResult {
/** Intersection geometry (projection units and coordinates) */
public final List<Geometry> intGeoms;
/** Geometry in MVT coordinates (tile extent units, screen coordinates) */
public final List<Geometry> mvtGeoms;
/**
* @param intGeoms geometry intersecting tile
* @param mvtGeoms geometry for MVT
* @throws NullPointerException if intGeoms or mvtGeoms are null
*/
public TileGeomResult(List<Geometry> intGeoms, List<Geometry> mvtGeoms) {
if(intGeoms == null || mvtGeoms == null) {
throw new NullPointerException();
}
this.intGeoms = intGeoms;
this.mvtGeoms = mvtGeoms;
}
}

View file

@ -0,0 +1,15 @@
package com.wdtinc.mapbox_vector_tile.adapt.jts;
import com.wdtinc.mapbox_vector_tile.build.MvtLayerProps;
import net.osmand.binary.VectorTile;
/**
* Ignores user data, does not take any action.
*
* @see IUserDataConverter
*/
public final class UserDataIgnoreConverter implements IUserDataConverter {
@Override
public void addTags(Object userData, MvtLayerProps layerProps, VectorTile.Tile.Feature.Builder featureBuilder) {}
}

View file

@ -0,0 +1,88 @@
package com.wdtinc.mapbox_vector_tile.adapt.jts;
import com.wdtinc.mapbox_vector_tile.build.MvtLayerProps;
import net.osmand.binary.VectorTile;
import java.util.Map;
import java.util.Objects;
/**
* Convert simple user data {@link Map} where the keys are {@link String} and values are {@link Object}. Supports
* converting a specific map key to a user id. If the key to user id conversion fails, the error occurs silently
* and the id is discarded.
*
* @see IUserDataConverter
*/
public final class UserDataKeyValueMapConverter implements IUserDataConverter {
/** If true, set feature id from user data */
private final boolean setId;
/** The {@link Map} key for the feature id. */
private final String idKey;
/**
* Does not set feature id.
*/
public UserDataKeyValueMapConverter() {
this.setId = false;
this.idKey = null;
}
/**
* Tries to set feature id using provided user data {@link Map} key.
*
* @param idKey user data {@link Map} key for getting id value.
*/
public UserDataKeyValueMapConverter(String idKey) {
if (idKey == null) {
throw new NullPointerException();
}
this.setId = true;
this.idKey = idKey;
}
@Override
public void addTags(Object userData, MvtLayerProps layerProps, VectorTile.Tile.Feature.Builder featureBuilder) {
if(userData != null) {
try {
@SuppressWarnings("unchecked")
final Map<String, Object> userDataMap = (Map<String, Object>)userData;
for(Map.Entry<String, Object> e :userDataMap.entrySet()){
final String key = e.getKey();
final Object value = e.getValue();
if(key != null && value != null) {
final int valueIndex = layerProps.addValue(value);
if(valueIndex >= 0) {
featureBuilder.addTags(layerProps.addKey(key));
featureBuilder.addTags(valueIndex);
}
}
}
// Set feature id value
if(setId) {
final Object idValue = userDataMap.get(idKey);
if (idValue != null) {
if(idValue instanceof Long || idValue instanceof Integer
|| idValue instanceof Float || idValue instanceof Double
|| idValue instanceof Byte || idValue instanceof Short) {
featureBuilder.setId((long)idValue);
} else if(idValue instanceof String) {
try {
featureBuilder.setId(Long.valueOf((String)idValue));
} catch (NumberFormatException ignored) {}
}
}
}
} catch (ClassCastException e) {
//LoggerFactory.getLogger(UserDataKeyValueMapConverter.class).error(e.getMessage(), e);
}
}
}
}

View file

@ -0,0 +1,63 @@
package com.wdtinc.mapbox_vector_tile.encoding;
/**
* MVT draw command types.
*
* @see GeomCmdHdr
*/
public enum GeomCmd {
MoveTo(1, 2),
LineTo(2, 2),
ClosePath(7, 0);
/** Unique command ID */
private final int cmdId;
/** Amount of parameters that follow the command */
private final int paramCount;
GeomCmd(int cmdId, int paramCount) {
this.cmdId = cmdId;
this.paramCount = paramCount;
}
/**
* @return unique command ID
*/
public int getCmdId() {
return cmdId;
}
/**
* @return amount of parameters that follow the command
*/
public int getParamCount() {
return paramCount;
}
/**
* Return matching {@link GeomCmd} for the provided cmdId, or null if there is not
* a matching command.
*
* @param cmdId command id to find match for
* @return command with matching id, or null if there is not a matching command
*/
public static GeomCmd fromId(int cmdId) {
final GeomCmd geomCmd;
switch (cmdId) {
case 1:
geomCmd = MoveTo;
break;
case 2:
geomCmd = LineTo;
break;
case 7:
geomCmd = ClosePath;
break;
default:
geomCmd = null;
}
return geomCmd;
}
}

View file

@ -0,0 +1,68 @@
package com.wdtinc.mapbox_vector_tile.encoding;
/**
* Utilities for working with geometry command headers.
*
* @see GeomCmd
*/
public final class GeomCmdHdr {
private static int CLOSE_PATH_HDR = cmdHdr(GeomCmd.ClosePath, 1);
/**
* <p>Encodes a 'command header' with the first 3 LSB as the command id, the remaining bits
* as the command length. See the vector-tile-spec for details.</p>
*
* @param cmd command to execute
* @param length how many times the command is repeated
* @return encoded 'command header' integer
*/
public static int cmdHdr(GeomCmd cmd, int length) {
return (cmd.getCmdId() & 0x7) | (length << 3);
}
/**
* Get the length component from the 'command header' integer.
*
* @param cmdHdr encoded 'command header' integer
* @return command length
*/
public static int getCmdLength(int cmdHdr) {
return cmdHdr >> 3;
}
/**
* Get the id component from the 'command header' integer.
*
* @param cmdHdr encoded 'command header' integer
* @return command id
*/
public static int getCmdId(int cmdHdr) {
return cmdHdr & 0x7;
}
/**
* Get the id component from the 'command header' integer, then find the
* {@link GeomCmd} with a matching id.
*
* @param cmdHdr encoded 'command header' integer
* @return command with matching id, or null if a match could not be made
*/
public static GeomCmd getCmd(int cmdHdr) {
final int cmdId = getCmdId(cmdHdr);
return GeomCmd.fromId(cmdId);
}
/**
* @return encoded 'command header' integer for {@link GeomCmd#ClosePath}.
*/
public static int closePathCmdHdr() {
return CLOSE_PATH_HDR;
}
/**
* Maximum allowed 'command header' length value.
*/
public static final int CMD_HDR_LEN_MAX = (int) (Math.pow(2, 29) - 1);
}

View file

@ -0,0 +1,33 @@
package com.wdtinc.mapbox_vector_tile.encoding;
import net.osmand.binary.VectorTile;
/**
* <p>Useful misc operations for encoding 'Mapbox Vector Tiles'.</p>
*
* <p>See: <a href="https://github.com/mapbox/vector-tile-spec">https://github.com/mapbox/vector-tile-spec</a></p>
*/
public final class MvtUtil {
/**
* Return whether the MVT geometry type should be closed with a {@link GeomCmd#ClosePath}.
*
* @param geomType the type of MVT geometry
* @return true if the geometry should be closed, false if it should not be closed
*/
public static boolean shouldClosePath(VectorTile.Tile.GeomType geomType) {
final boolean closeReq;
switch(geomType) {
case POLYGON:
closeReq = true;
break;
default:
closeReq = false;
break;
}
return closeReq;
}
}

View file

@ -0,0 +1,94 @@
package com.wdtinc.mapbox_vector_tile.encoding;
import net.osmand.binary.VectorTile;
/**
* Utility class for working with {@link VectorTile.Tile.Value} instances.
*
* @see VectorTile.Tile.Value
*/
public final class MvtValue {
/**
* Covert an {@link Object} to a new {@link VectorTile.Tile.Value} instance.
*
* @param value target for conversion
* @return new instance with String or primitive value set
*/
public static VectorTile.Tile.Value toValue(Object value) {
final VectorTile.Tile.Value.Builder tileValue = VectorTile.Tile.Value.newBuilder();
if(value instanceof Boolean) {
tileValue.setBoolValue((Boolean) value);
} else if(value instanceof Integer) {
tileValue.setSintValue((Integer) value);
} else if(value instanceof Long) {
tileValue.setSintValue((Long) value);
} else if(value instanceof Float) {
tileValue.setFloatValue((Float) value);
} else if(value instanceof Double) {
tileValue.setDoubleValue((Double) value);
} else if(value instanceof String) {
tileValue.setStringValue((String) value);
}
return tileValue.build();
}
/**
* Convert {@link VectorTile.Tile.Value} to String or boxed primitive object.
*
* @param value target for conversion
* @return String or boxed primitive
*/
public static Object toObject(VectorTile.Tile.Value value) {
Object result = null;
if(value.hasDoubleValue()) {
result = value.getDoubleValue();
} else if(value.hasFloatValue()) {
result = value.getFloatValue();
} else if(value.hasIntValue()) {
result = value.getIntValue();
} else if(value.hasBoolValue()) {
result = value.getBoolValue();
} else if(value.hasStringValue()) {
result = value.getStringValue();
} else if(value.hasSintValue()) {
result = value.getSintValue();
} else if(value.hasUintValue()) {
result = value.getUintValue();
}
return result;
}
/**
* Check if {@code value} is valid for encoding as a MVT layer property value.
*
* @param value target to check
* @return true is the object is a type that is supported by MVT
*/
public static boolean isValidPropValue(Object value) {
boolean isValid = false;
if(value instanceof Boolean || value instanceof Integer || value instanceof Long
|| value instanceof Float || value instanceof Double || value instanceof String) {
isValid = true;
}
return isValid;
}
}

View file

@ -0,0 +1,27 @@
package com.wdtinc.mapbox_vector_tile.encoding;
/**
* See: <a href="https://developers.google.com/protocol-buffers/docs/encoding#types">Google Protocol Buffers Docs</a>
*/
public final class ZigZag {
/**
* See: <a href="https://developers.google.com/protocol-buffers/docs/encoding#types">Google Protocol Buffers Docs</a>
*
* @param n integer to encode
* @return zig-zag encoded integer
*/
public static int encode(int n) {
return (n << 1) ^ (n >> 31);
}
/**
* See: <a href="https://developers.google.com/protocol-buffers/docs/encoding#types">Google Protocol Buffers Docs</a>
*
* @param n zig-zag encoded integer to decode
* @return decoded integer
*/
public static int decode(int n) {
return (n >> 1) ^ (-(n & 1));
}
}

View file

@ -0,0 +1,151 @@
package com.wdtinc.mapbox_vector_tile.util;
import com.vividsolutions.jts.geom.*;
import com.wdtinc.mapbox_vector_tile.adapt.jts.JtsAdapter;
import net.osmand.binary.VectorTile;
import java.util.*;
public final class JtsGeomStats {
public static final class FeatureStats {
public int totalPts;
public int repeatedPts;
@Override
public String toString() {
return "FeatureStats{" +
"totalPts=" + totalPts +
", repeatedPts=" + repeatedPts +
'}';
}
}
public Map<VectorTile.Tile.GeomType, Integer> featureCounts;
public List<FeatureStats> featureStats;
private JtsGeomStats() {
final VectorTile.Tile.GeomType[] geomTypes = VectorTile.Tile.GeomType.values();
featureCounts = new HashMap<>(geomTypes.length);
for(VectorTile.Tile.GeomType nextGeomType : geomTypes) {
featureCounts.put(nextGeomType, 0);
}
this.featureStats = new ArrayList<>();
}
@Override
public String toString() {
return "JtsGeomStats{" +
"featureCounts=" + featureCounts +
", featureStats=" + featureStats +
'}';
}
public static JtsGeomStats getStats(List<Geometry> flatGeomList) {
final JtsGeomStats stats = new JtsGeomStats();
for(Geometry nextGeom : flatGeomList) {
final VectorTile.Tile.GeomType geomType = JtsAdapter.toGeomType(nextGeom);
// Count features by type
Integer oldValue = stats.featureCounts.get(geomType);
if(oldValue!=null){
oldValue+=1;
stats.featureCounts.put(geomType, oldValue);
}
// Get stats per feature
stats.featureStats.add(getStats(nextGeom, geomType));
}
return stats;
}
private static FeatureStats getStats(Geometry geom, VectorTile.Tile.GeomType type) {
FeatureStats featureStats;
switch (type) {
case POINT:
featureStats = pointStats(geom);
break;
case LINESTRING:
featureStats = lineStats(geom);
break;
case POLYGON:
featureStats = polyStats(geom);
break;
default:
featureStats = new FeatureStats();
}
return featureStats;
}
private static FeatureStats pointStats(Geometry geom) {
final FeatureStats featureStats = new FeatureStats();
final HashSet<Point> pointSet = new HashSet<>(geom.getNumPoints());
featureStats.totalPts = geom.getNumPoints();
for(int i = 0; i < geom.getNumGeometries(); ++i) {
final Point p = (Point) geom.getGeometryN(i);
featureStats.repeatedPts += pointSet.add(p) ? 0 : 1;
}
return featureStats;
}
private static FeatureStats lineStats(Geometry geom) {
final FeatureStats featureStats = new FeatureStats();
for(int i = 0; i < geom.getNumGeometries(); ++i) {
final LineString lineString = (LineString) geom.getGeometryN(i);
featureStats.totalPts += lineString.getNumPoints();
featureStats.repeatedPts += checkRepeatedPoints2d(lineString);
}
return featureStats;
}
private static FeatureStats polyStats(Geometry geom) {
final FeatureStats featureStats = new FeatureStats();
for(int i = 0; i < geom.getNumGeometries(); ++i) {
final Polygon nextPoly = (Polygon) geom.getGeometryN(i);
// Stats: exterior ring
final LineString exteriorRing = nextPoly.getExteriorRing();
featureStats.totalPts += exteriorRing.getNumPoints();
featureStats.repeatedPts += checkRepeatedPoints2d(exteriorRing);
// Stats: interior rings
for(int ringIndex = 0; ringIndex < nextPoly.getNumInteriorRing(); ++ringIndex) {
final LineString nextInteriorRing = nextPoly.getInteriorRingN(ringIndex);
featureStats.totalPts += nextInteriorRing.getNumPoints();
featureStats.repeatedPts += checkRepeatedPoints2d(nextInteriorRing);
}
}
return featureStats;
}
private static int checkRepeatedPoints2d(LineString lineString) {
int repeatedPoints = 0;
final CoordinateSequence coordSeq = lineString.getCoordinateSequence();
Coordinate nextCoord = null, prevCoord;
for(int i = 0; i < coordSeq.size(); ++i) {
prevCoord = nextCoord;
nextCoord = coordSeq.getCoordinate(i);
if(nextCoord.equals(prevCoord)) {
++repeatedPoints;
}
}
return repeatedPoints;
}
}

View file

@ -0,0 +1,126 @@
package com.wdtinc.mapbox_vector_tile.util;
/**
* Mutable Vector with double-valued x and y dimensions.
*/
public final class Vec2d {
public double x, y;
/**
* Construct instance with x = 0, y = 0.
*/
public Vec2d() {
set(0d, 0d);
}
/**
* Construct instance with (x, y) values set to passed parameters
*
* @param x value in x
* @param y value in y
*/
public Vec2d(double x, double y) {
set(x, y);
}
/**
* Constructs instance with values from the input vector 'v'.
*
* @param v The vector
*/
public Vec2d(Vec2d v) {
set(v);
}
/**
* Set the x and y values of this vector. Return this vector for chaining.
*
* @param x value in x
* @param y value in y
* @return this vector for chaining
*/
public Vec2d set(double x, double y) {
this.x = x;
this.y = y;
return this;
}
/**
* Set the x and y values of this vector to match input vector 'v'. Return this vector for chaining.
*
* @param v contains values to copy
* @return this vector for chaining
*/
public Vec2d set(Vec2d v) {
return set(v.x, v.y);
}
/**
* Adds the given values to this vector. Return this vector for chaining.
*
* @param x value in x
* @param y value in y
* @return this vector for chaining
*/
public Vec2d add(double x, double y) {
this.x += x;
this.y += y;
return this;
}
/**
* Adds the given vector 'v' to this vector. Return this vector for chaining.
*
* @param v vector to add
* @return this vector for chaining
*/
public Vec2d add(Vec2d v) {
return add(v.x, v.y);
}
/**
* Subtracts the given values from this vector. Return this vector for chaining.
*
* @param x value in x to subtract
* @param y value in y to subtract
* @return this vector for chaining
*/
public Vec2d sub(double x, double y) {
this.x -= x;
this.y -= y;
return this;
}
/**
* Subtracts the given vector 'v' from this vector. Return this vector for chaining.
*
* @param v vector to subtract
* @return this vector for chaining
*/
public Vec2d sub(Vec2d v) {
return sub(v.x, v.y);
}
/**
* Scales this vector's values by a constant.
*
* @param scalar constant to scale this vector's values by
* @return this vector for chaining
*/
public Vec2d scale(double scalar) {
this.x *= scalar;
this.y *= scalar;
return this;
}
@Override
public String toString () {
return "(" + x + "," + y + ")";
}
}

View file

@ -0,0 +1,22 @@
package net.osmand.binary;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.wdtinc.mapbox_vector_tile.adapt.jts.MvtReader;
import com.wdtinc.mapbox_vector_tile.adapt.jts.TagKeyValueMapConverter;
import net.osmand.data.GeometryTile;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.List;
public class BinaryVectorTileReader {
public static GeometryTile readTile(File file) throws IOException {
GeometryFactory geomFactory = new GeometryFactory();
return new GeometryTile(
MvtReader.loadMvt(new FileInputStream(file), geomFactory, new TagKeyValueMapConverter()));
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,18 @@
package net.osmand.data;
import com.vividsolutions.jts.geom.Geometry;
import java.util.List;
public class GeometryTile {
private List<Geometry> data;
public GeometryTile(List<Geometry> data) {
this.data = data;
}
public List<Geometry> getData() {
return data;
}
}

View file

@ -40,11 +40,14 @@ public class TileSourceManager {
new TileSourceTemplate("OsmAnd (online tiles)", "http://tile.osmand.net/hd/{0}/{1}/{2}.png", ".png", 19, 1, 512, 8, 18000); //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$
private static final TileSourceTemplate CYCLE_MAP_SOURCE =
new TileSourceTemplate("CycleMap", "http://b.tile.opencyclemap.org/cycle/{0}/{1}/{2}.png", ".png", 16, 1, 256, 32, 18000); //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$
private static final TileSourceTemplate MAPILLARY_SOURCE =
new TileSourceTemplate("Mapillary (raster tiles)", "https://d6a1v2w10ny40.cloudfront.net/v0.1/{0}/{1}/{2}.png", ".png", 17, 0, 256, 16, 32000);
private static final TileSourceTemplate MAPILLARY_RASTER_SOURCE =
new TileSourceTemplate("Mapillary (raster tiles)", "https://d6a1v2w10ny40.cloudfront.net/v0.1/{0}/{1}/{2}.png", ".png", 14, 0, 256, 16, 32000);
private static final TileSourceTemplate MAPILLARY_VECTOR_SOURCE =
new TileSourceTemplate("Mapillary (vector tiles)", "https://d25uarhxywzl1j.cloudfront.net/v0.1/{0}/{1}/{2}.mvt", ".mvt", 21, 15, 256, 16, 3200);
static {
MAPILLARY_SOURCE.setExpirationTimeMinutes(60 * 24);
MAPILLARY_RASTER_SOURCE.setExpirationTimeMinutes(60 * 24);
MAPILLARY_VECTOR_SOURCE.setExpirationTimeMinutes(60 * 24);
}
public static class TileSourceTemplate implements ITileSource, Cloneable {
@ -424,7 +427,8 @@ public class TileSourceManager {
java.util.List<TileSourceTemplate> list = new ArrayList<TileSourceTemplate>();
list.add(getMapnikSource());
list.add(getCycleMapSource());
list.add(getMapillarySource());
list.add(getMapillaryRasterSource());
list.add(getMapillaryVectorSource());
return list;
}
@ -437,8 +441,12 @@ public class TileSourceManager {
return CYCLE_MAP_SOURCE;
}
public static TileSourceTemplate getMapillarySource() {
return MAPILLARY_SOURCE;
public static TileSourceTemplate getMapillaryRasterSource() {
return MAPILLARY_RASTER_SOURCE;
}
public static TileSourceTemplate getMapillaryVectorSource() {
return MAPILLARY_VECTOR_SOURCE;
}
public static List<TileSourceTemplate> downloadTileSourceTemplates(String versionAsUrl) {

Binary file not shown.

View file

@ -1,26 +1,22 @@
package net.osmand.core.android;
import android.graphics.Bitmap;
import java.io.IOException;
import net.osmand.IndexConstants;
import net.osmand.core.jni.AlphaChannelPresence;
import net.osmand.core.jni.IMapDataProvider;
import net.osmand.core.jni.IMapTiledDataProvider;
import net.osmand.core.jni.ImageMapLayerProvider;
import net.osmand.core.jni.MapStubStyle;
import net.osmand.core.jni.SWIGTYPE_p_QByteArray;
import net.osmand.core.jni.SwigUtilities;
import net.osmand.core.jni.TileId;
import net.osmand.core.jni.ZoomLevel;
import net.osmand.core.jni.interface_ImageMapLayerProvider;
import net.osmand.core.jni.IMapTiledDataProvider;
import net.osmand.core.jni.ImageMapLayerProvider;
import net.osmand.map.ITileSource;
import net.osmand.map.MapTileDownloader;
import net.osmand.plus.OsmandApplication;
import net.osmand.plus.resources.AsyncLoadingThread;
import net.osmand.plus.resources.ResourceManager;
import java.io.IOException;
public class TileSourceProxyProvider extends interface_ImageMapLayerProvider {
private final OsmandApplication app;
@ -62,7 +58,7 @@ public class TileSourceProxyProvider extends interface_ImageMapLayerProvider {
final TileReadyCallback tileReadyCallback = new TileReadyCallback(tileSource,
request.getTileId().getX(), request.getTileId().getY(), request.getZoom().swigValue());
rm.getMapTileDownloader().addDownloaderCallback(tileReadyCallback);
while (rm.getTileImageForMapAsync(tileFilename, tileSource, request.getTileId().getX(), request.getTileId().getY(),
while (rm.getBitmapTilesCache().getTileForMapAsync(tileFilename, tileSource, request.getTileId().getX(), request.getTileId().getY(),
request.getZoom().swigValue(), true) == null) {
synchronized (tileReadyCallback.getSync()) {
if (tileReadyCallback.isReady()) {

View file

@ -166,7 +166,7 @@ public class DownloadTilesDialog {
if (rm.tileExistOnFileSystem(tileId, map, x, y, z)) {
progressDlg.setProgress(progressDlg.getProgress() + 1);
} else {
rm.getTileImageForMapSync(tileId, map, x, y, z, true);
rm.hasTileForMapSync(tileId, map, x, y, z, true);
requests++;
}
if (!cancel) {

View file

@ -540,7 +540,7 @@ public class MapActivityActions implements DialogProvider {
for (int i = 0; i < width; i++) {
for (int j = 0; j < height; j++) {
((OsmandApplication) mapActivity.getApplication()).getResourceManager().
clearTileImageForMap(null, mapSource, i + left, j + top, zoom);
clearTileForMap(null, mapSource, i + left, j + top, zoom);
}
}

View file

@ -0,0 +1,15 @@
package net.osmand.plus.mapillary;
public class MapillaryImage {
/*
* ca number Camera heading. -1 if not found.
* captured_at number When the image was captured, expressed as UTC epoch time in milliseconds. Must be non-negative integer; 0 if not found.
* key Key Image key.
* pano number Whether the image is panorama ( 1 ), or not ( 0 ).
* skey Key Sequence key.
* userkey Key User key. Empty if not found.
*/
private double ca = Double.NaN;
}

View file

@ -119,15 +119,8 @@ public class MapillaryImageDialog extends ContextMenuCardDialog {
private void setImageLocation(LatLon latLon, double ca, boolean animated) {
OsmandMapTileView mapView = getMapActivity().getMapView();
MapillaryLayer layer = mapView.getLayerByClass(MapillaryLayer.class);
if (layer != null) {
layer.setSelectedImageLocation(latLon);
if (!Double.isNaN(ca)) {
layer.setSelectedImageCameraAngle((float) ca);
} else {
layer.setSelectedImageCameraAngle(null);
}
}
updateLayer(mapView.getLayerByClass(MapillaryRasterLayer.class), latLon, ca);
updateLayer(mapView.getLayerByClass(MapillaryVectorLayer.class), latLon, ca);
if (latLon != null) {
if (animated) {
mapView.getAnimatedDraggingThread().startMoving(
@ -140,6 +133,17 @@ public class MapillaryImageDialog extends ContextMenuCardDialog {
}
}
private void updateLayer(MapillaryLayer layer, LatLon latLon, double ca) {
if (layer != null) {
layer.setSelectedImageLocation(latLon);
if (!Double.isNaN(ca)) {
layer.setSelectedImageCameraAngle((float) ca);
} else {
layer.setSelectedImageCameraAngle(null);
}
}
}
public View getContentView() {
if (getMapActivity().getMyApplication().getSettings().WEBGL_SUPPORTED.get()) {
return getWebView();

View file

@ -1,69 +1,10 @@
package net.osmand.plus.mapillary;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import net.osmand.data.LatLon;
import net.osmand.data.RotatedTileBox;
import net.osmand.plus.R;
import net.osmand.plus.views.MapTileLayer;
import net.osmand.plus.views.OsmandMapLayer;
import net.osmand.plus.views.OsmandMapTileView;
public class MapillaryLayer extends MapTileLayer {
interface MapillaryLayer {
private LatLon selectedImageLocation;
private Float selectedImageCameraAngle;
private Bitmap selectedImage;
private Bitmap headingImage;
private Paint paintIcon;
void setSelectedImageLocation(LatLon selectedImageLocation);
public MapillaryLayer() {
super(false);
}
@Override
public void initLayer(OsmandMapTileView view) {
super.initLayer(view);
paintIcon = new Paint();
selectedImage = BitmapFactory.decodeResource(view.getResources(), R.drawable.map_default_location);
headingImage = BitmapFactory.decodeResource(view.getResources(), R.drawable.map_pedestrian_location_view_angle);
}
public LatLon getSelectedImageLocation() {
return selectedImageLocation;
}
public void setSelectedImageLocation(LatLon selectedImageLocation) {
this.selectedImageLocation = selectedImageLocation;
}
public Float getSelectedImageCameraAngle() {
return selectedImageCameraAngle;
}
public void setSelectedImageCameraAngle(Float selectedImageCameraAngle) {
this.selectedImageCameraAngle = selectedImageCameraAngle;
}
@Override
public void onPrepareBufferImage(Canvas canvas, RotatedTileBox tileBox, DrawSettings drawSettings) {
super.onPrepareBufferImage(canvas, tileBox, drawSettings);
if (selectedImageLocation != null) {
float x = tileBox.getPixXFromLatLon(selectedImageLocation.getLatitude(), selectedImageLocation.getLongitude());
float y = tileBox.getPixYFromLatLon(selectedImageLocation.getLatitude(), selectedImageLocation.getLongitude());
if (selectedImageCameraAngle != null) {
canvas.save();
canvas.rotate(selectedImageCameraAngle - 180, x, y);
canvas.drawBitmap(headingImage, x - headingImage.getWidth() / 2,
y - headingImage.getHeight() / 2, paintIcon);
canvas.restore();
}
canvas.drawBitmap(selectedImage, x - selectedImage.getWidth() / 2, y - selectedImage.getHeight() / 2, paintIcon);
}
}
void setSelectedImageCameraAngle(Float selectedImageCameraAngle);
}

View file

@ -41,7 +41,8 @@ public class MapillaryPlugin extends OsmandPlugin {
private OsmandSettings settings;
private OsmandApplication app;
private MapillaryLayer rasterLayer;
private MapillaryRasterLayer rasterLayer;
private MapillaryVectorLayer vectorLayer;
private TextInfoWidget mapillaryControl;
private MapWidgetRegInfo mapillaryWidgetRegInfo;
@ -82,7 +83,8 @@ public class MapillaryPlugin extends OsmandPlugin {
}
private void createLayers() {
rasterLayer = new MapillaryLayer();
rasterLayer = new MapillaryRasterLayer();
vectorLayer = new MapillaryVectorLayer();
}
@Override
@ -91,23 +93,28 @@ public class MapillaryPlugin extends OsmandPlugin {
}
private void updateMapLayers(OsmandMapTileView mapView, final MapActivityLayers layers) {
if (rasterLayer == null) {
if (rasterLayer == null || vectorLayer == null) {
createLayers();
}
if (isActive()) {
updateLayer(mapView, rasterLayer, 0.6f);
ITileSource rasterSource = null;
ITileSource vectorSource = null;
if (settings.SHOW_MAPILLARY.get()) {
rasterSource = settings.getTileSourceByName(TileSourceManager.getMapillaryRasterSource().getName(), false);
vectorSource = settings.getTileSourceByName(TileSourceManager.getMapillaryVectorSource().getName(), false);
}
updateLayer(mapView, rasterSource, rasterLayer, 0.6f);
updateLayer(mapView, vectorSource, vectorLayer, 0.61f);
} else {
mapView.removeLayer(rasterLayer);
rasterLayer.setMap(null);
mapView.removeLayer(vectorLayer);
vectorLayer.setMap(null);
}
layers.updateMapSource(mapView, null);
}
private void updateLayer(OsmandMapTileView mapView, MapTileLayer layer, float layerOrder) {
ITileSource mapillarySource = null;
if (settings.SHOW_MAPILLARY.get()) {
mapillarySource = settings.getTileSourceByName(TileSourceManager.getMapillarySource().getName(), false);
}
private void updateLayer(OsmandMapTileView mapView, ITileSource mapillarySource, MapTileLayer layer, float layerOrder) {
if (!Algorithms.objectEquals(mapillarySource, layer.getMap()) || !mapView.isLayerVisible(layer)) {
if (mapView.getMapRenderer() == null && !mapView.isLayerVisible(layer)) {
mapView.addLayer(layer, layerOrder);

View file

@ -0,0 +1,74 @@
package net.osmand.plus.mapillary;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import net.osmand.data.LatLon;
import net.osmand.data.RotatedTileBox;
import net.osmand.map.ITileSource;
import net.osmand.plus.R;
import net.osmand.plus.views.MapTileLayer;
import net.osmand.plus.views.OsmandMapTileView;
class MapillaryRasterLayer extends MapTileLayer implements MapillaryLayer {
private LatLon selectedImageLocation;
private Float selectedImageCameraAngle;
private Bitmap selectedImage;
private Bitmap headingImage;
private Paint paintIcon;
MapillaryRasterLayer() {
super(false);
}
@Override
public void initLayer(OsmandMapTileView view) {
super.initLayer(view);
paintIcon = new Paint();
selectedImage = BitmapFactory.decodeResource(view.getResources(), R.drawable.map_default_location);
headingImage = BitmapFactory.decodeResource(view.getResources(), R.drawable.map_pedestrian_location_view_angle);
}
@Override
public void setSelectedImageLocation(LatLon selectedImageLocation) {
this.selectedImageLocation = selectedImageLocation;
}
@Override
public void setSelectedImageCameraAngle(Float selectedImageCameraAngle) {
this.selectedImageCameraAngle = selectedImageCameraAngle;
}
@Override
public void onPrepareBufferImage(Canvas canvas, RotatedTileBox tileBox, DrawSettings drawSettings) {
super.onPrepareBufferImage(canvas, tileBox, drawSettings);
if (selectedImageLocation != null) {
float x = tileBox.getPixXFromLatLon(selectedImageLocation.getLatitude(), selectedImageLocation.getLongitude());
float y = tileBox.getPixYFromLatLon(selectedImageLocation.getLatitude(), selectedImageLocation.getLongitude());
if (selectedImageCameraAngle != null) {
canvas.save();
canvas.rotate(selectedImageCameraAngle - 180, x, y);
canvas.drawBitmap(headingImage, x - headingImage.getWidth() / 2,
y - headingImage.getHeight() / 2, paintIcon);
canvas.restore();
}
canvas.drawBitmap(selectedImage, x - selectedImage.getWidth() / 2, y - selectedImage.getHeight() / 2, paintIcon);
}
}
@Override
public void drawTileMap(Canvas canvas, RotatedTileBox tileBox) {
ITileSource map = this.map;
if (map == null) {
return;
}
int maxZoom = map.getMaximumZoomSupported();
if (tileBox.getZoom() > maxZoom) {
return;
}
super.drawTileMap(canvas, tileBox);
}
}

View file

@ -0,0 +1,160 @@
package net.osmand.plus.mapillary;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.Point;
import net.osmand.data.GeometryTile;
import net.osmand.data.LatLon;
import net.osmand.data.QuadRect;
import net.osmand.data.RotatedTileBox;
import net.osmand.map.ITileSource;
import net.osmand.plus.OsmandPlugin;
import net.osmand.plus.R;
import net.osmand.plus.rastermaps.OsmandRasterMapsPlugin;
import net.osmand.plus.resources.ResourceManager;
import net.osmand.plus.views.MapTileLayer;
import net.osmand.plus.views.OsmandMapTileView;
import net.osmand.util.MapUtils;
import java.util.LinkedHashMap;
import java.util.Map;
class MapillaryVectorLayer extends MapTileLayer implements MapillaryLayer {
private static final int TILE_ZOOM = 14;
private LatLon selectedImageLocation;
private Float selectedImageCameraAngle;
private Bitmap selectedImage;
private Bitmap headingImage;
private Paint paintIcon;
private Bitmap point;
MapillaryVectorLayer() {
super(false);
}
@Override
public void initLayer(OsmandMapTileView view) {
super.initLayer(view);
paintIcon = new Paint();
selectedImage = BitmapFactory.decodeResource(view.getResources(), R.drawable.map_default_location);
headingImage = BitmapFactory.decodeResource(view.getResources(), R.drawable.map_pedestrian_location_view_angle);
point = BitmapFactory.decodeResource(view.getResources(), R.drawable.map_note_small);
}
@Override
public void setSelectedImageLocation(LatLon selectedImageLocation) {
this.selectedImageLocation = selectedImageLocation;
}
@Override
public void setSelectedImageCameraAngle(Float selectedImageCameraAngle) {
this.selectedImageCameraAngle = selectedImageCameraAngle;
}
@Override
public void onPrepareBufferImage(Canvas canvas, RotatedTileBox tileBox, DrawSettings drawSettings) {
super.onPrepareBufferImage(canvas, tileBox, drawSettings);
if (selectedImageLocation != null) {
float x = tileBox.getPixXFromLatLon(selectedImageLocation.getLatitude(), selectedImageLocation.getLongitude());
float y = tileBox.getPixYFromLatLon(selectedImageLocation.getLatitude(), selectedImageLocation.getLongitude());
if (selectedImageCameraAngle != null) {
canvas.save();
canvas.rotate(selectedImageCameraAngle - 180, x, y);
canvas.drawBitmap(headingImage, x - headingImage.getWidth() / 2,
y - headingImage.getHeight() / 2, paintIcon);
canvas.restore();
}
canvas.drawBitmap(selectedImage, x - selectedImage.getWidth() / 2, y - selectedImage.getHeight() / 2, paintIcon);
}
}
@Override
public void drawTileMap(Canvas canvas, RotatedTileBox tileBox) {
ITileSource map = this.map;
if (map == null) {
return;
}
int nzoom = tileBox.getZoom();
if (nzoom < map.getMinimumZoomSupported()) {
return;
}
ResourceManager mgr = resourceManager;
final QuadRect tilesRect = tileBox.getTileBounds();
// recalculate for ellipsoid coordinates
float ellipticTileCorrection = 0;
if (map.isEllipticYTile()) {
ellipticTileCorrection = (float) (MapUtils.getTileEllipsoidNumberY(nzoom, tileBox.getLatitude()) - tileBox.getCenterTileY());
}
int left = (int) Math.floor(tilesRect.left);
int top = (int) Math.floor(tilesRect.top + ellipticTileCorrection);
int width = (int) Math.ceil(tilesRect.right - left);
int height = (int) Math.ceil(tilesRect.bottom + ellipticTileCorrection - top);
boolean useInternet = (OsmandPlugin.getEnabledPlugin(OsmandRasterMapsPlugin.class) != null || OsmandPlugin.getEnabledPlugin(MapillaryPlugin.class) != null) &&
settings.USE_INTERNET_TO_DOWNLOAD_TILES.get() && settings.isInternetConnectionAvailable() && map.couldBeDownloadedFromInternet();
Map<String, GeometryTile> tiles = new LinkedHashMap<>();
for (int i = 0; i < width; i++) {
for (int j = 0; j < height; j++) {
int leftPlusI = left + i;
int topPlusJ = top + j;
int x1 = tileBox.getPixXFromTileXNoRot(leftPlusI);
int x2 = tileBox.getPixXFromTileXNoRot(leftPlusI + 1);
int y1 = tileBox.getPixYFromTileYNoRot(topPlusJ - ellipticTileCorrection);
int y2 = tileBox.getPixYFromTileYNoRot(topPlusJ + 1 - ellipticTileCorrection);
bitmapToDraw.set(x1, y1, x2, y2);
int tileX = leftPlusI;
int tileY = topPlusJ;
//String tileId = mgr.calculateTileId(map, tileX, tileY, nzoom);
int dzoom = nzoom - TILE_ZOOM;
int div = (int) Math.pow(2.0, dzoom);
tileX /= div;
tileY /= div;
String tileId = mgr.calculateTileId(map, tileX, tileY, TILE_ZOOM);
GeometryTile tile = tiles.get(tileId);
if (tile == null) {
// asking tile image async
boolean imgExist = mgr.tileExistOnFileSystem(tileId, map, tileX, tileY, TILE_ZOOM);
if (imgExist || useInternet) {
tile = mgr.getGeometryTilesCache().getTileForMapAsync(tileId, map, tileX, tileY, TILE_ZOOM, useInternet);
}
if (tile != null) {
tiles.put(tileId, tile);
if (tile.getData() != null) {
drawPoints(canvas, tileBox, tileX, tileY, tile);
}
}
}
}
}
}
private void drawPoints(Canvas canvas, RotatedTileBox tileBox, int tileX, int tileY, GeometryTile tile) {
for (Geometry g : tile.getData()) {
if (g instanceof Point && g.getCoordinate() != null) {
int x = (int) g.getCoordinate().x;
int y = (int) g.getCoordinate().y;
double lat = MapUtils.getLatitudeFromTile(TILE_ZOOM, tileY + y / 4096f);
double lon = MapUtils.getLongitudeFromTile(TILE_ZOOM, tileX + x / 4096f);
if (tileBox.containsLatLon(lat, lon)) {
float px = tileBox.getPixXFromLatLon(lat, lon);
float py = tileBox.getPixYFromLatLon(lat, lon);
canvas.drawBitmap(point, px - point.getWidth() / 2, py - point.getHeight() / 2, paintIcon);
}
}
}
}
}

View file

@ -1,12 +1,6 @@
package net.osmand.plus.resources;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Stack;
import net.osmand.PlatformUtil;
import net.osmand.ResultMatcher;
import net.osmand.data.RotatedTileBox;
@ -17,6 +11,12 @@ import net.osmand.util.Algorithms;
import org.apache.commons.logging.Log;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Stack;
/**
* Thread to load map objects (POI, transport stops )async
*/
@ -44,7 +44,7 @@ public class AsyncLoadingThread extends Thread {
Object req = requests.pop();
if (req instanceof TileLoadDownloadRequest) {
TileLoadDownloadRequest r = (TileLoadDownloadRequest) req;
tileLoaded |= resourceManger.getRequestedImageTile(r) != null;
tileLoaded |= resourceManger.hasRequestedTile(r);
} else if (req instanceof MapLoadRequest) {
if (!mapLoaded) {
MapLoadRequest r = (MapLoadRequest) req;
@ -69,7 +69,7 @@ public class AsyncLoadingThread extends Thread {
}
}
public void requestToLoadImage(TileLoadDownloadRequest req) {
public void requestToLoadTile(TileLoadDownloadRequest req) {
requests.push(req);
}

View file

@ -0,0 +1,55 @@
package net.osmand.plus.resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import net.osmand.map.ITileSource;
import net.osmand.plus.SQLiteTileSource;
import net.osmand.plus.resources.AsyncLoadingThread.TileLoadDownloadRequest;
import java.io.File;
public class BitmapTilesCache extends TilesCache<Bitmap> {
public BitmapTilesCache(AsyncLoadingThread asyncLoadingThread) {
super(asyncLoadingThread);
// it is not good investigated but no more than 64 (satellite images)
// Only 8 MB (from 16 Mb whole mem) available for images : image 64K * 128 = 8 MB (8 bit), 64 - 16 bit, 32 - 32 bit
// at least 3*9?
maxCacheSize = 28;
}
@Override
public boolean isTileSourceSupported(ITileSource tileSource) {
return !".mvt".equals(tileSource.getTileFormat());
}
@Override
protected Bitmap getTileObject(TileLoadDownloadRequest req) {
Bitmap bmp = null;
if (req.tileSource instanceof SQLiteTileSource) {
try {
long[] tm = new long[1];
bmp = ((SQLiteTileSource) req.tileSource).getImage(req.xTile, req.yTile, req.zoom, tm);
if (tm[0] != 0) {
downloadIfExpired(req, tm[0]);
}
} catch (OutOfMemoryError e) {
log.error("Out of memory error", e); //$NON-NLS-1$
clearTiles();
}
} else {
File en = new File(req.dirWithTiles, req.tileId);
if (en.exists()) {
try {
bmp = BitmapFactory.decodeFile(en.getAbsolutePath());
downloadIfExpired(req, en.lastModified());
} catch (OutOfMemoryError e) {
log.error("Out of memory error", e); //$NON-NLS-1$
clearTiles();
}
}
}
return bmp;
}
}

View file

@ -0,0 +1,40 @@
package net.osmand.plus.resources;
import net.osmand.binary.BinaryVectorTileReader;
import net.osmand.data.GeometryTile;
import net.osmand.map.ITileSource;
import net.osmand.plus.resources.AsyncLoadingThread.TileLoadDownloadRequest;
import java.io.File;
import java.io.IOException;
public class GeometryTilesCache extends TilesCache<GeometryTile> {
public GeometryTilesCache(AsyncLoadingThread asyncLoadingThread) {
super(asyncLoadingThread);
maxCacheSize = 50;
}
@Override
public boolean isTileSourceSupported(ITileSource tileSource) {
return ".mvt".equals(tileSource.getTileFormat());
}
@Override
protected GeometryTile getTileObject(TileLoadDownloadRequest req) {
GeometryTile tile = null;
File en = new File(req.dirWithTiles, req.tileId);
if (en.exists()) {
try {
tile = BinaryVectorTileReader.readTile(en);
downloadIfExpired(req, en.lastModified());
} catch (IOException e) {
log.error("Cannot read tile", e);
} catch (OutOfMemoryError e) {
log.error("Out of memory error", e);
clearTiles();
}
}
return tile;
}
}

View file

@ -4,8 +4,6 @@ package net.osmand.plus.resources;
import android.content.Context;
import android.content.res.AssetManager;
import android.database.sqlite.SQLiteException;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.HandlerThread;
import android.text.format.DateFormat;
import android.util.DisplayMetrics;
@ -35,7 +33,6 @@ import net.osmand.plus.AppInitializer.InitEvents;
import net.osmand.plus.OsmandApplication;
import net.osmand.plus.OsmandPlugin;
import net.osmand.plus.R;
import net.osmand.plus.SQLiteTileSource;
import net.osmand.plus.Version;
import net.osmand.plus.render.MapRenderRepositories;
import net.osmand.plus.render.NativeOsmandLibrary;
@ -80,24 +77,17 @@ public class ResourceManager {
public static final String VECTOR_MAP = "#vector_map"; //$NON-NLS-1$
private static final String INDEXES_CACHE = "ind.cache";
private static final Log log = PlatformUtil.getLog(ResourceManager.class);
protected static ResourceManager manager = null;
// it is not good investigated but no more than 64 (satellite images)
// Only 8 MB (from 16 Mb whole mem) available for images : image 64K * 128 = 8 MB (8 bit), 64 - 16 bit, 32 - 32 bit
// at least 3*9?
protected int maxImgCacheSize = 28;
protected Map<String, Bitmap> cacheOfImages = new LinkedHashMap<String, Bitmap>();
protected Map<String, Boolean> imagesOnFS = new LinkedHashMap<String, Boolean>() ;
protected File dirWithTiles ;
private final OsmandApplication context;
private List<TilesCache> tilesCacheList = new ArrayList<>();
private BitmapTilesCache bitmapTilesCache;
private GeometryTilesCache geometryTilesCache;
private final OsmandApplication context;
private List<ResourceListener> resourceListeners = new ArrayList<>();
public interface ResourceListener {
@ -105,7 +95,6 @@ public class ResourceManager {
void onMapsIndexed();
}
// Indexes
public enum BinaryMapReaderResourceType {
POI,
@ -217,6 +206,12 @@ public class ResourceManager {
this.context = context;
this.renderer = new MapRenderRepositories(context);
bitmapTilesCache = new BitmapTilesCache(asyncLoadingThread);
geometryTilesCache = new GeometryTilesCache(asyncLoadingThread);
tilesCacheList.add(bitmapTilesCache);
tilesCacheList.add(geometryTilesCache);
asyncLoadingThread.start();
renderingBufferImageThread = new HandlerThread("RenderingBaseImage");
renderingBufferImageThread.start();
@ -224,14 +219,23 @@ public class ResourceManager {
tileDownloader = MapTileDownloader.getInstance(Version.getFullVersion(context));
dateFormat = DateFormat.getDateFormat(context);
resetStoreDirectory();
WindowManager mgr = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics dm = new DisplayMetrics();
mgr.getDefaultDisplay().getMetrics(dm);
// Only 8 MB (from 16 Mb whole mem) available for images : image 64K * 128 = 8 MB (8 bit), 64 - 16 bit, 32 - 32 bit
// at least 3*9?
float tiles = (dm.widthPixels / 256 + 2) * (dm.heightPixels / 256 + 2) * 3;
log.info("Tiles to load in memory : " + tiles);
maxImgCacheSize = (int) (tiles) ;
log.info("Bitmap tiles to load in memory : " + tiles);
bitmapTilesCache.setMaxCacheSize((int) (tiles));
}
public BitmapTilesCache getBitmapTilesCache() {
return bitmapTilesCache;
}
public GeometryTilesCache getGeometryTilesCache() {
return geometryTilesCache;
}
public MapTileDownloader getMapTileDownloader() {
@ -261,6 +265,9 @@ public class ResourceManager {
context.getAppPath(".nomedia").createNewFile(); //$NON-NLS-1$
} catch( Exception e ) {
}
for (TilesCache tilesCache : tilesCacheList) {
tilesCache.setDirWithTiles(dirWithTiles);
}
}
public java.text.DateFormat getDateFormat() {
@ -277,219 +284,57 @@ public class ResourceManager {
////////////////////////////////////////////// Working with tiles ////////////////////////////////////////////////
public Bitmap getTileImageForMapAsync(String file, ITileSource map, int x, int y, int zoom, boolean loadFromInternetIfNeeded) {
return getTileImageForMap(file, map, x, y, zoom, loadFromInternetIfNeeded, false, true);
private TilesCache getTilesCache(ITileSource map) {
for (TilesCache tc : tilesCacheList) {
if (tc.isTileSourceSupported(map)) {
return tc;
}
public synchronized Bitmap getTileImageFromCache(String file){
return cacheOfImages.get(file);
}
public synchronized void putTileInTheCache(String file, Bitmap bmp) {
cacheOfImages.put(file, bmp);
}
public Bitmap getTileImageForMapSync(String file, ITileSource map, int x, int y, int zoom, boolean loadFromInternetIfNeeded) {
return getTileImageForMap(file, map, x, y, zoom, loadFromInternetIfNeeded, true, true);
return null;
}
public synchronized void tileDownloaded(DownloadRequest request){
if (request instanceof TileLoadDownloadRequest) {
TileLoadDownloadRequest req = ((TileLoadDownloadRequest) request);
imagesOnFS.put(req.tileId, Boolean.TRUE);
/* if(req.fileToSave != null && req.tileSource instanceof SQLiteTileSource){
try {
((SQLiteTileSource) req.tileSource).insertImage(req.xTile, req.yTile, req.zoom, req.fileToSave);
} catch (IOException e) {
log.warn("File "+req.fileToSave.getName() + " couldn't be read", e); //$NON-NLS-1$//$NON-NLS-2$
}
req.fileToSave.delete();
String[] l = req.fileToSave.getParentFile().list();
if(l == null || l.length == 0){
req.fileToSave.getParentFile().delete();
l = req.fileToSave.getParentFile().getParentFile().list();
if(l == null || l.length == 0){
req.fileToSave.getParentFile().getParentFile().delete();
TilesCache cache = getTilesCache(req.tileSource);
if (cache != null) {
cache.tilesOnFS.put(req.tileId, Boolean.TRUE);
}
}
}*/
}
}
public synchronized boolean tileExistOnFileSystem(String file, ITileSource map, int x, int y, int zoom) {
if(!imagesOnFS.containsKey(file)){
boolean ex = false;
if(map instanceof SQLiteTileSource){
if(((SQLiteTileSource) map).isLocked()){
return false;
}
ex = ((SQLiteTileSource) map).exists(x, y, zoom);
} else {
if(file == null){
file = calculateTileId(map, x, y, zoom);
}
ex = new File(dirWithTiles, file).exists();
}
if (ex) {
imagesOnFS.put(file, Boolean.TRUE);
} else {
imagesOnFS.put(file, null);
}
}
return imagesOnFS.get(file) != null || cacheOfImages.get(file) != null;
TilesCache cache = getTilesCache(map);
return cache != null && !cache.tilesOnFS.containsKey(file)
&& cache.tileExistOnFileSystem(file, map, x, y, zoom);
}
public void clearTileImageForMap(String file, ITileSource map, int x, int y, int zoom){
getTileImageForMap(file, map, x, y, zoom, true, false, true, true);
public void clearTileForMap(String file, ITileSource map, int x, int y, int zoom){
TilesCache cache = getTilesCache(map);
if (cache != null) {
cache.getTileForMap(file, map, x, y, zoom, true, false, true, true);
}
}
/**
* @param file - null could be passed if you do not call very often with that param
*/
protected Bitmap getTileImageForMap(String file, ITileSource map, int x, int y, int zoom,
boolean loadFromInternetIfNeeded, boolean sync, boolean loadFromFs) {
return getTileImageForMap(file, map, x, y, zoom, loadFromInternetIfNeeded, sync, loadFromFs, false);
}
// introduce cache in order save memory
protected StringBuilder builder = new StringBuilder(40);
protected char[] tileId = new char[120];
private GeoidAltitudeCorrection geoidAltitudeCorrection;
private boolean searchAmenitiesInProgress;
public synchronized String calculateTileId(ITileSource map, int x, int y, int zoom) {
builder.setLength(0);
if (map == null) {
builder.append(IndexConstants.TEMP_SOURCE_TO_LOAD);
} else {
builder.append(map.getName());
TilesCache cache = getTilesCache(map);
if (cache != null) {
return cache.calculateTileId(map, x, y, zoom);
}
if (map instanceof SQLiteTileSource) {
builder.append('@');
} else {
builder.append('/');
}
builder.append(zoom).append('/').append(x).append('/').append(y).
append(map == null ? ".jpg" : map.getTileFormat()).append(".tile"); //$NON-NLS-1$ //$NON-NLS-2$
return builder.toString();
}
protected synchronized Bitmap getTileImageForMap(String tileId, ITileSource map, int x, int y, int zoom,
boolean loadFromInternetIfNeeded, boolean sync, boolean loadFromFs, boolean deleteBefore) {
if (tileId == null) {
tileId = calculateTileId(map, x, y, zoom);
if(tileId == null){
return null;
}
protected boolean hasRequestedTile(TileLoadDownloadRequest req) {
TilesCache cache = getTilesCache(req.tileSource);
return cache != null && cache.getRequestedTile(req) != null;
}
if(deleteBefore){
cacheOfImages.remove(tileId);
if (map instanceof SQLiteTileSource) {
((SQLiteTileSource) map).deleteImage(x, y, zoom);
} else {
File f = new File(dirWithTiles, tileId);
if (f.exists()) {
f.delete();
}
}
imagesOnFS.put(tileId, null);
}
if (loadFromFs && cacheOfImages.get(tileId) == null && map != null) {
boolean locked = map instanceof SQLiteTileSource && ((SQLiteTileSource) map).isLocked();
if(!loadFromInternetIfNeeded && !locked && !tileExistOnFileSystem(tileId, map, x, y, zoom)){
return null;
}
String url = loadFromInternetIfNeeded ? map.getUrlToLoad(x, y, zoom) : null;
File toSave = null;
if (url != null) {
if (map instanceof SQLiteTileSource) {
toSave = new File(dirWithTiles, calculateTileId(((SQLiteTileSource) map).getBase(), x, y, zoom));
} else {
toSave = new File(dirWithTiles, tileId);
}
}
TileLoadDownloadRequest req = new TileLoadDownloadRequest(dirWithTiles, url, toSave,
tileId, map, x, y, zoom, map.getReferer());
if(sync){
return getRequestedImageTile(req);
} else {
asyncLoadingThread.requestToLoadImage(req);
}
}
return cacheOfImages.get(tileId);
}
protected Bitmap getRequestedImageTile(TileLoadDownloadRequest req){
if(req.tileId == null || req.dirWithTiles == null){
return null;
}
Bitmap cacheBmp = cacheOfImages.get(req.tileId);
if (cacheBmp != null) {
return cacheBmp;
}
if (cacheOfImages.size() > maxImgCacheSize) {
clearTiles();
}
if (req.dirWithTiles.canRead() && !asyncLoadingThread.isFileCurrentlyDownloaded(req.fileToSave)
&& !asyncLoadingThread.isFilePendingToDownload(req.fileToSave)) {
long time = System.currentTimeMillis();
if (log.isDebugEnabled()) {
log.debug("Start loaded file : " + req.tileId + " " + Thread.currentThread().getName()); //$NON-NLS-1$ //$NON-NLS-2$
}
Bitmap bmp = null;
if (req.tileSource instanceof SQLiteTileSource) {
try {
long[] tm = new long[1];
bmp = ((SQLiteTileSource) req.tileSource).getImage(req.xTile, req.yTile, req.zoom, tm);
if (tm[0] != 0) {
int ts = req.tileSource.getExpirationTimeMillis();
if (ts != -1 && req.url != null && time - tm[0] > ts) {
asyncLoadingThread.requestToDownload(req);
}
}
} catch (OutOfMemoryError e) {
log.error("Out of memory error", e); //$NON-NLS-1$
clearTiles();
}
} else {
File en = new File(req.dirWithTiles, req.tileId);
if (en.exists()) {
try {
bmp = BitmapFactory.decodeFile(en.getAbsolutePath());
int ts = req.tileSource.getExpirationTimeMillis();
if(ts != -1 && req.url != null && time - en.lastModified() > ts) {
asyncLoadingThread.requestToDownload(req);
}
} catch (OutOfMemoryError e) {
log.error("Out of memory error", e); //$NON-NLS-1$
clearTiles();
}
}
}
if (bmp != null) {
cacheOfImages.put(req.tileId, bmp);
if (log.isDebugEnabled()) {
log.debug("Loaded file : " + req.tileId + " " + -(time - System.currentTimeMillis()) + " ms " + cacheOfImages.size()); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
}
}
if (cacheOfImages.get(req.tileId) == null && req.url != null) {
asyncLoadingThread.requestToDownload(req);
}
}
return cacheOfImages.get(req.tileId);
public boolean hasTileForMapSync(String file, ITileSource map, int x, int y, int zoom, boolean loadFromInternetIfNeeded) {
TilesCache cache = getTilesCache(map);
return cache != null && cache.getTileForMapSync(file, map, x, y, zoom, loadFromInternetIfNeeded) != null;
}
@ -1077,7 +922,9 @@ public class ResourceManager {
}
public synchronized void close(){
imagesOnFS.clear();
for (TilesCache tc : tilesCacheList) {
tc.close();
}
indexFileNames.clear();
basemapFileNames.clear();
renderer.clearAllResources();
@ -1140,7 +987,7 @@ public class ResourceManager {
if (lf != null) {
for (File f : lf) {
if (f != null && f.getName().endsWith(IndexConstants.BINARY_MAP_INDEX_EXT)) {
map.put(f.getName(), AndroidUtils.formatDate(context, f.lastModified())); //$NON-NLS-1$
map.put(f.getName(), AndroidUtils.formatDate(context, f.lastModified()));
}
}
}
@ -1149,12 +996,14 @@ public class ResourceManager {
}
public synchronized void reloadTilesFromFS() {
imagesOnFS.clear();
for (TilesCache tc : tilesCacheList) {
tc.tilesOnFS.clear();
}
}
/// On low memory method ///
public void onLowMemory() {
log.info("On low memory : cleaning tiles - size = " + cacheOfImages.size()); //$NON-NLS-1$
log.info("On low memory");
clearTiles();
for (RegionAddressRepository r : addressMap.values()) {
r.clearCache();
@ -1172,13 +1021,10 @@ public class ResourceManager {
return context.getRegions();
}
protected synchronized void clearTiles() {
log.info("Cleaning tiles - size = " + cacheOfImages.size()); //$NON-NLS-1$
ArrayList<String> list = new ArrayList<String>(cacheOfImages.keySet());
// remove first images (as we think they are older)
for (int i = 0; i < list.size() / 2; i++) {
cacheOfImages.remove(list.get(i));
log.info("Cleaning tiles...");
for (TilesCache tc : tilesCacheList) {
tc.clearTiles();
}
}

View file

@ -0,0 +1,234 @@
package net.osmand.plus.resources;
import net.osmand.IndexConstants;
import net.osmand.PlatformUtil;
import net.osmand.map.ITileSource;
import net.osmand.plus.SQLiteTileSource;
import net.osmand.plus.resources.AsyncLoadingThread.TileLoadDownloadRequest;
import org.apache.commons.logging.Log;
import java.io.File;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
public abstract class TilesCache<T> {
private AsyncLoadingThread asyncLoadingThread;
protected static final Log log = PlatformUtil.getLog(TilesCache.class);
Map<String, T> cache = new LinkedHashMap<>();
Map<String, Boolean> tilesOnFS = new LinkedHashMap<>();
protected File dirWithTiles;
protected int maxCacheSize = 30;
public TilesCache(AsyncLoadingThread asyncLoadingThread) {
this.asyncLoadingThread = asyncLoadingThread;
}
protected StringBuilder builder = new StringBuilder(40);
public int getMaxCacheSize() {
return maxCacheSize;
}
public void setMaxCacheSize(int maxCacheSize) {
this.maxCacheSize = maxCacheSize;
}
public File getDirWithTiles() {
return dirWithTiles;
}
public void setDirWithTiles(File dirWithTiles) {
this.dirWithTiles = dirWithTiles;
}
public abstract boolean isTileSourceSupported(ITileSource tileSource);
public synchronized String calculateTileId(ITileSource map, int x, int y, int zoom) {
builder.setLength(0);
if (map == null) {
builder.append(IndexConstants.TEMP_SOURCE_TO_LOAD);
} else {
builder.append(map.getName());
}
if (map instanceof SQLiteTileSource) {
builder.append('@');
} else {
builder.append('/');
}
builder.append(zoom).append('/').append(x).append('/').append(y).
append(map == null ? ".jpg" : map.getTileFormat()).append(".tile"); //$NON-NLS-1$ //$NON-NLS-2$
return builder.toString();
}
public synchronized boolean tileExistOnFileSystem(String file, ITileSource map, int x, int y, int zoom) {
if (!tilesOnFS.containsKey(file)) {
boolean ex = false;
if (map instanceof SQLiteTileSource){
if (((SQLiteTileSource) map).isLocked()){
return false;
}
ex = ((SQLiteTileSource) map).exists(x, y, zoom);
} else {
if(file == null){
file = calculateTileId(map, x, y, zoom);
}
ex = new File(dirWithTiles, file).exists();
}
if (ex) {
tilesOnFS.put(file, Boolean.TRUE);
} else {
tilesOnFS.put(file, null);
}
}
return tilesOnFS.get(file) != null || cache.get(file) != null;
}
public T getTileForMapAsync(String file, ITileSource map, int x, int y, int zoom, boolean loadFromInternetIfNeeded) {
return getTileForMap(file, map, x, y, zoom, loadFromInternetIfNeeded, false, true);
}
public T getTileForMapSync(String file, ITileSource map, int x, int y, int zoom, boolean loadFromInternetIfNeeded) {
return getTileForMap(file, map, x, y, zoom, loadFromInternetIfNeeded, true, true);
}
/**
* @param file - null could be passed if you do not call very often with that param
*/
protected T getTileForMap(String file, ITileSource map, int x, int y, int zoom,
boolean loadFromInternetIfNeeded, boolean sync, boolean loadFromFs) {
return getTileForMap(file, map, x, y, zoom, loadFromInternetIfNeeded, sync, loadFromFs, false);
}
protected synchronized T getTileForMap(String tileId, ITileSource map, int x, int y, int zoom,
boolean loadFromInternetIfNeeded, boolean sync,
boolean loadFromFs, boolean deleteBefore) {
if (tileId == null) {
tileId = calculateTileId(map, x, y, zoom);
if(tileId == null){
return null;
}
}
if (deleteBefore){
cache.remove(tileId);
if (map instanceof SQLiteTileSource) {
((SQLiteTileSource) map).deleteImage(x, y, zoom);
} else {
File f = new File(dirWithTiles, tileId);
if (f.exists()) {
f.delete();
}
}
tilesOnFS.put(tileId, null);
}
if (loadFromFs && cache.get(tileId) == null && map != null) {
boolean locked = map instanceof SQLiteTileSource && ((SQLiteTileSource) map).isLocked();
if (!loadFromInternetIfNeeded && !locked && !tileExistOnFileSystem(tileId, map, x, y, zoom)){
return null;
}
String url = loadFromInternetIfNeeded ? map.getUrlToLoad(x, y, zoom) : null;
File toSave = null;
if (url != null) {
if (map instanceof SQLiteTileSource) {
toSave = new File(dirWithTiles, calculateTileId(((SQLiteTileSource) map).getBase(), x, y, zoom));
} else {
toSave = new File(dirWithTiles, tileId);
}
}
TileLoadDownloadRequest req = new TileLoadDownloadRequest(dirWithTiles, url, toSave,
tileId, map, x, y, zoom, map.getReferer());
if (sync) {
return getRequestedTile(req);
} else {
asyncLoadingThread.requestToLoadTile(req);
}
}
return cache.get(tileId);
}
protected T getRequestedTile(TileLoadDownloadRequest req) {
if (req.tileId == null || req.dirWithTiles == null) {
return null;
}
T cacheObject = cache.get(req.tileId);
if (cacheObject != null) {
return cacheObject;
}
if (cache.size() > maxCacheSize) {
clearTiles();
}
if (req.dirWithTiles.canRead() && !asyncLoadingThread.isFileCurrentlyDownloaded(req.fileToSave)
&& !asyncLoadingThread.isFilePendingToDownload(req.fileToSave)) {
long time = System.currentTimeMillis();
if (log.isDebugEnabled()) {
log.debug("Start loaded file : " + req.tileId + " " + Thread.currentThread().getName());
}
T tileObject = getTileObject(req);
if (tileObject != null) {
cache.put(req.tileId, tileObject);
if (log.isDebugEnabled()) {
log.debug("Loaded file : " + req.tileId + " " + -(time - System.currentTimeMillis()) + " ms " + cache.size());
}
}
if (cache.get(req.tileId) == null && req.url != null) {
asyncLoadingThread.requestToDownload(req);
}
}
return cache.get(req.tileId);
}
protected abstract T getTileObject(TileLoadDownloadRequest req);
protected void downloadIfExpired(TileLoadDownloadRequest req, long lastModified) {
long time = System.currentTimeMillis();
int ts = req.tileSource.getExpirationTimeMillis();
if(ts != -1 && req.url != null && time - lastModified > ts) {
asyncLoadingThread.requestToDownload(req);
}
}
protected synchronized void clearTiles() {
log.info("Cleaning tiles - size = " + cache.size());
List<String> list = new ArrayList<>(cache.keySet());
// remove first images (as we think they are older)
for (int i = 0; i < list.size() / 2; i++) {
cache.remove(list.get(i));
}
}
public synchronized T get(String key) {
return cache.get(key);
}
public synchronized void put(String key, T value) {
cache.put(key, value);
}
public synchronized T remove(String key) {
return cache.remove(key);
}
public synchronized int size() {
return cache.size();
}
public synchronized Set<String> keySet() {
return cache.keySet();
}
public void close() {
tilesOnFS.clear();
}
}

View file

@ -1,5 +1,14 @@
package net.osmand.plus.views;
import android.annotation.SuppressLint;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.widget.Toast;
import net.osmand.data.QuadRect;
import net.osmand.data.RotatedTileBox;
import net.osmand.map.ITileSource;
@ -12,32 +21,24 @@ import net.osmand.plus.mapillary.MapillaryPlugin;
import net.osmand.plus.rastermaps.OsmandRasterMapsPlugin;
import net.osmand.plus.resources.ResourceManager;
import net.osmand.util.MapUtils;
import android.annotation.SuppressLint;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.widget.Toast;
public class MapTileLayer extends BaseMapLayer {
protected static final int emptyTileDivisor = 16;
public static final int OVERZOOM_IN = 2;
private final boolean mainMap;
protected final boolean mainMap;
protected ITileSource map = null;
protected MapTileAdapter mapTileAdapter = null;
Paint paintBitmap;
protected Paint paintBitmap;
protected RectF bitmapToDraw = new RectF();
protected Rect bitmapToZoom = new Rect();
protected OsmandMapTileView view;
protected ResourceManager resourceManager;
private OsmandSettings settings;
protected OsmandSettings settings;
private boolean visible = true;
@ -175,7 +176,7 @@ public class MapTileLayer extends BaseMapLayer {
boolean imgExist = mgr.tileExistOnFileSystem(ordImgTile, map, tileX, tileY, nzoom);
boolean originalWillBeLoaded = useInternet && nzoom <= maxLevel;
if (imgExist || originalWillBeLoaded) {
bmp = mgr.getTileImageForMapAsync(ordImgTile, map, tileX, tileY, nzoom, useInternet);
bmp = mgr.getBitmapTilesCache().getTileForMapAsync(ordImgTile, map, tileX, tileY, nzoom, useInternet);
}
if (bmp == null) {
int div = 1;
@ -188,14 +189,14 @@ public class MapTileLayer extends BaseMapLayer {
div *= 2;
String imgTileId = mgr.calculateTileId(map, tileX / div, tileY / div, nzoom - kzoom);
if (readFromCache) {
bmp = mgr.getTileImageFromCache(imgTileId);
bmp = mgr.getBitmapTilesCache().get(imgTileId);
if (bmp != null) {
break;
}
} else if (loadIfExists) {
if (mgr.tileExistOnFileSystem(imgTileId, map, tileX / div, tileY / div, nzoom - kzoom)
|| (useInternet && nzoom - kzoom <= maxLevel)) {
bmp = mgr.getTileImageForMapAsync(imgTileId, map, tileX / div, tileY / div, nzoom
bmp = mgr.getBitmapTilesCache().getTileForMapAsync(imgTileId, map, tileX / div, tileY / div, nzoom
- kzoom, useInternet);
break;
}
@ -243,7 +244,7 @@ public class MapTileLayer extends BaseMapLayer {
bitmapToZoom.width(), bitmapToZoom.height(), m, true);
bitmapToZoom.set(0, 0, tileSize, tileSize);
// very expensive that's why put in the cache
mgr.putTileInTheCache(ordImgTile, sampled);
mgr.getBitmapTilesCache().put(ordImgTile, sampled);
canvas.drawBitmap(sampled, bitmapToZoom, bitmapToDraw, paintBitmap);
}
}