Google Maps API v2 draw part of circle on MapFragment
Asked Answered
W

2

6

I need to draw something like this which will be painted and have little transparency

Also it needs to be clickable (onTouch event etc)

enter image description here

I know that in API v1 you have to use Overlay and extend it using canvas and some mathematics. What is easiest way to do it in Google Map API v2?

PS: Radius is variable.

(For further reference) EDIT 1:

I implemented CanvasTileProvider subclass and override its onDraw() method:

@Override
void onDraw(Canvas canvas, TileProjection projection) {
    // TODO Auto-generated method stub

    LatLng tempLocation = moveByDistance(mSegmentLocation, mSegmentRadius, mSegmentAngle);

    DoublePoint segmentLocationPoint = new DoublePoint(0, 0);
    DoublePoint tempLocationPoint = new DoublePoint(0, 0);

    projection.latLngToPoint(mSegmentLocation, segmentLocationPoint);
    projection.latLngToPoint(tempLocationPoint, tempLocationPoint);

    float radiusInPoints = FloatMath.sqrt((float) (Math.pow(
            (segmentLocationPoint.x - tempLocationPoint.x), 2) + Math.pow(
            (segmentLocationPoint.y - tempLocationPoint.y), 2)));

    RectF segmentArea = new RectF();
    segmentArea.set((float)segmentLocationPoint.x - radiusInPoints, (float)segmentLocationPoint.y - radiusInPoints, 
            (float)segmentLocationPoint.x + radiusInPoints, (float)segmentLocationPoint.y + radiusInPoints);

    canvas.drawArc(segmentArea, getAdjustedAngle(mSegmentAngle), 
            getAdjustedAngle(mSegmentAngle + 60), true, getOuterCirclePaint());


}

Also, I added this from MapActivity:

private void loadSegmentTiles() {

     TileProvider tileProvider; 
     TileOverlay tileOverlay = mMap.addTileOverlay(
         new TileOverlayOptions().tileProvider(new SegmentTileProvider(new LatLng(45.00000,15.000000), 250, 30)));

}

Now I'm wondering why my arc isn't on map?

Waterresistant answered 4/12, 2013 at 18:15 Comment(0)
E
20

For drawing the circle segments, I would register a TileProvider, if the segments are mainly static. (Tiles are typically loaded only once and then cached.) For checking for click events, you can register an onMapClickListener and loop over your segments to check whether the clicked LatLng is inside one of your segments. (see below for more details.)

Here is a TileProvider example, which you could subclass and just implement the onDraw method.
One important note: The subclass must be thread safe! The onDraw method will be called by multiple threads simultaneously. So avoid any globals which are changed inside onDraw!

/* imports should be obvious */ 
public abstract class CanvasTileProvider implements TileProvider {
private static int TILE_SIZE = 256;

private BitMapThreadLocal tlBitmap;

@SuppressWarnings("unused")
private static final String TAG = CanvasTileProvider.class.getSimpleName();

public CanvasTileProvider() {
    super();
    tlBitmap = new BitMapThreadLocal();
}

@Override
// Warning: Must be threadsafe. To still avoid creation of lot of bitmaps,
// I use a subclass of ThreadLocal !!!
public Tile getTile(int x, int y, int zoom) {
    TileProjection projection = new TileProjection(TILE_SIZE,
            x, y, zoom);

    byte[] data;
    Bitmap image = getNewBitmap();
    Canvas canvas = new Canvas(image);
    onDraw(canvas, projection);
    data = bitmapToByteArray(image);
    Tile tile = new Tile(TILE_SIZE, TILE_SIZE, data);
    return tile;
}

/** Must be implemented by a concrete TileProvider */
abstract void onDraw(Canvas canvas, TileProjection projection);

/**
 * Get an empty bitmap, which may however be reused from a previous call in
 * the same thread.
 * 
 * @return
 */
private Bitmap getNewBitmap() {
    Bitmap bitmap = tlBitmap.get();
    // Clear the previous bitmap
    bitmap.eraseColor(Color.TRANSPARENT);
    return bitmap;
}

private static byte[] bitmapToByteArray(Bitmap bm) {
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    bm.compress(Bitmap.CompressFormat.PNG, 100, bos);
    byte[] data = bos.toByteArray();
    return data;
}

class BitMapThreadLocal extends ThreadLocal<Bitmap> {
    @Override
    protected Bitmap initialValue() {
        Bitmap image = Bitmap.createBitmap(TILE_SIZE, TILE_SIZE,
                Config.ARGB_8888);
        return image;
    }
}
}

Use the projection, which is passed into the onDraw method, to get at first the bounds of the tile. If no segment is inside the bounds, just return. Otherwise draw your seqment into the canvas. The method projection.latLngToPoint helps you to convert from LatLng to the pixels of the canvas.

/** Converts between LatLng coordinates and the pixels inside a tile. */
public class TileProjection {

private int x;
private int y;
private int zoom;
private int TILE_SIZE;

private DoublePoint pixelOrigin_;
private double pixelsPerLonDegree_;
private double pixelsPerLonRadian_;

TileProjection(int tileSize, int x, int y, int zoom) {
    this.TILE_SIZE = tileSize;
    this.x = x;
    this.y = y;
    this.zoom = zoom;
    pixelOrigin_ = new DoublePoint(TILE_SIZE / 2, TILE_SIZE / 2);
    pixelsPerLonDegree_ = TILE_SIZE / 360d;
    pixelsPerLonRadian_ = TILE_SIZE / (2 * Math.PI);
}

/** Get the dimensions of the Tile in LatLng coordinates */
public LatLngBounds getTileBounds() {
    DoublePoint tileSW = new DoublePoint(x * TILE_SIZE, (y + 1) * TILE_SIZE);
    DoublePoint worldSW = pixelToWorldCoordinates(tileSW);
    LatLng SW = worldCoordToLatLng(worldSW);
    DoublePoint tileNE = new DoublePoint((x + 1) * TILE_SIZE, y * TILE_SIZE);
    DoublePoint worldNE = pixelToWorldCoordinates(tileNE);
    LatLng NE = worldCoordToLatLng(worldNE);
    return new LatLngBounds(SW, NE);
}

/**
 * Calculate the pixel coordinates inside a tile, relative to the left upper
 * corner (origin) of the tile.
 */
public void latLngToPoint(LatLng latLng, DoublePoint result) {
    latLngToWorldCoordinates(latLng, result);
    worldToPixelCoordinates(result, result);
    result.x -= x * TILE_SIZE;
    result.y -= y * TILE_SIZE;
}


private DoublePoint pixelToWorldCoordinates(DoublePoint pixelCoord) {
    int numTiles = 1 << zoom;
    DoublePoint worldCoordinate = new DoublePoint(pixelCoord.x / numTiles,
            pixelCoord.y / numTiles);
    return worldCoordinate;
}

/**
 * Transform the world coordinates into pixel-coordinates relative to the
 * whole tile-area. (i.e. the coordinate system that spans all tiles.)
 * 
 * 
 * Takes the resulting point as parameter, to avoid creation of new objects.
 */
private void worldToPixelCoordinates(DoublePoint worldCoord, DoublePoint result) {
    int numTiles = 1 << zoom;
    result.x = worldCoord.x * numTiles;
    result.y = worldCoord.y * numTiles;
}

private LatLng worldCoordToLatLng(DoublePoint worldCoordinate) {
    DoublePoint origin = pixelOrigin_;
    double lng = (worldCoordinate.x - origin.x) / pixelsPerLonDegree_;
    double latRadians = (worldCoordinate.y - origin.y)
            / -pixelsPerLonRadian_;
    double lat = Math.toDegrees(2 * Math.atan(Math.exp(latRadians))
            - Math.PI / 2);
    return new LatLng(lat, lng);
}

/**
 * Get the coordinates in a system describing the whole globe in a
 * coordinate range from 0 to TILE_SIZE (type double).
 * 
 * Takes the resulting point as parameter, to avoid creation of new objects.
 */
private void latLngToWorldCoordinates(LatLng latLng, DoublePoint result) {
    DoublePoint origin = pixelOrigin_;

    result.x = origin.x + latLng.longitude * pixelsPerLonDegree_;

    // Truncating to 0.9999 effectively limits latitude to 89.189. This is
    // about a third of a tile past the edge of the world tile.
    double siny = bound(Math.sin(Math.toRadians(latLng.latitude)), -0.9999,
            0.9999);
    result.y = origin.y + 0.5 * Math.log((1 + siny) / (1 - siny))
            * -pixelsPerLonRadian_;
};

/** Return value reduced to min and max if outside one of these bounds. */
private double bound(double value, double min, double max) {
    value = Math.max(value, min);
    value = Math.min(value, max);
    return value;
}

/** A Point in an x/y coordinate system with coordinates of type double */
public static class DoublePoint {
    double x;
    double y;

    public DoublePoint(double x, double y) {
        this.x = x;
        this.y = y;
    }
}

}

Finally you need something to check, whether a click on a LatLng-Coordinate is inside of your segment. I would therefore approximate the segment by a list of LatLng-Coordinates, where in your case a simple triangle may be sufficient. For each list of LatLng coordinates, i.e. for each segment, you may then call something like the following:

private static boolean isPointInsidePolygon(List<LatLng> vertices, LatLng point) {
    /**
     * Test is based on a horizontal ray, starting from point to the right.
     * If the ray is crossed by an even number of polygon-sides, the point
     * is inside the polygon, otherwise it is outside.
     */
    int i, j;
    boolean inside = false;
    int size = vertices.size();
    for (i = 0, j = size - 1; i < size; j = i++) {
        LatLng vi = vertices.get(i);
        LatLng vj = vertices.get(j);
        if ((vi.latitude > point.latitude) != (vj.latitude > point.latitude)) {
            /* The polygonside crosses the horizontal level of the ray. */
            if (point.longitude <= vi.longitude
                    && point.longitude <= vj.longitude) {
                /*
                 * Start and end of the side is right to the point. Side
                 * crosses the ray.
                 */
                inside = !inside;
            } else if (point.longitude >= vi.longitude
                    && point.longitude >= vj.longitude) {
                /*
                 * Start and end of the side is left of the point. No
                 * crossing of the ray.
                 */
            } else {
                double crossingLongitude = (vj.longitude - vi.longitude)
                        * (point.latitude - vi.latitude)
                        / (vj.latitude - vi.latitude) + vi.longitude;
                if (point.longitude < crossingLongitude) {
                    inside = !inside;
                }
            }
        }
    }
    return inside;
}

As you may see, I had a very similar task to solve :-)

Element answered 5/12, 2013 at 19:6 Comment(25)
Wow man... Thanks!! I'll immediatly implement code and let you know.Waterresistant
One question as Im very new to map api and Tiles. Where to implement draw method, and how to provide LatLng and radius?Waterresistant
Create a subclass which extends my CanvasTileProvider. There you have to implement onDraw (as this is abstract in CanvasTileProvider).Element
Not like this? TileOverlayOptions opts = new TileOverlayOptions(); opts.tileProvider(new CanvasTileProvider() { @Override void onDraw(Canvas canvas, TileProjection projection) { // TODO Auto-generated method stub } }); opts.zIndex(5); TileOverlay overlay = mMap.addTileOverlay(opts);Waterresistant
I don't think you should define that as anonymous class. The onDraw will do the whole work of drawing. And i guess that's a bit too much for inline definition. Create a real subclass instead. To get a new LatLng within the given distance see my answer to #20407810. Having two points you can use projection.latLngToPoint to get the points on the canvas and you can just use rule of pythogoras to get the radius in pixels.Element
understand. But that what I'm trying to avoid. I have just one LatLng (Point) and Radius. I do not need to calculate radius. Sorry for confusion. I have following: LatLng, Radius, Start Angle, End Angle.Waterresistant
Yes, you have the radius in meter, but not the radius in pixels. So you need to convert from meter to pixels. This is what I meant: Create a LatLng in the given distance and convert it also into the pixel coordinate system. The distance between the two points in the pixel coordinate system is your "pixel"-radius.Element
I have done like you said.. trying to debug code... maybe I'm missing something?Waterresistant
What exactly happens? Can you see anything you are drawing on the canvas?Element
Unfortunately no. I can see that 6 threads are executed, sometimes more... This is probably for each Tile. I think that math for calculating spots are wrong... now I'm trying to figure canvas, RectF, radius nad points relations , I'm using GroundOverlay, it should be similar with Tiles also.Waterresistant
I did not see your edited code first. But there must be something missing in it. You must somehow get a LatLng in the distance of your radius, and use that as input to latLngToPoint in order to get the tmpLocationPoint. (In your coding both parameters are tmpLocationPoint which will not compile). I would start drawing a simple line from 0,0 to width,height, just to see, that drawing works. Then start drawing the more complicated things.Element
OK. Will figure what. Just one question, what if my arc is partly in one Tile and partly in another?Waterresistant
Just draw it in both tiles. The overlapping part will be clipped. (I think using GroundOverlay will zoom in and out your image including the line width, which looks nasty when zoomed too much. Whereas when using tiles, there will be a new tile requested when the next zoom level is reached.)Element
Hi. I did it like you wrote up.. Having for now two problems. First one is that at certain zoom level some Tiles are not returned (I look in getTile(), there are a lot of exceptions with returning null). I LogCat i can see threadid=29: still suspended after undo. Second one is onClick. You wrote code for checking is LatLng in vertices... how I can link that with on Map listeners ... Also I get too much work on main thread. Yous aid that you use ThreadLocal for creating Tiles. Is code above implements it? Thanks...Waterresistant
See invitation to chat: chat.stackoverflow.com/rooms/43038/…Element
Hi there... Long time no see... Did you every analyze heap usage with bitmap and CavanTileProvider? I know that thread will prevent ANR, but i get sometimes more than 1000 bitmaps on my map on one zoom level, and my heap keep grows above 40MB sometimes. Any comments?Waterresistant
Thank you for the very helpful example TileProvider. I avoided a few pitfalls I'd have hit if I rolled my own from the documentation, for sure. In my case I had some complex extra math to do where I had a line segment that passed across a tile, but was not contained entirely within it. I don't see any easy way to "clip" to the desired tile, so I had lots of calculation to find the portion of my path that was within the boundaries. Anyone know any tricks for this?Bumper
@RobP: I don't think you need to care for clipping. Of course you should avoid unnecessary calculation if the whole path is outside the tile. But besides that, I think you can just draw into the canvas without caring for the boundaries. The path will be clipped to the tile boundaries automatically.Element
One IMPORTANT note to add is that "public Tile getTile(int x, int y, int zoom)" has try-catch from the super class and will NOT show you any logs if anything crashes within it! Makes debuging really funCircumfluent
Hi, I am gratefully using your code for tile projection. I was wondering how you would convert a distance to pixels? For example I need to draw a circle using center point latlng x with radius 25nm. So I need to convert the 25nm to pixels to use polyPath.addCircle(pointStart.x, pointStart.y, ??, Path.Direction.CW); Trapper
@Reafidy: At first you should be aware of the fact, that a circle on the surface of the earth is not exactly a circle on the map. But if you ignore this inaccuracy, you just need to create a LatLng point in 25nm distance, and then use latLngToPoint method to get the pixels. Comparing them with the pixels of the center, gives you the radius. For creating a LatLng in a given distance see the answer to this SO question: #20572451 (method move)Element
Just found your question here #37626940 and added the same answer. (Previous comment could not be changed any longer)Element
Is there a way to manual force the tile to redraw? For example I draw a list of points on the map via the ondraw event. But if the users adds a new point once they have all been drawn I have no way of forcing the map to regenerate the tile and draw the new point. The only time I can draw is when the map calls for a new tile but I cant call GetTile because I don't know what x and y are.Trapper
If I understand what you are doing correctly, the Polyline will not have constant width as you smooth-zoom from one zoom level to next, right? This will look pretty bad ...Fixative
Now, the width is indeed increasing until the next zoom level is reached. But whether this "looks pretty bad" depends. The streets in the map are acting exactly the same.Element
A
1

Create a View, override its onDraw method to use drawArc on its canvas, and add it to your MapFragment. You can specify the radius in drawArc. Set the onClickListener on the View (or onTouch, any listener you can use for normal views, really).

Approval answered 4/12, 2013 at 18:29 Comment(4)
this will work however when the map moves the arc will no move with itLizabethlizard
You can always track the map movement and translate the view as necessary.Approval
Great... will try your suggestion ... It shouldn't move. It's static element on map. Only it should be clickable ... Thanks for answer, will try and let you know.Waterresistant
Do you have any code examples? How to architect code for this? Problem is that I'll have more than 100 arc objects on map. I'll use web services to provide LatLng and radius for each "Arc" object.Waterresistant

© 2022 - 2024 — McMap. All rights reserved.