Android地圖軌跡抽稀、動態繪製

爲何會有這篇文章?

因公司業務調整下降運動門檻,產品部要求引入地圖,記錄用戶的運動軌跡上傳至服務器,用戶進入記錄頁面可查看運動軌跡。並且繪製軌跡的時候要求有一個繪製動畫(參照咕咚)。聽到這心中萬隻草泥馬 ~~~ 但是需求下來了,仍是得硬着頭皮作啊。

提前說一些廢話吧(萬一被我枯燥的文字折磨的滑不到文末,就中途退出,遂提至此處)

做爲一個非計算機專業出身的菜鳥程序員(原專業:大通訊)作完這個需求以後,真正的意識到算法的重要性。若是你能堅持看完,我相信你會和我同樣以爲賊TM diao!!!
下面是公司20年Java大神告誡咱們的話:
「 若是之後要是跳槽,最好選擇去這兩種公司:分析數據的公司 和 收集數據的公司 。未來全部的業務都是根據數據來定的,將來一個公司最寶貴的就是所收集的數據。利用這些數據能夠擴展本身的業務,甚至你只須要收集數據,分析數據,利用數據衍生出服務,將這些服務賣個那些須要的羣體。」
「 數學是分析數據的基礎,趕忙撿起大家的高等數學、線性代數、數理統計、算法導論等書。」
「 將來若是你不懂得學習,那樣你真的只會被淘汰的,人工智能時代真正到來的時候一個不會學習的人只可能生活在「膠囊」裏面。」
PS: 40好幾的人,依然保持旺盛的學習能力,並且據我本身觀察,學習新知識超級快哦,瞬秒咱們公司20歲的一幫小正太(羞射~)

廢話多了,直接進入正題。
本文用到的一切地圖相關的東西都來源高德地圖。至於地圖展現不是本文重點,因此不作贅述。文中說起的距離的計算,根據本身引用的地圖類型作替換便可。文中是地圖API全部會有標註。效果如以下java

運動軌跡效果圖

1、定位點的抽稀-道格拉斯算法

一、道格拉斯簡介

Douglas一Peukcer算法由D.Douglas和T.Peueker於1973年提出,簡稱D一P算法,是眼下公認的線狀要素化簡經典算法。現有的線化簡算法中,有至關一部分都是在該算法基礎上進行改進產生的。它的長處是具備平移和旋轉不變性,給定曲線與閡值後,抽樣結果必定。
算法的基本思路是:對每一條曲線的首末點虛連一條直線,求全部點與直線的距離,並找出最大距離值dmax ,用dmax與限差D相比:若dmax < D ,這條曲線上的中間點全部捨去;若dmax ≥D ,保留dmax 相應的座標點,並以該點爲界,把曲線分爲兩部分,對這兩部分反覆使用該方法。git

算法的具體過程以下:
(1) 在曲線首尾兩點間虛連一條直線,求出其他各點到該直線的距離,如圖3(1)。
(2) 選其最大者與閾值相比較,若大於閾值,則離該直線距離最大的點保留,不然將直線兩端點間各點全部捨去,如圖3(2),第4點保留。
(3) 根據所保留的點,將已知曲線分紅兩部分處理,反覆第一、2步操做,迭代操做,即仍選距離最大者與閾值比較,依次取捨,直到無點可捨去,最後獲得知足給定精度限差的曲線點座標,如圖3(3)、(4)依次保留第6點、第7點,捨去其它點,即完成線的化簡。程序員

道格拉斯算法示意圖

二、算法代碼實現

存儲經緯度座標的實體LatLngPointgithub

public class LatLngPoint implements Comparable<LatLngPoint> {
    /**
     * 用於記錄每個點的序號
     */
    public int id;
    /**
     * 每個點的經緯度
     */
    public LatLng latLng;

    public LatLngPoint(int id,LatLng latLng){
        this.id = id;
        this.latLng = latLng;
    }

    @Override
    public int compareTo(@NonNull LatLngPoint o) {
        if (this.id < o.id) {
            return -1;
        } else if (this.id > o.id)
            return 1;
        return 0;
    }
}

使用三角形面積(使用海倫公式求得)相等方法計算點pX到點pA和pB所肯定的直線的距離,
AMapUtils.calculateLineDistance(start.latLng, end.latLng)計算兩點之間的距離,此公式高德API面試

/**
     * 使用三角形面積(使用海倫公式求得)相等方法計算點pX到點pA和pB所肯定的直線的距離
     * @param start  起始經緯度
     * @param end    結束經緯度
     * @param center 前兩個點之間的中心點
     * @return 中心點到 start和end所在直線的距離
     */
    private double distToSegment(LatLngPoint start, LatLngPoint end, LatLngPoint center) {
        double a = Math.abs(AMapUtils.calculateLineDistance(start.latLng, end.latLng));
        double b = Math.abs(AMapUtils.calculateLineDistance(start.latLng, center.latLng));
        double c = Math.abs(AMapUtils.calculateLineDistance(end.latLng, center.latLng));
        double p = (a + b + c) / 2.0;
        double s = Math.sqrt(Math.abs(p * (p - a) * (p - b) * (p - c)));
        double d = s * 2.0 / a;
        return d;
    }

Douglas工具類具體代碼算法

public Douglas(ArrayList<LatLng> mLineInit, double dmax) {
        if (mLineInit == null) {
            throw new IllegalArgumentException("傳入的經緯度座標list == null");
        }
        this.dMax = dmax;
        this.start = 0;
        this.end = mLineInit.size() - 1;
        for (int i = 0; i < mLineInit.size(); i++) {
            this.mLineInit.add(new LatLngPoint(i, mLineInit.get(i)));
        }
    }

    /**
     * 壓縮經緯度點
     *
     * @return
     */
    public ArrayList<LatLng> compress() {
        int size = mLineInit.size();
        ArrayList<LatLngPoint> latLngPoints = compressLine(mLineInit.toArray(new LatLngPoint[size]), mLineFilter, start, end, dMax);
        latLngPoints.add(mLineInit.get(0));
        latLngPoints.add(mLineInit.get(size-1));
        //對抽稀以後的點進行排序
        Collections.sort(latLngPoints, new Comparator<LatLngPoint>() {
            @Override
            public int compare(LatLngPoint o1, LatLngPoint o2) {
                return o1.compareTo(o2);
            }
        });
        ArrayList<LatLng> latLngs = new ArrayList<>();
        for (LatLngPoint point : latLngPoints) {
            latLngs.add(point.latLng);
        }
        return latLngs;
    }


    /**
     * 根據最大距離限制,採用DP方法遞歸的對原始軌跡進行採樣,獲得壓縮後的軌跡
     * x
     *
     * @param originalLatLngs 原始經緯度座標點數組
     * @param endLatLngs      保持過濾後的點座標數組
     * @param start           起始下標
     * @param end             結束下標
     * @param dMax            預先指定好的最大距離偏差
     */
    private ArrayList<LatLngPoint> compressLine(LatLngPoint[] originalLatLngs, ArrayList<LatLngPoint> endLatLngs, int start, int end, double dMax) {
        if (start < end) {
            //遞歸進行調教篩選
            double maxDist = 0;
            int currentIndex = 0;
            for (int i = start + 1; i < end; i++) {
                double currentDist = distToSegment(originalLatLngs[start], originalLatLngs[end], originalLatLngs[i]);
                if (currentDist > maxDist) {
                    maxDist = currentDist;
                    currentIndex = i;
                }
            }
            //若當前最大距離大於最大距離偏差
            if (maxDist >= dMax) {
                //將當前點加入到過濾數組中
                endLatLngs.add(originalLatLngs[currentIndex]);
                //將原來的線段以當前點爲中心拆成兩段,分別進行遞歸處理
                compressLine(originalLatLngs, endLatLngs, start, currentIndex, dMax);
                compressLine(originalLatLngs, endLatLngs, currentIndex, end, dMax);
            }
        }
        return endLatLngs;
    }

結果:

上圖中展現的軌跡是定位獲得的4417個點,通過抽稀以後繪製在地圖上的樣式。算法中傳入的闕值是104417個點處理以後只136個點。並且這136個點繪製的軌跡和4417個點繪製的軌跡幾乎沒有什麼差異。
不知道大家有沒有被震撼到,反正我是不折不扣被震到了。做爲算法小白的我,感受整個世界都被顛覆了。canvas

2、軌跡繪製-自定義運動軌跡View

最開始得時候認爲直接在地圖上繪製動態軌跡的,根據高德提供繪製軌跡的API,結果直接卡死。當時一臉懵逼的找高德客服,一提升德的客服更讓人窩火。算了不提了。後面本身試了好多遍以後放棄直接在地圖上繪製,不知道哪一刻,就忽然想到在地圖上覆蓋一個自定義的View。當時有一瞬間以爲本身是這個世界上智商最接近250的 ┐(‘~`;)┌
地圖API提供了經緯度轉換成手機上的座標,因此能夠拿到地圖上點對應的屏幕的位置,也就天然能夠自定義一個View動態的繪製軌跡,當自定義View的動畫結束以後,隱藏自定義View而後在地圖上繪製軌跡。這就是個人總體思路,下面袖子擼起,上代碼。數組

一、初始化變量、畫筆、path

* 起點Paint
     */
    private Paint mStartPaint;
    /**
     * 起點
     */
    private Point mStartPoint;
    /**
     * 起點bitmap
     */
    private Bitmap mStartBitmap;
    /**
     * 軌跡
     */
    private Paint mLinePaint;
    /**
     * 小亮球
     */
    private Paint mLightBallPaint;
    /**
     * 小兩球的bitmap  UI切圖
     */
    private Bitmap mLightBallBitmap;
    /**
     * 起點rect 若是爲空時不繪製小亮球
     */
    private Rect mStartRect;
    /**
     * 屏幕寬度
     */
    private int mWidth;
    /**
     * 屏幕高度
     */
    private int mHeight;
    /**
     * 軌跡path
     */
    private Path mLinePath;
    /**
     * 保存每一次刷新界面軌跡的重點座標
     */
    private float[] mCurrentPosition = new float[2];



    public SportTrailView(Context context) {
        this(context, null);
    }

    public SportTrailView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SportTrailView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initPaint();
    }

    /**
     * 初始化畫筆,path
     */
    private void initPaint() {
        mLinePaint = new Paint();
        mLinePaint.setColor(Color.parseColor("#ff00ff42"));
        mLinePaint.setStyle(Paint.Style.STROKE);
        mLinePaint.setStrokeWidth(10);
        mLinePaint.setStrokeCap(Paint.Cap.ROUND);
        mLinePaint.setAntiAlias(true);

        mLightBallPaint = new Paint();
        mLightBallPaint.setAntiAlias(true);
        mLightBallPaint.setFilterBitmap(true);

        mStartPaint = new Paint();
        mStartPaint.setAntiAlias(true);
        mStartPaint.setFilterBitmap(true);

        mLinePath = new Path();

    }

二、軌跡View繪製

protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //繪製軌跡
        canvas.drawPath(mLinePath, mLinePaint);
        //繪製引導亮球
        if (mLightBallBitmap !=null && mStartRect !=null){
            int width = mLightBallBitmap.getWidth();
            int height = mLightBallBitmap.getHeight();
            RectF rect = new RectF();
            rect.left = mCurrentPosition[0] - width;
            rect.right = mCurrentPosition[0] + width;
            rect.top = mCurrentPosition[1] - height;
            rect.bottom = mCurrentPosition[1] + height;
            canvas.drawBitmap(mLightBallBitmap, null, rect, mLightBallPaint);
        }
        //繪製起點
        if (mStartBitmap != null && mStartPoint != null) {
            if (mStartRect == null) {
                int width = mStartBitmap.getWidth() / 2;
                int height = mStartBitmap.getHeight() / 2;
                mStartRect = new Rect();
                mStartRect.left = mStartPoint.x - width;
                mStartRect.right = mStartPoint.x + width;
                mStartRect.top = mStartPoint.y - height;
                mStartRect.bottom = mStartPoint.y + height;
            }
            canvas.drawBitmap(mStartBitmap, null, mStartRect, mStartPaint);
        }
    }

三、設置數據

/**
     * 繪製運動軌跡
     * @param mPositions 道格拉斯算法抽稀事後對應的點座標
     * @param startPointResId 起點圖片的資源id
     * @param lightBall 小亮球的資源id
     * @param listener 軌跡繪製完成的監聽
     */
    public void drawSportLine(final List<Point> mPositions, @DrawableRes int startPointResId,@DrawableRes int lightBall, final OnTrailChangeListener listener) {
        if (mPositions.size() <= 1) {
            listener.onFinish();
            return;
        }
        //用於
        Path path = new Path();
        for (int i = 0; i < mPositions.size(); i++) {
            if (i == 0) {
                path.moveTo(mPositions.get(i).x, mPositions.get(i).y);
            } else {
                path.lineTo(mPositions.get(i).x, mPositions.get(i).y);
            }
        }

        final PathMeasure pathMeasure = new PathMeasure(path, false);
        //軌跡的長度
        final float length = pathMeasure.getLength();
        if (length < ViewUtil.dip2Px(getContext(), 16)) {
            listener.onFinish();
            return;
        }
        //動態圖中展現的亮色小球(UI切圖)
        mLightBallBitmap = BitmapFactory.decodeResource(getResources(), lightBall);
        //起點
        mStartPoint = new Point(mPositions.get(0).x, mPositions.get(0).y);
        mStartBitmap = BitmapFactory.decodeResource(getResources(), startPointResId);
        ValueAnimator animator = ValueAnimator.ofFloat(0, length);
        animator.setDuration(3000);
        animator.setInterpolator(new AccelerateDecelerateInterpolator());
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float value = (Float) animation.getAnimatedValue();
                // 獲取當前點座標封裝到mCurrentPosition  
                pathMeasure.getPosTan(value, mCurrentPosition, null);
                if (value == 0) {
                    //若是當前的運動軌跡長度爲0,設置path的起點
                    mLinePath.moveTo(mPositions.get(0).x, mPositions.get(0).y);
                }
                //pathMeasure.getSegment()方法用於保存當前path路徑,
                //下次繪製時從上一次終點位置處繪製,不會從開始的位置開始繪製。
                pathMeasure.getSegment(0, value, mLinePath, true);
                invalidate();
                //若是當前的長度等於pathMeasure測量的長度,則表示繪製完畢,
                if (value == length && listener != null) {
                    listener.onFinish();
                }
            }
        });
        animator.start();
    }

    /**
     * 軌跡繪製完成監聽
     */
    public interface OnTrailChangeListener {
        void onFinish();
    }

來來來,小板凳,劃重點!!!

本文重點:算法、算法、算法、PathMeasure、Path

說到底寫這篇文章的初衷仍是想讓大多數和我同樣的朋友能意識到算法的重要性,以前由於不是計算機專業畢業,因此只聽別人說算法如何如何重要,但在本身的心裏裏卻並無多重視。可是當你程序中真正用到的時候,你會發現算法之美。強大的算法會讓你在千千萬萬的數據中找尋真正的美(也就是去除噪聲,原諒我毫無徵兆的文藝一下)。做爲一個叛變的工科生,曾經懷疑過爲何要學數學,日常工做生活中徹底用不到當年所學的夾逼定理啊,讓人有種報國無門的感受,但是通過此次算法的洗禮以後,讓我想起了闊別多年的數學,並且讓我第一次真正的意識到數學真的賊TM有用。若是你還想在程序這條路上繼續下去那你真的應該儘快撿起數學
至此,我想說的也完了。但願能幫到有相似需求的猿友們,文中有錯誤的地方請指出。-.-服務器

Github主頁:https://github.com/Walll-E架構

Android開發資料+面試架構資料 免費分享 點擊連接 便可領取

《Android架構師必備學習資源免費領取(架構視頻+面試專題文檔+學習筆記)》

相關文章
相關標籤/搜索