I am able to display heat map properly
// Create a heat map tile provider, passing it the latlngs of the police stations.
mProvider = new HeatmapTileProvider.Builder()
.data(list)
.build();
// Add a tile overlay to the map, using the heat map tile provider.
mOverlay = mMap.addTileOverlay(new TileOverlayOptions().tileProvider(mProvider));
i have used this lib compile 'com.google.maps.android:android-maps-utils:0.5+'
is there anyway i can get click events for added heat map?
Heat Map is just TileOverlay with no OnClickListener interface. So, if you need just color of heat Map clicked point, you can do that in GoogleMap.onMapClickListener() this way:
mGoogleMap.setOnMapClickListener(new GoogleMap.OnMapClickListener() {
#Override
public void onMapClick(LatLng latLng) {
// get current zoom
int zoom = (int)mGoogleMap.getCameraPosition().zoom;
// get tile top-left coordinates in tile coordinate system
int tileX = getTileX(latLng, zoom);
int tileY = getTileY(latLng, zoom);
// get Tile object (with heatmap color data) from tile provider
Tile tile = mProvider.getTile(tileX, tileY, zoom);
if (tile.data == null) {
return;
}
// decode heatmap data into bitmap
Bitmap bitmap = BitmapFactory.decodeByteArray(tile.data, 0, tile.data.length);
// get tile coordinates in pixels
LatLng tileNorthWest = new LatLng(tile2lat(tileY, zoom), tile2long(tileX, zoom));
long tileNorthWestX = lonToX(tileNorthWest.longitude, zoom);
long tileNorthWestY = latToY(tileNorthWest.latitude, zoom);
// get "click" point coordinates in pixels
long pointNorthWestX = lonToX(latLng.longitude, zoom);
long pointNorthWestY = latToY(latLng.latitude, zoom);
// calculate offset of "click" point within current tile
// x2 because of hi density tiles 512x512
long dx = 2 * (pointNorthWestX - tileNorthWestX);
long dy = 2 * (pointNorthWestY - tileNorthWestY);
// test calculated coordinates and get color of clicked point as Heat Map data
if (dx >= 0 && dx < bitmap.getWidth() && dy >= 0 && dy < bitmap.getHeight()) {
// dx, dy - coordinates of current tile of target heatmap
// pixelColor is color value of target heatmap
int pixelColor = bitmap.getPixel((int) dx, (int) dy);
}
}
});
where
public static int getTileX(final LatLng latLng, final int zoom) {
int tileX = (int)Math.floor((latLng.longitude + 180) / 360 * (1 << zoom));
if (tileX < 0)
tileX = 0;
if (tileX >= (1 << zoom))
tileX = (1 << zoom)-1;
return tileX;
}
public static int getTileY(final LatLng latLng, final int zoom) {
int tileY = (int) Math.floor( (1 - Math.log(Math.tan(Math.toRadians(latLng.latitude)) + 1 / Math.cos(Math.toRadians(latLng.latitude))) / Math.PI) / 2 * (1 << zoom) ) ;
if (tileY < 0)
tileY = 0;
if (tileY >= (1 << zoom))
tileY=((1 << zoom)-1);
return tileY;
}
public static long lonToX(double lon, int zoom) {
int offset = 256 << (zoom - 1);
return (int)Math.floor(offset + (offset * lon / 180));
}
public static long latToY(double lat, int zoom) {
int offset = 256 << (zoom - 1);
return (int)Math.floor(offset - offset / Math.PI * Math.log((1 + Math.sin(Math.toRadians(lat)))
/ (1 - Math.sin(Math.toRadians(lat)))) / 2);
}
public static double tile2long(int x, int zoom) {
return (x / Math.pow(2,zoom) * 360 - 180);
}
public static double tile2lat(int y, int zoom) {
double n = Math.PI - 2 * Math.PI * y /Math.pow(2,zoom);
return (180 / Math.PI * Math.atan(0.5 * (Math.exp(n)-Math.exp(-n))));
}
But if you need not color, but numeric value, you should calculate int based on color, or create CustomTile class for store not only x,y and bitmap data, but array with calculated numeric values for heatmap.
I'm trying to develop simple board game. The board is of size 9x9 fields. The balls are appearing on the fields and when the user clicks on the field with the ball, the ball starts to jumping. I implemented the animation in two ways. The first one is working, but it's not easy to add another one following animation (like little stretch or something). And the second one, which seems to be better (there is used the AnimatorSet) is not working. When user clicks on the field with the ball, the ball disappears. I have no idea why :-(.
The first class implements the board and it is the child of View:
public class BoardView extends View {
...
/**
* Initializes fields of the board.
*/
private void initializeFields() {
this.fields = new ArrayList<Field>();
for (int row = 0; row < BoardView.FIELDS_NUMBER; row++) {
for (int column = 0; column < BoardView.FIELDS_NUMBER; column++) {
this.fields.add(new Field(this, row, column));
}
}
}
#Override
protected void onDraw(Canvas canvas) {
canvas.drawColor(BoardView.COLOR_ACTIVITY);
if (this.fields == null) {
this.initializeFields();
}
for (int i = 0; i < this.fields.size(); i++) {
this.fields.get(i).draw(canvas);
}
}
...
}
The second one implements the field:
public class Field {
...
/**
* Draws itself on the screen.
*
* #param Canvas canvas
*/
public void draw(Canvas canvas) {
Rect field = this.getRect();
int round = (int)Math.floor(this.board.getFieldSize() / 4);
this.board.getPainter().setStyle(Paint.Style.FILL);
this.board.getPainter().setColor(Field.COLOR_DEFAULT);
// draw field
canvas.drawRoundRect(new RectF(field), round, round, this.board.getPainter());
// draw selected field
if (this.selected) {
this.board.getPainter().setColor(Field.COLOR_SELECTED);
canvas.drawRoundRect(new RectF(field), round, round, this.board.getPainter());
}
// draw ball
if (this.ball != null) {
Point fieldOrigin = new Point(field.left, field.top);
if (this.selected) {
this.ball.animate(canvas, fieldOrigin);
} else {
this.ball.draw(canvas, fieldOrigin);
}
}
}
...
}
And the last one implements the ball:
Here is the first method, which completely works, but it's not flexible enough:
public class Ball {
...
/**
* Draws itself on the screen.
*
* #param Canvas canvas
* #param Point fieldOrigin
*/
public void draw(Canvas canvas, Point fieldOrigin) {
// set painter
Paint painter = this.field.getBoard().getPainter();
painter.setStyle(Paint.Style.FILL);
painter.setColor(Ball.COLORS[this.color]);
// calculate parameters
float halfSize = this.field.getBoard().getFieldSize() / 2;
float cX = fieldOrigin.x + halfSize;
float cY = fieldOrigin.y + halfSize + this.dy;
float radius = 0.6f * halfSize;
// draw circle
canvas.drawCircle(cX, cY, radius, painter);
// the code continues, because of the shadow and light simulation (radial gradients)
}
/**
* Draws jumping animation.
*
* #param Canvas canvas
* #param Point fieldOrigin
*/
public void animate(Canvas canvas, Point fieldOrigin) {
float currentDy = (this.dy - 0.1f);
this.setDy((float)Math.abs(Math.sin(currentDy)) * (-0.15f * this.field.getBoard().getFieldSize()));
this.draw(canvas, fieldOrigin);
this.setDy(currentDy);
try {
Thread.sleep(Ball.ANIMATION_DELAY);
} catch (InterruptedException e) {}
this.field.invalidate();
}
...
}
As you can see, the animation is implemented by sleeping the current Thread and changing parameter dy.
The second method is showing the ball on the field, but the animation is not working as I said in the beginning of the post (after click, the ball disappears):
public class BallShape {
private Field field;
private LayerDrawable ball;
private int color;
private float diameter,
x, y; // top left corner - THE GETTERS AND SETTERS ARE IMPLEMENTED (because of Animator)
...
/**
* Initializes the ball.
*
* #param Field field
* #param int color
*/
public BallShape(Field field, int color) {
this.field = field;
this.color = ((color == Ball.COLOR_RANDOM) ? Ball.randomColor() : color);
// create ball
float halfSize = this.field.getBoard().getFieldSize() / 2;
this.diameter = 0.6f * field.getBoard().getFieldSize();
float radius = this.diameter / 2;
Rect fieldArea = field.getRect();
this.x = fieldArea.left + halfSize - radius;
this.y = fieldArea.top + halfSize - radius;
// color circle
OvalShape circleShape = new OvalShape();
circleShape.resize(this.diameter, this.diameter);
ShapeDrawable circle = new ShapeDrawable(circleShape);
this.initPainter(circle.getPaint());
// the code continues, because of the shadow and light simulation (radial gradients)
// compound shape - ball
ShapeDrawable[] compound = { circle };//, shadow, light };
this.ball = new LayerDrawable(compound);
}
/**
* Draws itself on the screen.
*
* #param Canvas canvas
* #param Point fieldOrigin
*/
public void draw(Canvas canvas, Point fieldOrigin) {
canvas.save();
canvas.translate(this.x, this.y);
this.ball.draw(canvas);
canvas.restore();
}
/**
* Draws jumping animation.
*
* #param Canvas canvas
* #param Point fieldOrigin
*/
public void animate(Canvas canvas, Point fieldOrigin) {
// common data
float halfSize = this.field.getBoard().getFieldSize() / 2;
float radius = this.diameter / 2;
float startY = fieldOrigin.y + halfSize - radius;
float endY = startY - halfSize + 2;
// bounce animation
ValueAnimator bounceAnimation = ObjectAnimator.ofFloat(this, "y", startY, endY);
bounceAnimation.setDuration(BallShape.ANIMATION_LENGTH);
bounceAnimation.setInterpolator(new AccelerateInterpolator());
bounceAnimation.setRepeatCount(ValueAnimator.INFINITE);
bounceAnimation.setRepeatMode(ValueAnimator.REVERSE);
//bounceAnimation.start();
// animation
AnimatorSet bouncer = new AnimatorSet();
bouncer.play(bounceAnimation);
// start the animation
bouncer.start();
}
...
}
Any idea why it's not working? What I've done wrong?
Thank you very, very much.
Two things I would fix.
First of all you start animation in draw() method. You should either start it in onClick() or at least set this.selected to false, to not start it on every draw(). Secondly, after your value animator changes a property, you need to redraw the BallShape. Otherwise nothing will change. For instance you can define setY(float Y) method, change Y there and call invalidate().
From the image you can see that the ball fired on the left that fire behind it, does not match the calculated trajectory. Im drawing the ball trajectory using an equation from a SO question, this is modified to take into consideration the box2d steps of 30 frames per second. This does calculate a valid trajectory but it does not match the actual trajectory of the ball, the ball has a smaller trajectory. I am applying a box2d force to the ball, this also has a density set and a shape. The shape radius varies depending on the type of ball. Im setting the start velocity in the touchdown event.
public class ProjectileEquation {
public float gravity;
public Vector2 startVelocity = new Vector2();
public Vector2 startPoint = new Vector2();
public Vector2 gravityVec = new Vector2(0,-10f);
public float getX(float n) {
return startVelocity.x * (n * 1/30f) + startPoint.x;
}
public float getY(float n) {
float t = 1/30f * n;
return 0.5f * gravity * t * t + startVelocity.y * t + startPoint.y;
}
}
#Override
public void draw(SpriteBatch batch, float parentAlpha) {
float t = 0f;
float width = this.getWidth();
float height = this.getHeight();
float timeSeparation = this.timeSeparation;
for (int i = 0; i < trajectoryPointCount; i+=timeSeparation) {
//projectileEquation.getTrajectoryPoint(this.getX(), this.getY(), i);
float x = this.getX() + projectileEquation.getX(i);
float y = this.getY() + projectileEquation.getY(i);
batch.setColor(this.getColor());
if(trajectorySprite != null) batch.draw(trajectorySprite, x, y, width, height);
// t += timeSeparation;
}
}
public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) {
if(button==1 || world.showingDialog)return false;
touchPos.set(x, y);
float angle = touchPos.sub(playerCannon.position).angle();
if(angle > 270 ) {
angle = 0;
}
else if(angle >70) {
angle = 70;
}
playerCannon.setAngle(angle);
world.trajPath.controller.angle = angle;
float radians = (float) angle * MathUtils.degreesToRadians;
float ballSpeed = touchPos.sub(playerCannon.position).len()*12;
world.trajPath.projectileEquation.startVelocity.x = (float) (Math.cos(radians) * ballSpeed);
world.trajPath.projectileEquation.startVelocity.y = (float) (Math.sin(radians) * ballSpeed);
return true;
}
public CannonBall(float x, float y, float width, float height, float damage, World world, Cannon cannonOwner) {
super(x, y, width, height, damage, world);
active = false;
shape = new CircleShape();
shape.setRadius(width/2);
FixtureDef fd = new FixtureDef();
fd.shape = shape;
fd.density = 4.5f;
if(cannonOwner.isEnemy) { //Enemy cannon balls cannot hit other enemy cannons just the player
fd.filter.groupIndex = -16;
}
bodyDef.type = BodyType.DynamicBody;
bodyDef.position.set(this.position);
body = world.createBody(bodyDef);
body.createFixture(fd);
body.setUserData(this);
body.setBullet(true);
this.cannonOwner = cannonOwner;
this.hitByBall = null;
this.particleEffect = null;
}
private CannonBall createCannonBall(float radians, float ballSpeed, float radius, float damage)
{
CannonBall cannonBall = new CannonBall(CannonEnd().x, CannonEnd().y, radius * ballSizeMultiplier, radius * ballSizeMultiplier, damage, this.world, this);
cannonBall.velocity.x = (float) (Math.cos(radians) * ballSpeed);
//cannonBall.velocity.x = (float) ((Math.sqrt(10) * Math.sqrt(29) *
// Math.sqrt((Math.tan(cannon.angle)*Math.tan(cannon.angle))+1)) / Math.sqrt(2 * Math.tan(cannon.angle) - (2 * 10 * 2)/29))* -1f;
cannonBall.velocity.y = (float) (Math.sin(radians) * ballSpeed);
cannonBall.active = true;
//cannonBall.body.applyLinearImpulse(cannonBall.velocity, cannonBall.position);
cannonBall.body.applyForce(cannonBall.velocity, cannonBall.position );
return cannonBall;
}
trajPath = new TrajectoryActor(-10f);
trajPath.setX(playerCannon.CannonEnd().x);
trajPath.setY(playerCannon.CannonEnd().y);
trajPath.setWidth(10f);
trajPath.setHeight(10f);
stage.addActor(trajPath);
Here is a code that I used for one of my other games, which proved to be very precise. The trick is to apply the impulse on the body and read the initial velocity. Having that I calculate 10 positions where the body will be within 0.5 seconds. The language is called Squirrel which is Lua based with C/C++ like syntax. You should be able to grasp what is going on there. What returns from the getTrajectoryPointsForObjectAtImpulse is an array of 10 positions through which the ball will pass within 0.5 seconds.
const TIMESTER_DIVIDOR = 60.0;
function getTrajectoryPoint( startingPosition, startingVelocity, n )
{
local gravity = box2DWorld.GetGravity();
local t = 1 / 60.0;
local stepVelocity = b2Vec2.Create( t * startingVelocity.x, t * startingVelocity.y );
local stepGravity = b2Vec2.Create( t * t * gravity.x, t * t * gravity.y );
local result = b2Vec2.Create( 0, 0 );
result.x = ( startingPosition.x + n * stepVelocity.x + 0.5 * ( n * n + n ) * stepGravity.x ) * MTP;
result.y = ( startingPosition.y + n * stepVelocity.y + 0.5 * ( n * n + n ) * stepGravity.y ) * -MTP;
return result;
}
function getTrajectoryPointsForObjectAtImpulse (object, impulse)
{
if( !object || !impulse ) return [];
local result = [];
object.bBody.ApplyLinearImpulse( impulse, object.bBody.GetWorldCenter() );
local initialVelocity = object.bBody.GetLinearVelocity();
object.bBody.SetLinearVelocity( b2Vec2.Create(0, 0) );
object.bBody.SetActive(false);
for ( local i = 0.0 ; i < ( 0.5 * TIMESTER_DIVIDOR ) ; )
{
result.append( getTrajectoryPoint(object.bBody.GetPosition(), initialVelocity, i.tointeger() ) );
i += ( (0.5 * TIMESTER_DIVIDOR) * 0.1 );
}
return result;
}
If you do not understand any part of the code, please let me know and I will try to explain.
In V1 of Google Maps for Android I implemented a form of clustering using the ItemizedOverlay class. Each cluster has a center point (lat/lon), radius (in miles) and a count of the number of items in the cluster. I used ItemizedOverlay.draw(Canvas, MapView, shadow) to draw the clusters using Canvas object methods such as drawCircle() and drawText(). Each cluster consisted of a filled circle (sized according to the space required for the count) containing the count and a circle showing the radius.
After reading the docs on V2 and playing with the demo app, I see no equivalent to ItemizedOverlay, and no obvious alternative. I suspect that the only way to do this is to maintain a list of clusters myself, and then subclass MapView and provide an implementation of its onDraw() method.
Have I missed something in the V2 API that would be better than subclassing MapView?
Thanks,
Mark
After some research, I haven't found any better option than creating Bitmap for Marker on the fly either. But I'm also creating circle with polygon on the map. Be aware that this is not really high-performance solution, but for my case it is a decent option.
Code sample:
private static final int CIRCLE_POLYGON_VERTICES = 16;
private static final double EARTH_RADIUS = 6378.1d;
private List<LatLng> createCirclePolygon(LatLng center, double r) {
List<LatLng> res = new ArrayList<LatLng>(CIRCLE_POLYGON_VERTICES);
double r_latitude = MathUtils.rad2deg(r/EARTH_RADIUS);
double r_longitude = r_latitude / Math.cos(MathUtils.deg2rad(center.latitude));
for (int point = 0; point < CIRCLE_POLYGON_VERTICES + 1; point++) {
double theta = Math.PI * ((double)point / (CIRCLE_POLYGON_VERTICES / 2));
double circle_x = center.longitude + (r_longitude * Math.cos(theta));
double circle_y = center.latitude + (r_latitude * Math.sin(theta));
res.add(new LatLng(circle_y, circle_x));
}
return res;
}
private Bitmap getClusteredLabel(String cnt, Context ctx) {
Resources r = ctx.getResources();
Bitmap res = BitmapFactory.decodeResource(r, R.drawable.map_cluster_bg);
res = res.copy(Bitmap.Config.ARGB_8888, true);
Canvas c = new Canvas(res);
Paint textPaint = new Paint();
textPaint.setAntiAlias(true);
textPaint.setTextAlign(Paint.Align.CENTER);
textPaint.setTypeface(Typeface.DEFAULT_BOLD);
textPaint.setColor(Color.WHITE);
textPaint.setTextSize(21);
c.drawText(String.valueOf(cnt), res.getWidth()/2, res.getHeight()/2 + textPaint.getTextSize() / 3, textPaint);
return res;
}
public void createClusteredOverlay(MapPinData point, GoogleMap map, Context ctx) {
if (point.getCount() > 1) {
map.addMarker(new MarkerOptions().position(point.getLatLng()).anchor(0.5f, 0.5f).icon(BitmapDescriptorFactory.fromBitmap(getClusteredLabel(String.valueOf(point.getCount()), ctx))));
map.addPolygon(new PolygonOptions()
.addAll(createCirclePolygon(point.getLatLng(), point.getRadius()))
.fillColor(Color.argb(50, 0, 0, 10))
.strokeWidth(0)
);
} else {
map.addMarker(new MarkerOptions().position(point.getLatLng()).title(point.getTitle()));
}
}
My MathUtils methods:
public static double deg2rad(double deg) {
return (deg * Math.PI / 180.0);
}
public static double rad2deg(double rad) {
return (rad * 180.0 / Math.PI);
}
If you have radius in miles, you should change EARTH_RADIUS constant to miles, 3963 AFAIK.
I want to draw polyline in android maps api version 2. I want it to have many colors, preferably with gradients. It seems to me though, that polylines are allowed to have only single color.
How can I do that? I already have api-v1 overlay drawing what I like, so presumably I can reuse some code
public class RouteOverlayGoogle extends Overlay {
public void draw(Canvas canvas, MapView mapView, boolean shadow) {
//(...) draws line with color representing speed
}
I know it's been a pretty long time since this has been asked, but there are still no gradient polylines (as of writing, ~may 2015) and drawing multiple polylines really doesn't cut it (jagged edges, quite a bit of lag when dealing with several hundred of points, just not very visually appealing).
When I had to implement gradient polylines, what I ended up doing was implementing a TileOverlay that would render the polyline to a canvas and then rasterize it (see this gist for the specific code I wrote to do it https://gist.github.com/Dagothig/5f9cf0a4a7a42901a7b2).
The implementation doesn't try to do any sort of viewport culling because I ended up not needing it to reach the performance I wanted (I'm not sure about the numbers, but it was under a second per tiles, and multiple tiles will be rendered at the same time).
Rendering the gradient polyline can be pretty tricky to get properly however since you're dealing with varying viewports (positions and size): more than that, I hit a few issues with the limit on float precision at high zoom levels (20+). In the end I didn't use the scale and translate functions from the canvas because I would get weird corruption issues.
Something else to watch out for if you use a similar data structure to what I had (latitudes, longitudes and timestamps) is that you need multiple segments to render the gradient properly (I ended up working with 3 points at a time).
For posterity's sake, I'm going to also leave the code from the gist here:
(the projections are done using https://github.com/googlemaps/android-maps-utils if you're wondering where com.google.maps.android.projection.SphericalMercatorProjection comes from)
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Shader;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.Tile;
import com.google.android.gms.maps.model.TileProvider;
import com.google.maps.android.SphericalUtil;
import com.google.maps.android.geometry.Point;
import com.google.maps.android.projection.SphericalMercatorProjection;
import java.io.ByteArrayOutputStream;
import java.util.List;
/**
* Tile overlay used to display a colored polyline as a replacement for the non-existence of gradient
* polylines for google maps
*/
public class ColoredPolylineTileOverlay<T extends ColoredPolylineTileOverlay.PointHolder> implements TileProvider {
public static final double LOW_SPEED_CLAMP_KMpH = 0;
public static final double LOW_SPEED_CLAMP_MpS = 0;
// TODO: calculate speed as highest speed of pointsCollection
public static final double HIGH_SPEED_CLAMP_KMpH = 50;
public static final double HIGH_SPEED_CLAMP_MpS = HIGH_SPEED_CLAMP_KMpH * 1000 / (60 * 60);
public static final int BASE_TILE_SIZE = 256;
public static int[] getSpeedColors(Context context) {
return new int[] {
context.getResources().getColor(R.color.polyline_low_speed),
context.getResources().getColor(R.color.polyline_med_speed),
context.getResources().getColor(R.color.polyline_high_speed)
};
}
public static float getSpeedProportion(double metersPerSecond) {
return (float)(Math.max(Math.min(metersPerSecond, HIGH_SPEED_CLAMP_MpS), LOW_SPEED_CLAMP_MpS) / HIGH_SPEED_CLAMP_MpS);
}
public static int interpolateColor(int[] colors, float proportion) {
int rTotal = 0, gTotal = 0, bTotal = 0;
// We correct the ratio to colors.length - 1 so that
// for i == colors.length - 1 and p == 1, then the final ratio is 1 (see below)
float p = proportion * (colors.length - 1);
for (int i = 0; i < colors.length; i++) {
// The ratio mostly resides on the 1 - Math.abs(p - i) calculation :
// Since for p == i, then the ratio is 1 and for p == i + 1 or p == i -1, then the ratio is 0
// This calculation works BECAUSE p lies within [0, length - 1] and i lies within [0, length - 1] as well
float iRatio = Math.max(1 - Math.abs(p - i), 0.0f);
rTotal += (int)(Color.red(colors[i]) * iRatio);
gTotal += (int)(Color.green(colors[i]) * iRatio);
bTotal += (int)(Color.blue(colors[i]) * iRatio);
}
return Color.rgb(rTotal, gTotal, bTotal);
}
protected final Context context;
protected final PointCollection<T> pointsCollection;
protected final int[] speedColors;
protected final float density;
protected final int tileDimension;
protected final SphericalMercatorProjection projection;
// Caching calculation-related stuff
protected LatLng[] trailLatLngs;
protected Point[] projectedPts;
protected Point[] projectedPtMids;
protected double[] speeds;
public ColoredPolylineTileOverlay(Context context, PointCollection pointsCollection) {
super();
this.context = context;
this.pointsCollection = pointsCollection;
speedColors = getSpeedColors(context);
density = context.getResources().getDisplayMetrics().density;
tileDimension = (int)(BASE_TILE_SIZE * density);
projection = new SphericalMercatorProjection(BASE_TILE_SIZE);
calculatePointsAndSpeeds();
}
public void calculatePointsAndSpeeds() {
trailLatLngs = new LatLng[pointsCollection.getPoints().size()];
projectedPts = new Point[pointsCollection.getPoints().size()];
projectedPtMids = new Point[Math.max(pointsCollection.getPoints().size() - 1, 0)];
speeds = new double[Math.max(pointsCollection.getPoints().size() - 1, 0)];
List<T> points = pointsCollection.getPoints();
for (int i = 0; i < points.size(); i++) {
T point = points.get(i);
LatLng latLng = point.getLatLng();
trailLatLngs[i] = latLng;
projectedPts[i] = projection.toPoint(latLng);
// Mids
if (i > 0) {
LatLng previousLatLng = points.get(i - 1).getLatLng();
LatLng latLngMid = SphericalUtil.interpolate(previousLatLng, latLng, 0.5);
projectedPtMids[i - 1] = projection.toPoint(latLngMid);
T previousPoint = points.get(i - 1);
double speed = SphericalUtil.computeDistanceBetween(latLng, previousLatLng) / ((point.getTime() - previousPoint.getTime()) / 1000.0);
speeds[i - 1] = speed;
}
}
}
#Override
public Tile getTile(int x, int y, int zoom) {
// Because getTile can be called asynchronously by multiple threads, none of the info we keep in the class will be modified
// (getTile is essentially side-effect-less) :
// Instead, we create the bitmap, the canvas and the paints specifically for the call to getTile
Bitmap bitmap = Bitmap.createBitmap(tileDimension, tileDimension, Bitmap.Config.ARGB_8888);
// Normally, instead of the later calls for drawing being offset, we would offset them using scale() and translate() right here
// However, there seems to be funky issues related to float imprecisions that happen at large scales when using this method, so instead
// The points are offset properly when drawing
Canvas canvas = new Canvas(bitmap);
Matrix shaderMat = new Matrix();
Paint gradientPaint = new Paint();
gradientPaint.setStyle(Paint.Style.STROKE);
gradientPaint.setStrokeWidth(3f * density);
gradientPaint.setStrokeCap(Paint.Cap.BUTT);
gradientPaint.setStrokeJoin(Paint.Join.ROUND);
gradientPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
gradientPaint.setShader(new LinearGradient(0, 0, 1, 0, speedColors, null, Shader.TileMode.CLAMP));
gradientPaint.getShader().setLocalMatrix(shaderMat);
Paint colorPaint = new Paint();
colorPaint.setStyle(Paint.Style.STROKE);
colorPaint.setStrokeWidth(3f * density);
colorPaint.setStrokeCap(Paint.Cap.BUTT);
colorPaint.setStrokeJoin(Paint.Join.ROUND);
colorPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
// See https://developers.google.com/maps/documentation/android/views#zoom for handy info regarding what zoom is
float scale = (float)(Math.pow(2, zoom) * density);
renderTrail(canvas, shaderMat, gradientPaint, colorPaint, scale, x, y);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos);
return new Tile(tileDimension, tileDimension, baos.toByteArray());
}
public void renderTrail(Canvas canvas, Matrix shaderMat, Paint gradientPaint, Paint colorPaint, float scale, int x, int y) {
List<T> points = pointsCollection.getPoints();
double speed1, speed2;
MutPoint pt1 = new MutPoint(), pt2 = new MutPoint(), pt3 = new MutPoint(), pt1mid2 = new MutPoint(), pt2mid3 = new MutPoint();
// Guard statement: if the trail is only 1 point, just render the point by itself as a speed of 0
if (points.size() == 1) {
pt1.set(projectedPts[0], scale, x, y, tileDimension);
speed1 = 0;
float speedProp = getSpeedProportion(speed1);
colorPaint.setStyle(Paint.Style.FILL);
colorPaint.setColor(interpolateColor(speedColors, speedProp));
canvas.drawCircle((float) pt1.x, (float) pt1.y, colorPaint.getStrokeWidth() / 2f, colorPaint);
colorPaint.setStyle(Paint.Style.STROKE);
return;
}
// Guard statement: if the trail is exactly 2 points long, just render a line from A to B at d(A, B) / t speed
if (points.size() == 2) {
pt1.set(projectedPts[0], scale, x, y, tileDimension);
pt2.set(projectedPts[1], scale, x, y, tileDimension);
speed1 = speeds[0];
float speedProp = getSpeedProportion(speed1);
drawLine(canvas, colorPaint, pt1, pt2, speedProp);
return;
}
// Because we want to be displaying speeds as color ratios, we need multiple points to do it properly:
// Since we use calculate the speed using the distance and the time, we need at least 2 points to calculate the distance;
// this means we know the speed for a segment, not a point.
// Furthermore, since we want to be easing the color changes between every segment, we have to use 3 points to do the easing;
// every line is split into two, and we ease over the corners
// This also means the first and last corners need to be extended to include the first and last points respectively
// Finally (you can see about that in getTile()) we need to offset the point projections based on the scale and x, y because
// weird display behaviour occurs
for (int i = 2; i < points.size(); i++) {
pt1.set(projectedPts[i - 2], scale, x, y, tileDimension);
pt2.set(projectedPts[i - 1], scale, x, y, tileDimension);
pt3.set(projectedPts[i], scale, x, y, tileDimension);
// Because we want to split the lines in two to ease over the corners, we need the middle points
pt1mid2.set(projectedPtMids[i - 2], scale, x, y, tileDimension);
pt2mid3.set(projectedPtMids[i - 1], scale, x, y, tileDimension);
// The speed is calculated in meters per second (same format as the speed clamps); because getTime() is in millis, we need to correct for that
speed1 = speeds[i - 2];
speed2 = speeds[i - 1];
float speed1Prop = getSpeedProportion(speed1);
float speed1to2Prop = getSpeedProportion((speed1 + speed2) / 2);
float speed2Prop = getSpeedProportion(speed2);
// Circle for the corner (removes the weird empty corners that occur otherwise)
colorPaint.setStyle(Paint.Style.FILL);
colorPaint.setColor(interpolateColor(speedColors, speed1to2Prop));
canvas.drawCircle((float)pt2.x, (float)pt2.y, colorPaint.getStrokeWidth() / 2f, colorPaint);
colorPaint.setStyle(Paint.Style.STROKE);
// Corner
// Note that since for the very first point and the very last point we don't split it in two, we used them instead.
drawLine(canvas, shaderMat, gradientPaint, colorPaint, i - 2 == 0 ? pt1 : pt1mid2, pt2, speed1Prop, speed1to2Prop);
drawLine(canvas, shaderMat, gradientPaint, colorPaint, pt2, i == points.size() - 1 ? pt3 : pt2mid3, speed1to2Prop, speed2Prop);
}
}
/**
* Note: it is assumed the shader is 0, 0, 1, 0 (horizontal) so that it lines up with the rotation
* (rotations are usually setup so that the angle 0 points right)
*/
public void drawLine(Canvas canvas, Matrix shaderMat, Paint gradientPaint, Paint colorPaint, MutPoint pt1, MutPoint pt2, float ratio1, float ratio2) {
// Degenerate case: both ratios are the same; we just handle it using the colorPaint (handling it using the shader is just messy and ineffective)
if (ratio1 == ratio2) {
drawLine(canvas, colorPaint, pt1, pt2, ratio1);
return;
}
shaderMat.reset();
// PS: don't ask me why this specfic orders for calls works but other orders will mess up
// Since every call is pre, this is essentially ordered as (or my understanding is that it is):
// ratio translate -> ratio scale -> scale to pt length -> translate to pt start -> rotate
// (my initial intuition was to use only post calls and to order as above, but it resulted in odd corruptions)
// Setup based on points:
// We translate the shader so that it is based on the first point, rotated towards the second and since the length of the
// gradient is 1, then scaling to the length of the distance between the points makes it exactly as long as needed
shaderMat.preRotate((float) Math.toDegrees(Math.atan2(pt2.y - pt1.y, pt2.x - pt1.x)), (float)pt1.x, (float)pt1.y);
shaderMat.preTranslate((float)pt1.x, (float)pt1.y);
float scale = (float)Math.sqrt(Math.pow(pt2.x - pt1.x, 2) + Math.pow(pt2.y - pt1.y, 2));
shaderMat.preScale(scale, scale);
// Setup based on ratio
// By basing the shader to the first ratio, we ensure that the start of the gradient corresponds to it
// The inverse scaling of the shader means that it takes the full length of the call to go to the second ratio
// For instance; if d(ratio1, ratio2) is 0.5, then the shader needs to be twice as long so that an entire call (1)
// Results in only half of the gradient being used
shaderMat.preScale(1f / (ratio2 - ratio1), 1f / (ratio2 - ratio1));
shaderMat.preTranslate(-ratio1, 0);
gradientPaint.getShader().setLocalMatrix(shaderMat);
canvas.drawLine(
(float)pt1.x,
(float)pt1.y,
(float)pt2.x,
(float)pt2.y,
gradientPaint
);
}
public void drawLine(Canvas canvas, Paint colorPaint, MutPoint pt1, MutPoint pt2, float ratio) {
colorPaint.setColor(interpolateColor(speedColors, ratio));
canvas.drawLine(
(float)pt1.x,
(float)pt1.y,
(float)pt2.x,
(float)pt2.y,
colorPaint
);
}
public interface PointCollection<T extends PointHolder> {
List<T> getPoints();
}
public interface PointHolder {
LatLng getLatLng();
long getTime();
}
public static class MutPoint {
public double x, y;
public MutPoint set(Point point, float scale, int x, int y, int tileDimension) {
this.x = point.x * scale - x * tileDimension;
this.y = point.y * scale - y * tileDimension;
return this;
}
}
}
Note that this implementation assumes two relatively large things:
the polyline is already complete
that there is only one polyline.
I would assume handling (1) would not be very difficult. However, if you intend to draw multiple polylines this way, you may need to look at some ways to enhance performance (keeping a bounding box of the polylines to be able to easily discard those that do not fit the viewport for one).
One more thing to remember regarding using a TileOverlay is that it is rendered after movements are done, not during; so you may want to back up the overlay with an actual monochrome polyline underneath it to give it some continuity.
PS: this is the first time I try to answer a question, so if there's anything I should fix or do differently please tell me.
One simple solution: draw multiple polylines and individually set the color.