/* * Copyright 2012, osmdroid: Viesturs Zarins, Martin Pearman * Copyright 2012, Hannes Janetzek * * This program is free software: you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License as published by the Free Software * Foundation, either version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License along with * this program. If not, see . */ package org.oscim.overlay; import java.util.ArrayList; import org.oscim.core.GeoPoint; import org.oscim.core.MapPosition; import org.oscim.core.MercatorProjection; import org.oscim.renderer.layer.Layer; import org.oscim.renderer.layer.LineLayer; import org.oscim.renderer.overlays.BasicOverlay; import org.oscim.theme.renderinstruction.Line; import org.oscim.utils.FastMath; import org.oscim.view.MapView; import android.content.Context; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Paint.Cap; /** This class draws a path line in given color. */ public class PathOverlay extends Overlay { /** Stores points, converted to the map projection. */ /* package */protected final ArrayList mPoints; /* package */boolean mUpdatePoints; /** Paint settings. */ protected Paint mPaint = new Paint(); class RenderPath extends BasicOverlay { private static final byte MAX_ZOOM = 20; private static final int MIN_DIST = 4; // pre-projected points to zoomlovel 20 private int[] mPreprojected; // projected points private float[] mPPoints; private final short[] mIndex; private int mSize; private final Line mLine; // limit coords private final int max = 2048; public RenderPath(MapView mapView) { super(mapView); mLine = new Line(Color.BLUE, 3.0f, Cap.BUTT); mIndex = new short[1]; mPPoints = new float[1]; } // note: this is called from GL-Thread. so check your syncs! // TODO use an Overlay-Thread to build up layers (like for Labeling) @Override public synchronized void update(MapPosition curPos, boolean positionChanged, boolean tilesChanged) { if (!tilesChanged && !mUpdatePoints) return; float[] projected = mPPoints; if (mUpdatePoints) { // pre-project point on zoomlelvel 20 synchronized (mPoints) { mUpdatePoints = false; ArrayList geopoints = mPoints; int size = geopoints.size(); int[] points = mPreprojected; mSize = size * 2; if (mSize > projected.length) { points = mPreprojected = new int[mSize]; projected = mPPoints = new float[mSize]; } for (int i = 0, j = 0; i < size; i++, j += 2) { GeoPoint p = geopoints.get(i); points[j + 0] = (int) MercatorProjection.longitudeToPixelX( p.getLongitude(), MAX_ZOOM); points[j + 1] = (int) MercatorProjection.latitudeToPixelY( p.getLatitude(), MAX_ZOOM); } } } int size = mSize; // keep position to render relative to current state updateMapPosition(); // items are placed relative to scale == 1 mMapPosition.scale = 1; // layers.clear(); LineLayer ll = (LineLayer) layers.getLayer(1, Layer.LINE); // reset verticesCnt to reuse layer ll.verticesCnt = 0; ll.line = mLine; ll.width = 2.5f; int x, y, px = 0, py = 0; int i = 0; int diff = MAX_ZOOM - mMapPosition.zoomLevel; int mx = (int) mMapPosition.x; int my = (int) mMapPosition.y; for (int j = 0; j < size; j += 2) { // TODO translate mapPosition and do this after clipping x = (mPreprojected[j + 0] >> diff) - mx; y = (mPreprojected[j + 1] >> diff) - my; // TODO use line clipping, this doesnt work with 'GreatCircle' // TODO clip to view bounding box if (x > max || x < -max || y > max || y < -max) { if (i > 2) { mIndex[0] = (short) i; ll.addLine(projected, mIndex, false); } i = 0; continue; } // skip too near points int dx = x - px; int dy = y - py; if ((i == 0) || FastMath.absMaxCmp(dx, dy, MIN_DIST)) { projected[i + 0] = px = x; projected[i + 1] = py = y; i += 2; } } mIndex[0] = (short) i; ll.addLine(projected, mIndex, false); newData = true; } } public PathOverlay(MapView mapView, final int color, final Context ctx) { super(); this.mPaint.setColor(color); this.mPaint.setStrokeWidth(2.0f); this.mPaint.setStyle(Paint.Style.STROKE); this.mPoints = new ArrayList(); mLayer = new RenderPath(mapView); } public void setColor(final int color) { this.mPaint.setColor(color); } public void setAlpha(final int a) { this.mPaint.setAlpha(a); } public Paint getPaint() { return mPaint; } public void setPaint(final Paint pPaint) { if (pPaint == null) { throw new IllegalArgumentException("pPaint argument cannot be null"); } mPaint = pPaint; } /** * Draw a great circle. Calculate a point for every 100km along the path. * * @param startPoint * start point of the great circle * @param endPoint * end point of the great circle */ public void addGreatCircle(final GeoPoint startPoint, final GeoPoint endPoint) { synchronized (mPoints) { // get the great circle path length in meters final int greatCircleLength = startPoint.distanceTo(endPoint); // add one point for every 100kms of the great circle path final int numberOfPoints = greatCircleLength / 100000; addGreatCircle(startPoint, endPoint, numberOfPoints); } } /** * Draw a great circle. * * @param startPoint * start point of the great circle * @param endPoint * end point of the great circle * @param numberOfPoints * number of points to calculate along the path */ public 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() * Math.PI / 180; final double lon1 = startPoint.getLongitude() * Math.PI / 180; final double lat2 = endPoint.getLatitude() * Math.PI / 180; final double lon2 = endPoint.getLongitude() * Math.PI / 180; 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)) / -(Math.PI / 180); bearing = bearing < 0 ? 360 + bearing : bearing; for (int i = 0, j = numberOfPoints + 1; i < j; i++) { final double f = 1.0 / numberOfPoints * i; 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 / (Math.PI / 180) * 1E6), (int) (lonN / (Math.PI / 180) * 1E6)); } } public void clearPath() { synchronized (mPoints) { mPoints.clear(); mUpdatePoints = true; } } public void addPoint(final GeoPoint pt) { synchronized (mPoints) { this.mPoints.add(pt); mUpdatePoints = true; } } public void addPoint(final int latitudeE6, final int longitudeE6) { synchronized (mPoints) { this.mPoints.add(new GeoPoint(latitudeE6, longitudeE6)); mUpdatePoints = true; } } public int getNumberOfPoints() { return this.mPoints.size(); } }