Merge remote-tracking branch 'origin/master'
This commit is contained in:
commit
631db5a903
40 changed files with 7937 additions and 356 deletions
BIN
OsmAnd-java/libs/jts-core-1.14.0.jar
Normal file
BIN
OsmAnd-java/libs/jts-core-1.14.0.jar
Normal file
Binary file not shown.
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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) {}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
126
OsmAnd-java/src/com/wdtinc/mapbox_vector_tile/util/Vec2d.java
Normal file
126
OsmAnd-java/src/com/wdtinc/mapbox_vector_tile/util/Vec2d.java
Normal 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 + ")";
|
||||
}
|
||||
}
|
|
@ -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()));
|
||||
}
|
||||
}
|
4942
OsmAnd-java/src/net/osmand/binary/VectorTile.java
Normal file
4942
OsmAnd-java/src/net/osmand/binary/VectorTile.java
Normal file
File diff suppressed because it is too large
Load diff
18
OsmAnd-java/src/net/osmand/data/GeometryTile.java
Normal file
18
OsmAnd-java/src/net/osmand/data/GeometryTile.java
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
BIN
OsmAnd/libs/jts-core-1.14.0.jar
Normal file
BIN
OsmAnd/libs/jts-core-1.14.0.jar
Normal file
Binary file not shown.
|
@ -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()) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
15
OsmAnd/src/net/osmand/plus/mapillary/MapillaryImage.java
Normal file
15
OsmAnd/src/net/osmand/plus/mapillary/MapillaryImage.java
Normal 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;
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
160
OsmAnd/src/net/osmand/plus/mapillary/MapillaryVectorLayer.java
Normal file
160
OsmAnd/src/net/osmand/plus/mapillary/MapillaryVectorLayer.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
55
OsmAnd/src/net/osmand/plus/resources/BitmapTilesCache.java
Normal file
55
OsmAnd/src/net/osmand/plus/resources/BitmapTilesCache.java
Normal 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;
|
||||
}
|
||||
}
|
40
OsmAnd/src/net/osmand/plus/resources/GeometryTilesCache.java
Normal file
40
OsmAnd/src/net/osmand/plus/resources/GeometryTilesCache.java
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
@ -79,25 +76,18 @@ 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;
|
||||
|
||||
protected File dirWithTiles ;
|
||||
|
||||
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,16 +219,25 @@ 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() {
|
||||
return tileDownloader;
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -276,224 +283,62 @@ 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);
|
||||
}
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
private TilesCache getTilesCache(ITileSource map) {
|
||||
for (TilesCache tc : tilesCacheList) {
|
||||
if (tc.isTileSourceSupported(map)) {
|
||||
return tc;
|
||||
}
|
||||
}
|
||||
return imagesOnFS.get(file) != null || cacheOfImages.get(file) != null;
|
||||
}
|
||||
|
||||
public void clearTileImageForMap(String file, ITileSource map, int x, int y, int zoom){
|
||||
getTileImageForMap(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);
|
||||
return null;
|
||||
}
|
||||
|
||||
// introduce cache in order save memory
|
||||
public synchronized void tileDownloaded(DownloadRequest request){
|
||||
if (request instanceof TileLoadDownloadRequest) {
|
||||
TileLoadDownloadRequest req = ((TileLoadDownloadRequest) request);
|
||||
TilesCache cache = getTilesCache(req.tileSource);
|
||||
if (cache != null) {
|
||||
cache.tilesOnFS.put(req.tileId, Boolean.TRUE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected StringBuilder builder = new StringBuilder(40);
|
||||
protected char[] tileId = new char[120];
|
||||
public synchronized boolean tileExistOnFileSystem(String file, ITileSource map, int x, int y, int zoom) {
|
||||
TilesCache cache = getTilesCache(map);
|
||||
return cache != null && !cache.tilesOnFS.containsKey(file)
|
||||
&& cache.tileExistOnFileSystem(file, map, x, y, zoom);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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 boolean hasRequestedTile(TileLoadDownloadRequest req) {
|
||||
TilesCache cache = getTilesCache(req.tileSource);
|
||||
return cache != null && cache.getRequestedTile(req) != null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////// Working with indexes ////////////////////////////////////////////////
|
||||
////////////////////////////////////////////// Working with indexes ////////////////////////////////////////////////
|
||||
|
||||
public List<String> reloadIndexesOnStart(AppInitializer progress, List<String> warnings){
|
||||
close();
|
||||
|
@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1148,15 +995,17 @@ public class ResourceManager {
|
|||
return map;
|
||||
}
|
||||
|
||||
public synchronized void reloadTilesFromFS(){
|
||||
imagesOnFS.clear();
|
||||
public synchronized void reloadTilesFromFS() {
|
||||
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()){
|
||||
for (RegionAddressRepository r : addressMap.values()) {
|
||||
r.clearCache();
|
||||
}
|
||||
renderer.clearCache();
|
||||
|
@ -1171,14 +1020,11 @@ public class ResourceManager {
|
|||
public OsmandRegions getOsmandRegions() {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
234
OsmAnd/src/net/osmand/plus/resources/TilesCache.java
Normal file
234
OsmAnd/src/net/osmand/plus/resources/TilesCache.java
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue