package org.osmdroid.views.overlay; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Point; import android.graphics.Rect; import android.util.Log; import android.view.MotionEvent; import org.osmdroid.api.IMapView; import org.osmdroid.util.BoundingBox; import org.osmdroid.util.GeoPoint; import org.osmdroid.util.GeometryMath; import org.osmdroid.views.MapView; import org.osmdroid.views.Projection; import org.osmdroid.views.overlay.OverlayWithIW; import org.osmdroid.views.util.constants.MathConstants; import java.util.ArrayList; import java.util.List; import microsoft.mappoint.TileSystem; /** * A polyline is a list of points, where line segments are drawn between consecutive points. * Mimics the Polyline class from Google Maps Android API v2 as much as possible. Main differences:<br> * - Doesn't support Z-Index: drawing order is the order in map overlays<br> * - Supports InfoWindow (must be a BasicInfoWindow). <br> * <p></p> * Mimics the Polyline class from Google Maps Android API v2 as much as possible. Main differences:<br/> * - Doesn't support Z-Index: drawing order is the order in map overlays<br/> * - Supports InfoWindow (must be a BasicInfoWindow). <br/> * * <img alt="Class diagram around Marker class" width="686" height="413" src='src='./doc-files/marker-infowindow-classes.png' /> * * @author M.Kergall * @see <a href="http://developer.android.com/reference/com/google/android/gms/maps/model/Polyline.html">Google Maps Polyline</a> */ public class Polyline extends OverlayWithIW { /** original GeoPoints */ private double mOriginalPoints[][]; //as an array, to reduce object creation protected boolean mGeodesic; //private final Path mPath = new Path(); protected Paint mPaint = new Paint(); /** points, converted to the map projection */ private ArrayList<Point> mPoints; /** Number of points that have precomputed values */ private int mPointsPrecomputed; public boolean mRepeatPath = false; /** if true: at low zoom level showing multiple maps, path will be drawn on all maps */ /** Point coordinates for drawLines() */ private float[] mPts = null; /** Bounding rectangle for view */ private final Rect mClipRect = new Rect(); /** bounding rectangle for the current line segment */ private final Rect mLineBounds = new Rect(); private final Point mTempPoint1 = new Point(); private final Point mTempPoint2 = new Point(); protected OnClickListener mOnClickListener; /** Use {@link #Polyline()} instead */ @Deprecated public Polyline(Context ctx) { this(); } public Polyline(){ super(); //default as defined in Google API: this.mPaint.setColor(Color.BLACK); this.mPaint.setStrokeWidth(10.0f); this.mPaint.setStyle(Paint.Style.STROKE); mPaint.setAntiAlias(true); this.clearPath(); mOriginalPoints = new double[0][2]; mGeodesic = false; } protected void clearPath() { this.mPoints = new ArrayList<Point>(); this.mPointsPrecomputed = 0; this.mPts = null; } protected void addPoint(final GeoPoint aPoint) { addPoint(aPoint.getLatitudeE6(), aPoint.getLongitudeE6()); } protected void addPoint(final int aLatitudeE6, final int aLongitudeE6) { mPoints.add(new Point(aLatitudeE6, aLongitudeE6)); } /** @return a copy of the points. */ public List<GeoPoint> getPoints(){ List<GeoPoint> result = new ArrayList<GeoPoint>(mOriginalPoints.length); for (int i=0; i<mOriginalPoints.length; i++){ GeoPoint gp = new GeoPoint(mOriginalPoints[i][0], mOriginalPoints[i][1]); result.add(gp); } return result; } public int getNumberOfPoints(){ return mOriginalPoints.length; } public int getColor(){ return mPaint.getColor(); } public float getWidth(){ return mPaint.getStrokeWidth(); } /** @return the Paint used. This allows to set advanced Paint settings. */ public Paint getPaint(){ return mPaint; } public boolean isVisible(){ return isEnabled(); } public boolean isGeodesic(){ return mGeodesic; } public void setColor(int color){ mPaint.setColor(color); } public void setWidth(float width){ mPaint.setStrokeWidth(width); } public void setVisible(boolean visible){ setEnabled(visible); } public void setOnClickListener(OnClickListener listener){ mOnClickListener = listener; } protected void addGreatCircle(final GeoPoint startPoint, final GeoPoint endPoint, final int numberOfPoints) { // adapted from page http://compastic.blogspot.co.uk/2011/07/how-to-draw-great-circle-on-map-in.html // which was adapted from page http://maps.forum.nu/gm_flight_path.html // convert to radians final double lat1 = startPoint.getLatitude() * MathConstants.DEG2RAD; final double lon1 = startPoint.getLongitude() * MathConstants.DEG2RAD; final double lat2 = endPoint.getLatitude() * MathConstants.DEG2RAD; final double lon2 = endPoint.getLongitude() * MathConstants.DEG2RAD; final double d = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin((lat1 - lat2) / 2), 2) + Math.cos(lat1) * Math.cos(lat2) * Math.pow(Math.sin((lon1 - lon2) / 2), 2))); double bearing = Math.atan2(Math.sin(lon1 - lon2) * Math.cos(lat2), Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(lon1 - lon2)) / -MathConstants.DEG2RAD; bearing = bearing < 0 ? 360 + bearing : bearing; for (int i = 1; i <= numberOfPoints; i++) { final double f = 1.0 * i / (numberOfPoints+1); final double A = Math.sin((1 - f) * d) / Math.sin(d); final double B = Math.sin(f * d) / Math.sin(d); final double x = A * Math.cos(lat1) * Math.cos(lon1) + B * Math.cos(lat2) * Math.cos(lon2); final double y = A * Math.cos(lat1) * Math.sin(lon1) + B * Math.cos(lat2) * Math.sin(lon2); final double z = A * Math.sin(lat1) + B * Math.sin(lat2); final double latN = Math.atan2(z, Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2))); final double lonN = Math.atan2(y, x); addPoint((int) (latN * MathConstants.RAD2DEG * 1E6), (int) (lonN * MathConstants.RAD2DEG * 1E6)); } } /** Set the points. * Note that a later change in the original points List will have no effect. * To add/remove/change points, you must call setPoints again. * If geodesic mode has been set, the long segments will follow the earth "great circle". */ public void setPoints(List<GeoPoint> points){ clearPath(); int size = points.size(); mOriginalPoints = new double[size][2]; for (int i=0; i<size; i++){ GeoPoint p = points.get(i); mOriginalPoints[i][0] = p.getLatitude(); mOriginalPoints[i][1] = p.getLongitude(); if (!mGeodesic){ addPoint(p); } else { if (i>0){ //add potential intermediate points: GeoPoint prev = points.get(i-1); final int greatCircleLength = prev.distanceTo(p); //add one point for every 100kms of the great circle path final int numberOfPoints = greatCircleLength/100000; addGreatCircle(prev, p, numberOfPoints); } addPoint(p); } } } /** Sets whether to draw each segment of the line as a geodesic or not. * Warning: it takes effect only if set before setting the points in the Polyline. */ public void setGeodesic(boolean geodesic){ mGeodesic = geodesic; } protected void precomputePoints(Projection pj){ final int size = this.mPoints.size(); while (this.mPointsPrecomputed < size) { final Point pt = this.mPoints.get(this.mPointsPrecomputed); pj.toProjectedPixels(pt.x, pt.y, pt); this.mPointsPrecomputed++; } } /* protected void drawOld(final Canvas canvas, final MapView mapView, final boolean shadow) { if (shadow) { return; } final int size = this.mPoints.size(); if (size < 2) { // nothing to paint return; } final Projection pj = mapView.getProjection(); // precompute new points to the intermediate projection. precomputePoints(pj); Point screenPoint0 = null; // points on screen Point screenPoint1; Point projectedPoint0; // points from the points list Point projectedPoint1; // clipping rectangle in the intermediate projection, to avoid performing projection. BoundingBox boundingBox = pj.getBoundingBox(); Point topLeft = pj.toProjectedPixels(boundingBox.getLatNorth(), boundingBox.getLonWest(), null); Point bottomRight = pj.toProjectedPixels(boundingBox.getLatSouth(), boundingBox.getLonEast(), null); final Rect clipBounds = new Rect(topLeft.x, topLeft.y, bottomRight.x, bottomRight.y); // take into account map orientation: if (mapView.getMapOrientation() != 0.0f) GeometryMath.getBoundingBoxForRotatatedRectangle(clipBounds, mapView.getMapOrientation(), clipBounds); mPath.rewind(); projectedPoint0 = this.mPoints.get(size - 1); mLineBounds.set(projectedPoint0.x, projectedPoint0.y, projectedPoint0.x, projectedPoint0.y); for (int i = size - 2; i >= 0; i--) { // compute next points projectedPoint1 = this.mPoints.get(i); mLineBounds.union(projectedPoint1.x, projectedPoint1.y); if (!Rect.intersects(clipBounds, mLineBounds)) { // skip this line, move to next point projectedPoint0 = projectedPoint1; screenPoint0 = null; continue; } // the starting point may be not calculated, because previous segment was out of clip // bounds if (screenPoint0 == null) { screenPoint0 = pj.toPixelsFromProjected(projectedPoint0, this.mTempPoint1); mPath.moveTo(screenPoint0.x, screenPoint0.y); } screenPoint1 = pj.toPixelsFromProjected(projectedPoint1, this.mTempPoint2); // skip this point, too close to previous point if (Math.abs(screenPoint1.x - screenPoint0.x) + Math.abs(screenPoint1.y - screenPoint0.y) <= 1) { continue; } mPath.lineTo(screenPoint1.x, screenPoint1.y); // update starting point to next position projectedPoint0 = projectedPoint1; screenPoint0.x = screenPoint1.x; screenPoint0.y = screenPoint1.y; mLineBounds.set(projectedPoint0.x, projectedPoint0.y, projectedPoint0.x, projectedPoint0.y); } canvas.drawPath(mPath, mPaint); } */ @Override public void draw(final Canvas canvas, final MapView mapView, final boolean shadow) { if (shadow) { return; } final int size = mPoints.size(); if (size < 2) { // nothing to paint return; } final Projection pj = mapView.getProjection(); final int halfMapSize = TileSystem.MapSize(mapView.getProjection().getZoomLevel()) / 2; // 180° in longitude in pixels final int southLimit = pj.toPixelsFromMercator(0, halfMapSize * 2, null).y; // southern Limit of the map in Pixels // precompute new points to the intermediate projection. precomputePoints(pj); // not all points will be drawn due to zoom level and clipping (allow for 25-50% before calling drawLines) if (mPts == null || mPts.length < size) mPts = new float[Math.max(256, 2*size)]; int j=0; Point projectedPoint0 = mPoints.get(0); // points from the points list Point screenPoint0 = pj.toPixelsFromProjected(projectedPoint0, mTempPoint1); // points on screen Point screenPoint1; mapView.getScreenRect(mClipRect); for (int i = 1; i < size; i++) { // compute next points Point projectedPoint1 = mPoints.get(i); screenPoint1 = pj.toPixelsFromProjected(projectedPoint1, this.mTempPoint2); // skip points too close to previous point or on same side of view if (Math.abs(screenPoint1.x - screenPoint0.x) + Math.abs(screenPoint1.y - screenPoint0.y) <= 1) continue; if ( (screenPoint0.x < mClipRect.left && screenPoint1.x < mClipRect.left) || (screenPoint0.x > mClipRect.right && screenPoint1.x > mClipRect.right) || (screenPoint0.y < mClipRect.top && screenPoint1.y < mClipRect.top) || (screenPoint0.y > mClipRect.bottom && screenPoint1.y > mClipRect.bottom) ) { // update starting point to next position screenPoint0.x = screenPoint1.x; screenPoint0.y = screenPoint1.y; continue; } // check for lines exceeding 180° in longitude, or lines crossing to another map: // cut line into two segments if ((Math.abs(screenPoint1.x - screenPoint0.x) > halfMapSize) // check for lines crossing the southern limit || (screenPoint1.y >= southLimit) != (screenPoint0.y >= southLimit)) { // handle x and y coordinates separately int x0 = screenPoint0.x; int y0 = screenPoint0.y; int x1 = screenPoint1.x; int y1 = screenPoint1.y; // first check x if (Math.abs(screenPoint1.x - screenPoint0.x) > halfMapSize) {// x has to be adjusted if (screenPoint1.x < mapView.getWidth() / 2) { // screenPoint1 is left of screenPoint0 x1 += halfMapSize * 2; // set x1 360° east of screenPoint1 x0 -= halfMapSize * 2; // set x0 360° west of screenPoint0 } else { x1 -= halfMapSize * 2; x0 += halfMapSize * 2; } } // now check y if ((screenPoint1.y >= southLimit) != (screenPoint0.y >= southLimit)) { // line is crossing from one map to the other if (screenPoint1.y >= southLimit) { // screenPoint1 was switched to map below y1 -= halfMapSize * 2; // set y1 into northern map y0 += halfMapSize * 2; // set y0 into map below } else { y1 += halfMapSize * 2; y0 -= halfMapSize * 2; } } if (j+4 > mPts.length) { canvas.drawLines(mPts, 0, j, mPaint); //Log.d(IMapView.LOGTAG,"Polyline points : " + j/4); j = 0; } mPts[j++] = screenPoint0.x; mPts[j++] = screenPoint0.y; mPts[j++] = x1; mPts[j++] = y1; screenPoint0.x = x0; screenPoint0.y = y0; } // end of line break check if (j+4 > mPts.length) { canvas.drawLines(mPts, 0, j, mPaint); //Log.d(IMapView.LOGTAG,"Polyline points : " + j/4); j = 0; } mPts[j++] = screenPoint0.x; mPts[j++] = screenPoint0.y; mPts[j++] = screenPoint1.x; mPts[j++] = screenPoint1.y; // update starting point to next position screenPoint0.x = screenPoint1.x; screenPoint0.y = screenPoint1.y; } if ( j > 0 ) { canvas.drawLines(mPts, 0, j, mPaint); //Log.d(IMapView.LOGTAG,"Polyline points : " + j/4); } /* (Should we really keep that? This is not supported by any other overlay) if (mRepeatPath) { Path mPathCopy = new Path(mPath); mPathCopy.offset(-halfMapSize * 2, 0); // create left shifted copy of mPath if (halfMapSize * 2 < mapView.getWidth()) { mPathCopy.addPath(mPath, halfMapSize * 2, 0); // add right shifted copy of mPath } if (halfMapSize * 2 < mapView.getHeight()) { mPathCopy.addPath(mPathCopy, 0, halfMapSize * 2); // duplicates mPathCopy one map south mPathCopy.addPath(mPath, 0, halfMapSize * 2); // add right shifted copy of mPath } mPathCopy.addPath(mPath, 0, -halfMapSize * 2); // add up shifted copy of mPath canvas.drawPath(mPathCopy, mPaint); } */ } /** Detection is done is screen coordinates. * @param point * @param tolerance in pixels * @return true if the Polyline is close enough to the point. */ public boolean isCloseTo(GeoPoint point, double tolerance, MapView mapView) { final Projection pj = mapView.getProjection(); precomputePoints(pj); Point p = pj.toPixels(point, null); int i = 0; boolean found = false; while (i < mPointsPrecomputed - 1 && !found) { Point projectedPoint1 = mPoints.get(i); if (i == 0){ pj.toPixelsFromProjected(projectedPoint1, mTempPoint1); } else { //reuse last b: mTempPoint1.set(mTempPoint2.x, mTempPoint2.y); } Point projectedPoint2 = mPoints.get(i+1); pj.toPixelsFromProjected(projectedPoint2, mTempPoint2); found = (linePointDist(mTempPoint1, mTempPoint2, p, true) <= tolerance); //TODO: if found, compute and return the point ON the line. i++; } return found; } // Compute the dot product AB x AC private double dot(Point A, Point B, Point C) { double AB_X = B.x - A.x; double AB_Y = B.y - A.y; double BC_X = C.x - B.x; double BC_Y = C.y - B.y; double dot = AB_X * BC_X + AB_Y * BC_Y; return dot; } // Compute the cross product AB x AC private double cross(Point A, Point B, Point C) { double AB_X = B.x - A.x; double AB_Y = B.y - A.y; double AC_X = C.x - A.x; double AC_Y = C.y - A.y; double cross = AB_X * AC_Y - AB_Y * AC_X; return cross; } // Compute the distance from A to B private double distance(Point A, Point B) { double dX = A.x - B.x; double dY = A.y - B.y; return Math.sqrt(dX * dX + dY * dY); } /** * @param A * @param B * @param C * @param isSegment true if AB is a segment, not a line. * @return the distance from AB to C. */ private double linePointDist(Point A, Point B, Point C, boolean isSegment) { double dAB = distance(A, B); if (dAB == 0.0) return distance(A, C); double dist = cross(A, B, C) / dAB; if (isSegment) { double dot1 = dot(A, B, C); if (dot1 > 0) return distance(B, C); double dot2 = dot(B, A, C); if (dot2 > 0) return distance(A, C); } return Math.abs(dist); } public void showInfoWindow(GeoPoint position){ if (mInfoWindow == null) return; mInfoWindow.open(this, position, 0, 0); } @Override public boolean onSingleTapConfirmed(final MotionEvent event, final MapView mapView){ final Projection pj = mapView.getProjection(); GeoPoint eventPos = (GeoPoint) pj.fromPixels((int)event.getX(), (int)event.getY()); double tolerance = mPaint.getStrokeWidth(); boolean touched = isCloseTo(eventPos, tolerance, mapView); if (touched){ if (mOnClickListener == null){ return onClickDefault(this, mapView, eventPos); } else { return mOnClickListener.onClick(this, mapView, eventPos); } } else return touched; } //-- Polyline events listener interfaces ------------------------------------ public interface OnClickListener{ abstract boolean onClick(Polyline polyline, MapView mapView, GeoPoint eventPos); } /** default behaviour when no click listener is set */ protected boolean onClickDefault(Polyline polyline, MapView mapView, GeoPoint eventPos) { polyline.showInfoWindow(eventPos); return true; } @Override public void onDetach(MapView mapView) { mOnClickListener=null; onDestroy(); } }