13

I would like to be able to manually draw complex shapes on a mapbox map using the android sdk. I have inherited the map view class and overridden the ondraw event but unfortunately whatever I draw gets over drawn by the map itself.

As an example I need to be able to draw polygons with diamond shaped borders among other complex shapes. This i can do no problem in GoogleMaps using a custom tile provider and overriding ondraw.

Here is the only code I have so far for mapbox:

    @Override
    public void onDraw(Canvas canvas) {        
        super.onDraw(canvas);

        Paint stroke = new Paint();
        stroke.setColor(Color.BLACK);
        stroke.setStyle(Paint.Style.STROKE);
        stroke.setStrokeWidth(5);
        stroke.setAntiAlias(true); 

        canvas.drawLine(0f,0f,1440f,2464f,stroke);
    }

enter image description here

Reafidy
  • 8,240
  • 5
  • 51
  • 83

2 Answers2

12

You can do what You want by 2 ways:

1) as You propose: "inherit the MapView class and overridden the onDraw() event". But MapView extends FrameLayout which is ViewGroup, so You should override dispatchDraw() instead of onDraw().

This approach requires custom view, which extends MapViewand implements:

  • drawing over the MapView;

  • customizing line styles ("diamonds instead of a simple line");

  • binding path to Lat/Lon coordinates of MapView.

For drawing over the MapView You should override dispatchDraw(), for example like this:

@Override
public void dispatchDraw(Canvas canvas) {
    super.dispatchDraw(canvas);
    canvas.save();
    drawDiamondsPath(canvas);
    canvas.restore();
}

For customizing line styles You can use setPathEffect() method of Paint class. For this You should create path for "diamond stamp" (in pixels), which will repeated every "advance" (in pixels too):

        mPathDiamondStamp = new Path();
        mPathDiamondStamp.moveTo(-DIAMOND_WIDTH / 2, 0);
        mPathDiamondStamp.lineTo(0, DIAMOND_HEIGHT / 2);
        mPathDiamondStamp.lineTo(DIAMOND_WIDTH / 2, 0);
        mPathDiamondStamp.lineTo(0, -DIAMOND_HEIGHT / 2);
        mPathDiamondStamp.close();

        mPathDiamondStamp.moveTo(-DIAMOND_WIDTH / 2 + DIAMOND_BORDER_WIDTH, 0);
        mPathDiamondStamp.lineTo(0, -DIAMOND_HEIGHT / 2 + DIAMOND_BORDER_WIDTH / 2);
        mPathDiamondStamp.lineTo(DIAMOND_WIDTH / 2 - DIAMOND_BORDER_WIDTH, 0);
        mPathDiamondStamp.lineTo(0, DIAMOND_HEIGHT / 2 - DIAMOND_BORDER_WIDTH / 2);
        mPathDiamondStamp.close();

        mPathDiamondStamp.setFillType(Path.FillType.EVEN_ODD);

        mDiamondPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mDiamondPaint.setColor(Color.BLUE);
        mDiamondPaint.setStrokeWidth(2);
        mDiamondPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        mDiamondPaint.setStyle(Paint.Style.STROKE);
        mDiamondPaint.setPathEffect(new PathDashPathEffect(mPathDiamondStamp, DIAMOND_ADVANCE, DIAMOND_PHASE, PathDashPathEffect.Style.ROTATE));

(in this case there are 2 Path - first one (clockwise) for outer border and second (counter-clockwise) for inner border for "diamond" transparent "hole").

For binding path on screen to Lat/Lon coordinates of MapView You should have MapboxMap object of MapView - for that getMapAsync() and onMapReady() should be overridden:

@Override
public void getMapAsync(OnMapReadyCallback callback) {
    mMapReadyCallback = callback;
    super.getMapAsync(this);
}

@Override
public void onMapReady(MapboxMap mapboxMap) {
    mMapboxMap = mapboxMap;
    if (mMapReadyCallback != null) {
        mMapReadyCallback.onMapReady(mapboxMap);
    }
}

Than You can use it in "lat/lon-to-screen" convertating:

        mBorderPath = new Path();
        LatLng firstBorderPoint = mBorderPoints.get(0);
        PointF firstScreenPoint = mMapboxMap.getProjection().toScreenLocation(firstBorderPoint);
        mBorderPath.moveTo(firstScreenPoint.x, firstScreenPoint.y);

        for (int ixPoint = 1; ixPoint < mBorderPoints.size(); ixPoint++) {
            PointF currentScreenPoint = mMapboxMap.getProjection().toScreenLocation(mBorderPoints.get(ixPoint));
            mBorderPath.lineTo(currentScreenPoint.x, currentScreenPoint.y);
        }

Full source code:

Custom DrawMapView.java

public class DrawMapView extends MapView implements OnMapReadyCallback{

    private float DIAMOND_WIDTH = 42;
    private float DIAMOND_HEIGHT = 18;
    private float DIAMOND_ADVANCE = 1.5f * DIAMOND_WIDTH;       // spacing between each stamp of shape
    private float DIAMOND_PHASE = DIAMOND_WIDTH / 2;            // amount to offset before the first shape is stamped
    private float DIAMOND_BORDER_WIDTH = 6;                     // width of diamond border

    private Path mBorderPath;
    private Path mPathDiamondStamp;
    private Paint mDiamondPaint;
    private OnMapReadyCallback mMapReadyCallback;
    private MapboxMap mMapboxMap = null;
    private List<LatLng> mBorderPoints;

    public DrawMapView(@NonNull Context context) {
        super(context);
        init();
    }

    public DrawMapView(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public DrawMapView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    public DrawMapView(@NonNull Context context, @Nullable MapboxMapOptions options) {
        super(context, options);
        init();
    }

    public void setBorderPoints(List<LatLng> borderPoints) {
        mBorderPoints = borderPoints;
    }

    @Override
    public void getMapAsync(OnMapReadyCallback callback) {
        mMapReadyCallback = callback;
        super.getMapAsync(this);
    }

    @Override
    public void onMapReady(MapboxMap mapboxMap) {
        mMapboxMap = mapboxMap;
        if (mMapReadyCallback != null) {
            mMapReadyCallback.onMapReady(mapboxMap);
        }
    }

    @Override
    public void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        canvas.save();
        drawDiamondsPath(canvas);
        canvas.restore();
    }

    private void drawDiamondsPath(Canvas canvas) {
        if (mBorderPoints == null || mBorderPoints.size() == 0) {
            return;
        }

        mBorderPath = new Path();

        LatLng firstBorderPoint = mBorderPoints.get(0);
        PointF firstScreenPoint = mMapboxMap.getProjection().toScreenLocation(firstBorderPoint);
        mBorderPath.moveTo(firstScreenPoint.x, firstScreenPoint.y);

        for (int ixPoint = 1; ixPoint < mBorderPoints.size(); ixPoint++) {
            PointF currentScreenPoint = mMapboxMap.getProjection().toScreenLocation(mBorderPoints.get(ixPoint));
            mBorderPath.lineTo(currentScreenPoint.x, currentScreenPoint.y);
        }

        mPathDiamondStamp = new Path();
        mPathDiamondStamp.moveTo(-DIAMOND_WIDTH / 2, 0);
        mPathDiamondStamp.lineTo(0, DIAMOND_HEIGHT / 2);
        mPathDiamondStamp.lineTo(DIAMOND_WIDTH / 2, 0);
        mPathDiamondStamp.lineTo(0, -DIAMOND_HEIGHT / 2);
        mPathDiamondStamp.close();

        mPathDiamondStamp.moveTo(-DIAMOND_WIDTH / 2 + DIAMOND_BORDER_WIDTH, 0);
        mPathDiamondStamp.lineTo(0, -DIAMOND_HEIGHT / 2 + DIAMOND_BORDER_WIDTH / 2);
        mPathDiamondStamp.lineTo(DIAMOND_WIDTH / 2 - DIAMOND_BORDER_WIDTH, 0);
        mPathDiamondStamp.lineTo(0, DIAMOND_HEIGHT / 2 - DIAMOND_BORDER_WIDTH / 2);
        mPathDiamondStamp.close();

        mPathDiamondStamp.setFillType(Path.FillType.EVEN_ODD);

        mDiamondPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mDiamondPaint.setColor(Color.BLUE);
        mDiamondPaint.setStrokeWidth(2);
        mDiamondPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        mDiamondPaint.setStyle(Paint.Style.STROKE);
        mDiamondPaint.setPathEffect(new PathDashPathEffect(mPathDiamondStamp, DIAMOND_ADVANCE, DIAMOND_PHASE, PathDashPathEffect.Style.ROTATE));

        canvas.drawPath(mBorderPath, mDiamondPaint);
    }

    private void init() {
        mBorderPath = new Path();
        mPathDiamondStamp = new Path();
    }
}

ActivityMain.java

public class MainActivity extends AppCompatActivity {

    private DrawMapView mapView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        MapboxAccountManager.start(this, getString(R.string.access_token));
        setContentView(R.layout.activity_main);

        mapView = (DrawMapView) findViewById(R.id.mapView);
        mapView.onCreate(savedInstanceState);
        mapView.getMapAsync(new OnMapReadyCallback() {
            @Override
            public void onMapReady(MapboxMap mapboxMap) {

                mapView.setBorderPoints(Arrays.asList(new LatLng(-36.930129, 174.958843),
                        new LatLng(-36.877860, 174.978108),
                        new LatLng(-36.846373, 174.901841),
                        new LatLng(-36.829215, 174.814659),
                        new LatLng(-36.791326, 174.779337),
                        new LatLng(-36.767680, 174.823242)));
            }
        });
    }

    @Override
    public void onResume() {
        super.onResume();
        mapView.onResume();
    }

    @Override
    public void onPause() {
        super.onPause();
        mapView.onPause();
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        mapView.onSaveInstanceState(outState);
    }

    @Override
    public void onLowMemory() {
        super.onLowMemory();
        mapView.onLowMemory();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mapView.onDestroy();
    }

}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:mapbox="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="ua.com.omelchenko.mapboxlines.MainActivity">

    <ua.com.omelchenko.mapboxlines.DrawMapView
        android:id="@+id/mapView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        mapbox:center_latitude="-36.841362"
        mapbox:center_longitude="174.851110"
        mapbox:style_url="@string/style_mapbox_streets"
        mapbox:zoom="10"/>
</RelativeLayout>

Finally, as a result, you should get something like this:

enter image description here

And You should take into account some "special cases", for example if all points of path is outside current view of map, there is no lines on it, even line should cross view of map and should be visible.

2) (better way) create and publish map with your additional lines and custom style for them (especially take a look at "Line patterns with Images" sections). You can use Mapbox Studio for this. And in this approach all "special cases" and performance issues is solved on Mabpox side.

Andrii Omelchenko
  • 13,183
  • 12
  • 43
  • 79
  • Gidday Andriy, shortly after posting the question I had last minute unexpected business travel so I apologize for not getting back to you sooner. Your answer is just what I needed. Thank you so much for the help. – Reafidy Nov 28 '16 at 00:58
  • Thank You,@Kevin :) Hope answer helps You. – Andrii Omelchenko Dec 18 '16 at 01:58
  • 1
    Does this also work when panning and zooming the map? Here, onDraw() and dispatchDraw() are called only once – j3App Oct 16 '18 at 06:54
  • @j3App It works: polylines will be in correct positions, the size of line pattern has not changed. – Andrii Omelchenko Oct 16 '18 at 11:31
6

If I understand correctly, you are trying to add a diamond shape to the map (user isn't drawing the shape)? If this is the case, you have a few options:

  1. Use Polygon, simply add the list of points and it will draw the shape (in this case, a diamond). This would be the easiest, but I assume you already tried and it doesn't work for you.

    List<LatLng> polygon = new ArrayList<>();
    polygon.add(<LatLng Point 1>);
    polygon.add(<LatLng Point 2>);
    
    ...
    
    mapboxMap.addPolygon(new PolygonOptions()
      .addAll(polygon)
      .fillColor(Color.parseColor("#3bb2d0")));
    
  2. Add a Fill layer using the new Style API introduced in 4.2.0 (still in beta). Doing this will first require you to create a GeoJSON object with points and then to add it to the map. The closest example I have to doing this would be this example, found in the demo app.

  3. Use onDraw which would essential just translate the canvas to a GeoJSON object and add as a layer like explained in step 2. I'd only recommend this if you are having the user draw shapes during runtime, in this case the coordinates would be uncertain.

I'll edit this answer if you are looking for something different.

cammace
  • 3,138
  • 1
  • 13
  • 18
  • thanks for the reply. Im not trying to draw a diamond polygon. Im trying to draw a polygon which has a border made of small diamonds instead of a simple line. See the image to clarify my question. Option 3 is sort of what I am trying to do but have been unable to achieve as anything i draw on the canvas is overriden once the map renders async from the ondraw event. I also dont think converting the canvas to geojson will work as the geojson cant define the complex polygon diamonds. – Reafidy Sep 19 '16 at 20:42
  • did you have any further suggestions? I would love to switch to mapbox as the custom maps are way better, but as I cant draw the shapes I need Im stuck with googlemaps until i find a solution. Really appreciate the help. – Reafidy Sep 21 '16 at 18:47
  • do you know if this possible or not? Would like to move on if its not possible. Thanks again for your help. – Reafidy Oct 15 '16 at 03:04
  • "do you know if this possible or not?" - it is possible via several ways. – Andrii Omelchenko Nov 22 '16 at 20:55