package mil.nga.geopackage.tiles.features; import android.annotation.TargetApi; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import android.graphics.RectF; import android.util.Log; import android.util.LruCache; import java.util.List; import mil.nga.geopackage.BoundingBox; import mil.nga.geopackage.GeoPackage; import mil.nga.geopackage.GeoPackageException; import mil.nga.geopackage.extension.style.FeatureStyle; import mil.nga.geopackage.extension.style.IconRow; import mil.nga.geopackage.extension.style.StyleRow; import mil.nga.geopackage.features.index.FeatureIndexResults; import mil.nga.geopackage.features.user.FeatureCursor; import mil.nga.geopackage.features.user.FeatureDao; import mil.nga.geopackage.features.user.FeatureRow; import mil.nga.geopackage.geom.GeoPackageGeometryData; import mil.nga.geopackage.tiles.TileBoundingBoxUtils; import mil.nga.sf.CompoundCurve; import mil.nga.sf.Geometry; import mil.nga.sf.GeometryCollection; import mil.nga.sf.GeometryEnvelope; import mil.nga.sf.GeometryType; import mil.nga.sf.LineString; import mil.nga.sf.MultiLineString; import mil.nga.sf.MultiPoint; import mil.nga.sf.MultiPolygon; import mil.nga.sf.Point; import mil.nga.sf.Polygon; import mil.nga.sf.PolyhedralSurface; import mil.nga.sf.proj.ProjectionTransform; /** * Default Feature Tiles implementation using Android Graphics to draw tiles * from Well Known Binary Geometries * * @author osbornb * @since 1.3.1 */ public class DefaultFeatureTiles extends FeatureTiles { /** * Default max number of feature geometries to retain in cache * * @since 3.3.0 */ public static final int DEFAULT_GEOMETRY_CACHE_SIZE = 1000; /** * Geometry cache */ protected final LruCache<Long, GeoPackageGeometryData> geometryCache = new LruCache<>(DEFAULT_GEOMETRY_CACHE_SIZE); /** * When true, geometries are cached. Default is true */ protected boolean cacheGeometries = true; /** * Constructor * * @param context context * @param featureDao feature dao */ public DefaultFeatureTiles(Context context, FeatureDao featureDao) { super(context, featureDao); } /** * Constructor * * @param context context * @param featureDao feature dao * @param density display density: {@link android.util.DisplayMetrics#density} * @since 3.2.0 */ public DefaultFeatureTiles(Context context, FeatureDao featureDao, float density) { super(context, featureDao, density); } /** * Constructor * * @param context context * @param featureDao feature dao * @param width drawn tile width * @param height drawn tile height * @since 3.2.0 */ public DefaultFeatureTiles(Context context, FeatureDao featureDao, int width, int height) { super(context, featureDao, width, height); } /** * Constructor, auto creates the index manager for indexed tables and feature styles for styled tables * * @param context context * @param geoPackage GeoPackage * @param featureDao feature dao * @since 3.2.0 */ public DefaultFeatureTiles(Context context, GeoPackage geoPackage, FeatureDao featureDao) { super(context, geoPackage, featureDao); } /** * Constructor, auto creates the index manager for indexed tables and feature styles for styled tables * * @param context context * @param geoPackage GeoPackage * @param featureDao feature dao * @param density display density: {@link android.util.DisplayMetrics#density} * @since 3.2.0 */ public DefaultFeatureTiles(Context context, GeoPackage geoPackage, FeatureDao featureDao, float density) { super(context, geoPackage, featureDao, density); } /** * Constructor, auto creates the index manager for indexed tables and feature styles for styled tables * * @param context context * @param geoPackage GeoPackage * @param featureDao feature dao * @param width drawn tile width * @param height drawn tile height * @since 3.2.0 */ public DefaultFeatureTiles(Context context, GeoPackage geoPackage, FeatureDao featureDao, int width, int height) { super(context, geoPackage, featureDao, width, height); } /** * Constructor, auto creates the index manager for indexed tables and feature styles for styled tables * * @param context context * @param geoPackage GeoPackage * @param featureDao feature dao * @param density display density: {@link android.util.DisplayMetrics#density} * @param width drawn tile width * @param height drawn tile height * @since 3.2.0 */ public DefaultFeatureTiles(Context context, GeoPackage geoPackage, FeatureDao featureDao, float density, int width, int height) { super(context, geoPackage, featureDao, density, width, height); } /** * Constructor, only for retrieving default feature attributes * * @param context context */ public DefaultFeatureTiles(Context context) { this(context, null); } /** * Is caching geometries enabled? * * @return true if caching geometries * @since 3.3.0 */ public boolean isCacheGeometries() { return cacheGeometries; } /** * Set the cache geometries flag * * @param cacheGeometries true to cache geometries * @since 3.3.0 */ public void setCacheGeometries(boolean cacheGeometries) { this.cacheGeometries = cacheGeometries; } /** * {@inheritDoc} */ @Override public void clearCache() { super.clearCache(); clearGeometryCache(); } /** * Clear the geometry cache * * @since 3.3.0 */ public void clearGeometryCache() { geometryCache.evictAll(); } /** * Set / resize the geometry cache size * * @param size new size * @since 3.3.0 */ @TargetApi(21) public void setGeometryCacheSize(int size) { geometryCache.resize(size); } /** * {@inheritDoc} */ @Override public Bitmap drawTile(int zoom, BoundingBox boundingBox, FeatureIndexResults results) { FeatureTileCanvas canvas = new FeatureTileCanvas(tileWidth, tileHeight); ProjectionTransform transform = getProjectionToWebMercatorTransform(featureDao.getProjection()); BoundingBox expandedBoundingBox = expandBoundingBox(boundingBox); boolean drawn = false; for (FeatureRow featureRow : results) { if (drawFeature(zoom, boundingBox, expandedBoundingBox, transform, canvas, featureRow)) { drawn = true; } } results.close(); Bitmap bitmap = null; if (drawn) { bitmap = canvas.createBitmap(); bitmap = checkIfDrawn(bitmap); } else { canvas.recycle(); } return bitmap; } /** * {@inheritDoc} */ @Override public Bitmap drawTile(int zoom, BoundingBox boundingBox, FeatureCursor cursor) { FeatureTileCanvas canvas = new FeatureTileCanvas(tileWidth, tileHeight); ProjectionTransform transform = getProjectionToWebMercatorTransform(featureDao.getProjection()); BoundingBox expandedBoundingBox = expandBoundingBox(boundingBox); boolean drawn = false; while (cursor.moveToNext()) { FeatureRow row = cursor.getRow(); if (drawFeature(zoom, boundingBox, expandedBoundingBox, transform, canvas, row)) { drawn = true; } } cursor.close(); Bitmap bitmap = null; if (drawn) { bitmap = canvas.createBitmap(); bitmap = checkIfDrawn(bitmap); } else { canvas.recycle(); } return bitmap; } /** * {@inheritDoc} */ @Override public Bitmap drawTile(int zoom, BoundingBox boundingBox, List<FeatureRow> featureRow) { FeatureTileCanvas canvas = new FeatureTileCanvas(tileWidth, tileHeight); ProjectionTransform transform = getProjectionToWebMercatorTransform(featureDao.getProjection()); BoundingBox expandedBoundingBox = expandBoundingBox(boundingBox); boolean drawn = false; for (FeatureRow row : featureRow) { if (drawFeature(zoom, boundingBox, expandedBoundingBox, transform, canvas, row)) { drawn = true; } } Bitmap bitmap = null; if (drawn) { bitmap = canvas.createBitmap(); bitmap = checkIfDrawn(bitmap); } else { canvas.recycle(); } return bitmap; } /** * Draw the feature on the canvas * * @param zoom zoom level * @param boundingBox bounding box * @param expandedBoundingBox expanded bounding box * @param transform projection transform * @param canvas feature tile canvas * @param row feature row * @return true if at least one feature was drawn */ private boolean drawFeature(int zoom, BoundingBox boundingBox, BoundingBox expandedBoundingBox, ProjectionTransform transform, FeatureTileCanvas canvas, FeatureRow row) { boolean drawn = false; try { GeoPackageGeometryData geomData = null; BoundingBox transformedBoundingBox = null; long rowId = -1; // Check the cache for the geometry data if (cacheGeometries) { rowId = row.getId(); geomData = geometryCache.get(rowId); if (geomData != null) { transformedBoundingBox = new BoundingBox(geomData.getEnvelope()); } } if (geomData == null) { // Read the geometry geomData = row.getGeometry(); } if (geomData != null) { Geometry geometry = geomData.getGeometry(); if (geometry != null) { if (transformedBoundingBox == null) { GeometryEnvelope envelope = geomData.getOrBuildEnvelope(); BoundingBox geometryBoundingBox = new BoundingBox(envelope); transformedBoundingBox = geometryBoundingBox.transform(transform); if (cacheGeometries) { // Set the geometry envelope to the transformed bounding box geomData.setEnvelope(transformedBoundingBox.buildEnvelope()); } } if (cacheGeometries) { // Cache the geometry geometryCache.put(rowId, geomData); } if (expandedBoundingBox.intersects(transformedBoundingBox, true)) { double simplifyTolerance = TileBoundingBoxUtils.toleranceDistance(zoom, tileWidth, tileHeight); drawn = drawShape(simplifyTolerance, boundingBox, transform, canvas, row, geometry); } } } } catch (Exception e) { Log.e(DefaultFeatureTiles.class.getSimpleName(), "Failed to draw feature in tile. Table: " + featureDao.getTableName(), e); } return drawn; } /** * Draw the geometry on the canvas * * @param simplifyTolerance simplify tolerance in meters * @param boundingBox bounding box * @param transform projection transform * @param canvas feature tile canvas * @param featureRow feature row * @param geometry feature geometry * @return true if drawn */ private boolean drawShape(double simplifyTolerance, BoundingBox boundingBox, ProjectionTransform transform, FeatureTileCanvas canvas, FeatureRow featureRow, Geometry geometry) { boolean drawn = false; GeometryType geometryType = geometry.getGeometryType(); FeatureStyle featureStyle = getFeatureStyle(featureRow, geometryType); switch (geometryType) { case POINT: Point point = (Point) geometry; drawn = drawPoint(boundingBox, transform, canvas, point, featureStyle); break; case LINESTRING: case CIRCULARSTRING: LineString lineString = (LineString) geometry; Path linePath = new Path(); addLineString(simplifyTolerance, boundingBox, transform, linePath, lineString); drawn = drawLinePath(canvas, linePath, featureStyle); break; case POLYGON: case TRIANGLE: Polygon polygon = (Polygon) geometry; Path polygonPath = new Path(); addPolygon(simplifyTolerance, boundingBox, transform, polygonPath, polygon); drawn = drawPolygonPath(canvas, polygonPath, featureStyle); break; case MULTIPOINT: MultiPoint multiPoint = (MultiPoint) geometry; for (Point pointFromMulti : multiPoint.getPoints()) { drawn = drawPoint(boundingBox, transform, canvas, pointFromMulti, featureStyle) || drawn; } break; case MULTILINESTRING: MultiLineString multiLineString = (MultiLineString) geometry; Path multiLinePath = new Path(); for (LineString lineStringFromMulti : multiLineString.getLineStrings()) { addLineString(simplifyTolerance, boundingBox, transform, multiLinePath, lineStringFromMulti); } drawn = drawLinePath(canvas, multiLinePath, featureStyle); break; case MULTIPOLYGON: MultiPolygon multiPolygon = (MultiPolygon) geometry; Path multiPolygonPath = new Path(); for (Polygon polygonFromMulti : multiPolygon.getPolygons()) { addPolygon(simplifyTolerance, boundingBox, transform, multiPolygonPath, polygonFromMulti); } drawn = drawPolygonPath(canvas, multiPolygonPath, featureStyle); break; case COMPOUNDCURVE: CompoundCurve compoundCurve = (CompoundCurve) geometry; Path compoundCurvePath = new Path(); for (LineString lineStringFromCompoundCurve : compoundCurve.getLineStrings()) { addLineString(simplifyTolerance, boundingBox, transform, compoundCurvePath, lineStringFromCompoundCurve); } drawn = drawLinePath(canvas, compoundCurvePath, featureStyle); break; case POLYHEDRALSURFACE: case TIN: PolyhedralSurface polyhedralSurface = (PolyhedralSurface) geometry; Path polyhedralSurfacePath = new Path(); for (Polygon polygonFromPolyhedralSurface : polyhedralSurface.getPolygons()) { addPolygon(simplifyTolerance, boundingBox, transform, polyhedralSurfacePath, polygonFromPolyhedralSurface); } drawn = drawPolygonPath(canvas, polyhedralSurfacePath, featureStyle); break; case GEOMETRYCOLLECTION: @SuppressWarnings("unchecked") GeometryCollection<Geometry> geometryCollection = (GeometryCollection) geometry; List<Geometry> geometries = geometryCollection.getGeometries(); for (Geometry geometryFromCollection : geometries) { drawn = drawShape(simplifyTolerance, boundingBox, transform, canvas, featureRow, geometryFromCollection) || drawn; } break; default: throw new GeoPackageException("Unsupported Geometry Type: " + geometry.getGeometryType().getName()); } return drawn; } /** * Draw the line path on the canvas * * @param canvas canvas * @param path path * @param featureStyle feature style * @return true if drawn */ private boolean drawLinePath(FeatureTileCanvas canvas, Path path, FeatureStyle featureStyle) { Canvas lineCanvas = canvas.getLineCanvas(); Paint pathPaint = getLinePaint(featureStyle); lineCanvas.drawPath(path, pathPaint); return true; } /** * Draw the path on the canvas * * @param canvas canvas * @param path path * @param featureStyle feature style */ private boolean drawPolygonPath(FeatureTileCanvas canvas, Path path, FeatureStyle featureStyle) { Canvas polygonCanvas = canvas.getPolygonCanvas(); Paint fillPaint = getPolygonFillPaint(featureStyle); if (fillPaint != null) { path.setFillType(Path.FillType.EVEN_ODD); polygonCanvas.drawPath(path, fillPaint); } Paint pathPaint = getPolygonPaint(featureStyle); polygonCanvas.drawPath(path, pathPaint); return true; } /** * Add the linestring to the path * * @param simplifyTolerance simplify tolerance in meters * @param boundingBox bounding box * @param transform projection transform * @param path path * @param lineString line string */ private void addLineString(double simplifyTolerance, BoundingBox boundingBox, ProjectionTransform transform, Path path, LineString lineString) { List<Point> points = lineString.getPoints(); if (points.size() >= 2) { // Try to simplify the number of points in the LineString points = simplifyPoints(simplifyTolerance, points); for (int i = 0; i < points.size(); i++) { Point point = points.get(i); Point webMercatorPoint = transform.transform(point); float x = TileBoundingBoxUtils.getXPixel(tileWidth, boundingBox, webMercatorPoint.getX()); float y = TileBoundingBoxUtils.getYPixel(tileHeight, boundingBox, webMercatorPoint.getY()); if (i == 0) { path.moveTo(x, y); } else { path.lineTo(x, y); } } } } /** * Add the polygon on the canvas * * @param simplifyTolerance simplify tolerance in meters * @param boundingBox bounding box * @param transform projection transform * @param path path * @param polygon polygon */ private void addPolygon(double simplifyTolerance, BoundingBox boundingBox, ProjectionTransform transform, Path path, Polygon polygon) { List<LineString> rings = polygon.getRings(); if (!rings.isEmpty()) { // Add the polygon points LineString polygonLineString = rings.get(0); List<Point> polygonPoints = polygonLineString.getPoints(); if (polygonPoints.size() >= 2) { addRing(simplifyTolerance, boundingBox, transform, path, polygonPoints); // Add the holes for (int i = 1; i < rings.size(); i++) { LineString holeLineString = rings.get(i); List<Point> holePoints = holeLineString.getPoints(); if (holePoints.size() >= 2) { addRing(simplifyTolerance, boundingBox, transform, path, holePoints); } } } } } /** * Add a ring * * @param simplifyTolerance simplify tolerance in meters * @param boundingBox bounding box * @param transform projection transform * @param path path * @param points points */ private void addRing(double simplifyTolerance, BoundingBox boundingBox, ProjectionTransform transform, Path path, List<Point> points) { // Try to simplify the number of points in the LineString points = simplifyPoints(simplifyTolerance, points); for (int i = 0; i < points.size(); i++) { Point point = points.get(i); Point webMercatorPoint = transform.transform(point); float x = TileBoundingBoxUtils.getXPixel(tileWidth, boundingBox, webMercatorPoint.getX()); float y = TileBoundingBoxUtils.getYPixel(tileHeight, boundingBox, webMercatorPoint.getY()); if (i == 0) { path.moveTo(x, y); } else { path.lineTo(x, y); } } path.close(); } /** * Draw the point on the canvas * * @param boundingBox bounding box * @param transform projection transform * @param canvas draw canvas * @param point point * @param featureStyle feature style * @return true if drawn */ private boolean drawPoint(BoundingBox boundingBox, ProjectionTransform transform, FeatureTileCanvas canvas, Point point, FeatureStyle featureStyle) { boolean drawn = false; Point webMercatorPoint = transform.transform(point); float x = TileBoundingBoxUtils.getXPixel(tileWidth, boundingBox, webMercatorPoint.getX()); float y = TileBoundingBoxUtils.getYPixel(tileHeight, boundingBox, webMercatorPoint.getY()); if (featureStyle != null && featureStyle.useIcon()) { IconRow iconRow = featureStyle.getIcon(); Bitmap icon = getIcon(iconRow); int width = icon.getWidth(); int height = icon.getHeight(); if (x >= 0 - width && x <= tileWidth + width && y >= 0 - height && y <= tileHeight + height) { float anchorU = (float) iconRow.getAnchorUOrDefault(); float anchorV = (float) iconRow.getAnchorVOrDefault(); float left = x - (anchorU * width); float right = left + width; float top = y - (anchorV * height); float bottom = top + height; RectF destination = new RectF(left, top, right, bottom); Canvas iconCanvas = canvas.getIconCanvas(); iconCanvas.drawBitmap(icon, null, destination, pointPaint); drawn = true; } } else if (pointIcon != null) { float width = this.density * pointIcon.getWidth(); float height = this.density * pointIcon.getHeight(); if (x >= 0 - width && x <= tileWidth + width && y >= 0 - height && y <= tileHeight + height) { Canvas iconCanvas = canvas.getIconCanvas(); float left = x - this.density * pointIcon.getXOffset(); float top = y - this.density * pointIcon.getYOffset(); RectF rect = new RectF(left, top, left + width, top + height); iconCanvas.drawBitmap(pointIcon.getIcon(), null, rect, pointPaint); drawn = true; } } else { Float radius = null; if (featureStyle != null) { StyleRow styleRow = featureStyle.getStyle(); if (styleRow != null) { radius = this.density * (float) (styleRow.getWidthOrDefault() / 2.0f); } } if (radius == null) { radius = this.density * pointRadius; } if (x >= 0 - radius && x <= tileWidth + radius && y >= 0 - radius && y <= tileHeight + radius) { Paint pointPaint = getPointPaint(featureStyle); Canvas pointCanvas = canvas.getPointCanvas(); pointCanvas.drawCircle(x, y, radius, pointPaint); drawn = true; } } return drawn; } }