3

我想在 android maps api 版本 2 中绘制折线。我希望它有多种颜色,最好是渐变。但在我看来,折线只允许有单一颜色。我怎样才能做到这一点?我已经有了 api-v1 覆盖画我喜欢的东西,所以想必我可以重用一些代码

public class RouteOverlayGoogle extends Overlay {
    public void draw(Canvas canvas, MapView mapView, boolean shadow) {
       //(...) draws line with color representing speed
    }
4

2 回答 2

8

我知道自从有人问这个问题以来已经有很长时间了,但是仍然没有渐变折线(截至撰写本文时,〜 2015 年 5 月)并且绘制多条折线确实不会削减它(锯齿状的边缘,当处理几百个点,只是在视觉上不是很吸引人)。

当我不得不实现渐变多段线时,我最终做的是实现一个TileOverlay将多段线渲染到画布然后栅格化它的方法(请参阅此要点了解我编写的具体代码https://gist.github.com /Dagothig/5f9cf0a4a7a42901a7b2)。

该实现不会尝试进行任何类型的视口剔除,因为我最终不需要它来达到我想要的性能(我不确定数字,但每个图块不到一秒,并且多个图块将是同时渲染)。

正确渲染渐变折线可能非常棘手,但是因为您正在处理不同的视口(位置和大小):不仅如此,我还遇到了一些问题,即在高缩放级别(20+)下浮动精度的限制。最后我没有使用画布上的缩放和翻译功能,因为我会遇到奇怪的腐败问题。

如果您使用与我所拥有的数据结构相似的数据结构(纬度、经度和时间戳),还有一点需要注意的是,您需要多个段来正确渲染渐变(我最终一次使用 3 个点)。

为了后代的缘故,我还将在这里留下要点中的代码:(如果您想知道来自哪里,则使用https://github.com/googlemaps/android-maps-utils完成预测)com.google.maps.android.projection.SphericalMercatorProjection

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;
        }
    }
}

请注意,此实现假设了两个相对较大的事情:

  1. 折线已经完成
  2. 只有一条折线。

我认为处理(1)不会很困难。但是,如果您打算以这种方式绘制多条折线,您可能需要寻找一些方法来提高性能(保留折线的边界框以便能够轻松丢弃那些不适合视口的折线)。

关于使用 a 要记住的另一件事TileOverlay是,它是在动作完成后渲染的,而不是在执行期间渲染;因此,您可能希望在其下方使用实际的单色折线来备份叠加层,以使其具有一定的连续性。

PS:这是我第一次尝试回答问题,所以如果有什么我应该解决或做不同的事情,请告诉我。

于 2015-05-11T21:39:47.357 回答
0

一种简单的解决方案:绘制多条折线并单独设置颜色。

于 2013-01-13T06:54:21.353 回答