軌跡回放實(shí)現(xiàn)與優(yōu)化)
簡(jiǎn)介面向C#開發(fā)者的GMap地圖開發(fā)實(shí)戰(zhàn)資源重點(diǎn)演示軌跡回放功能的完整實(shí)現(xiàn)。資源涵蓋TXT坐標(biāo)文件解析、地圖源切換、自定義Marker圖標(biāo)、路徑顏色與樣式設(shè)置、自適應(yīng)屏幕等關(guān)鍵知識(shí)點(diǎn)并附有MapSimulator示例工程完整展示從讀取經(jīng)緯度數(shù)據(jù)到定時(shí)器驅(qū)動(dòng)動(dòng)態(tài)播放軌跡的全過程。壓縮包共377個(gè)文件主要包含cs源碼、dll庫(kù)文件、png圖標(biāo)素材、xml配置文件、exe示例程序、txt坐標(biāo)數(shù)據(jù)及pdb調(diào)試符號(hào)整體體積約68.99MB。包內(nèi)目錄組織清晰適合直接載入Visual Studio學(xué)習(xí)或二次開發(fā)幫助讀者快速掌握GMapControl、GMapRoute、GMarkerGoogle等核心類的實(shí)際用法。已有1605人瀏覽學(xué)習(xí)適合需要在地圖應(yīng)用中實(shí)現(xiàn)軌跡回放、自定義圖標(biāo)與線路樣式的地圖開發(fā)初學(xué)者和進(jìn)階者。 做 C# 上位機(jī)項(xiàng)目的朋友應(yīng)該都有同感只要系統(tǒng)里涉及到車輛、設(shè)備或者人員定位軌跡回放這個(gè)功能早晚會(huì)排到需求列表上。我這次接手的設(shè)備監(jiān)控項(xiàng)目里客戶明確要求把所有設(shè)備的移動(dòng)軌跡按時(shí)間順序重新播放一遍還要支持調(diào)速和進(jìn)度條拖動(dòng)。地圖組件我直接選了 GMap.NET——C# 生態(tài)里少見的把地圖控件做到開箱即用的庫(kù)支持 WinForms 和 WPF內(nèi)置多種在線地圖源軌跡回放里最麻煩的瓦片加載和坐標(biāo)投影它都替你處理了。這篇文章順著我實(shí)際的開發(fā)過程把軌跡數(shù)據(jù)的組織、軌跡線繪制、回放動(dòng)畫實(shí)現(xiàn)以及性能優(yōu)化和踩坑記錄全部過一遍準(zhǔn)備做類似功能的可以直接拿它當(dāng)參考方案。1. 項(xiàng)目背景與整體思路1.1 為什么選 GMap.NET 而不是其他地圖方案C# 桌面端做地圖能走的路其實(shí)不多。我估摸著自己也算把幾種方案都試過一遍的人這里直接說對(duì)比結(jié)論。第一種是用 WebBrowser 控件嵌一個(gè)網(wǎng)頁版地圖調(diào)用高德、百度的 JavaScript API。這種方案地圖效果好、接口全但問題出在 C# 和前端頁面的通信上。軌跡回放需要頻繁傳坐標(biāo)、控制播放狀態(tài)每次都要走InvokeScript或者ObjectForScripting調(diào)試起來兩頭跑遇到頁面加載慢、控件版本兼容性差的時(shí)候想死的心都有。如果我們只是顯示一張靜態(tài)地圖還行要做高頻更新的回放動(dòng)畫這種方案的體驗(yàn)不好。第二種是商業(yè)收費(fèi)控件功能穩(wěn)定但要錢而且很多是國(guó)外庫(kù)中文資料少萬一要改個(gè)樣式、加個(gè)圖層你去翻文檔的時(shí)間夠再寫一個(gè)功能了。第三種是自己解析瓦片再 GDI 繪制這個(gè)工作量和學(xué)習(xí)曲線就太大了適合專門做 GIS 產(chǎn)品的團(tuán)隊(duì)不適合項(xiàng)目里要快速落地一個(gè)輔助功能。GMap.NET 是我最后的選擇理由很實(shí)在開源、免費(fèi)、純 C# 實(shí)現(xiàn)不需要額外裝運(yùn)行時(shí)對(duì) WinForms 的友好程度幾乎是天生的直接拖一個(gè)控件到窗體里就能看到地圖支持 OpenStreetMap 等多個(gè)地圖源而且可以在代碼里隨時(shí)切換社區(qū)活躍國(guó)內(nèi)外做車輛監(jiān)控、物流調(diào)度的人用它的非常多遇到問題基本都能搜到答案。軌跡回放里最花時(shí)間的瓦片緩存、投影轉(zhuǎn)換、縮放響應(yīng)它內(nèi)部都處理好了我們可以集中精力寫業(yè)務(wù)邏輯。1.2 軌跡回放的本質(zhì)是四個(gè)子問題很多第一次做軌跡回放的朋友容易把它想成讓一個(gè)圖標(biāo)在地圖上動(dòng)起來實(shí)現(xiàn)過程卻不斷返工。我把這個(gè)功能拆成了四個(gè)子問題每個(gè)單獨(dú)處理思路一下就清晰了。數(shù)據(jù)層軌跡點(diǎn)怎么存、怎么讀取一個(gè)點(diǎn)包含哪些字段才能支撐后面的繪制和動(dòng)畫。繪制層整條軌跡線怎么畫到地圖上回放過程中已經(jīng)走過的路線和尚未走過的路線如何區(qū)分。動(dòng)畫層用什么機(jī)制驅(qū)動(dòng)坐標(biāo)點(diǎn)隨時(shí)間推進(jìn)怎么實(shí)現(xiàn)播放、暫停、停止、倍速、進(jìn)度條拖動(dòng)。交互層地圖在回放時(shí)是自由拖動(dòng)還是鎖定跟隨縮放之后怎么快速回到目標(biāo)位置點(diǎn)擊軌跡能不能彈出詳情信息。這四個(gè)問題不是串行的而是要同時(shí)考慮。比如數(shù)據(jù)層如果沒有存方向角后面畫車輛圖標(biāo)的時(shí)候就會(huì)發(fā)現(xiàn)圖標(biāo)朝向沒辦法計(jì)算只能再補(bǔ)錄數(shù)據(jù)。所以一開始建數(shù)據(jù)結(jié)構(gòu)時(shí)就要把整個(gè)回放鏈路需要的字段一次想清楚。2. 軌跡數(shù)據(jù)模型與軌跡線繪制2.1 軌跡點(diǎn)數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)軌跡回放的數(shù)據(jù)來源一般是設(shè)備上報(bào)的 GPS 定位數(shù)據(jù)落到數(shù)據(jù)庫(kù)里通常是這樣的字段設(shè)備編號(hào)、經(jīng)度、緯度、定位時(shí)間、速度、方向角。我建了一個(gè)TrackPoint類來對(duì)應(yīng)public class TrackPoint { public double Lng { get; set; } // 經(jīng)度 public double Lat { get; set; } // 緯度 public DateTime Time { get; set; } // 定位時(shí)間 public double Speed { get; set; } // 速度 km/h public double Heading { get; set; } // 方向角正北為0順時(shí)針 }有幾個(gè)字段很多人會(huì)忽略我重點(diǎn)說一下。Time字段很多人覺得播放順序就是列表順序存時(shí)間多余實(shí)際上進(jìn)度條要顯示時(shí)間、要按時(shí)間區(qū)間篩選軌跡沒有時(shí)間字段后面做步進(jìn)計(jì)算會(huì)很別扭。Heading方向角不是必須的但如果沒有它回放時(shí)車輛圖標(biāo)就沒有朝向代碼里只能通過前后兩個(gè)點(diǎn)算方向遇到車輛原地停留或者 GPS 抖動(dòng)圖標(biāo)就會(huì)瘋狂亂轉(zhuǎn)。Speed在軌跡詳情展示里用得上也能用于判斷異常停車。讀取數(shù)據(jù)時(shí)按時(shí)間排序是必須的我一般查完數(shù)據(jù)庫(kù)之后還會(huì)在內(nèi)存里做一次校驗(yàn)防止臟數(shù)據(jù)把播放順序打亂。2.2 用 GMap.NET 把軌跡線畫到地圖上GMap.NET 的核心邏輯是 Overlay 圖層機(jī)制。一個(gè)地圖上可以掛多個(gè) Overlay比如一個(gè)圖層放軌跡線一個(gè)圖層放設(shè)備圖標(biāo)一個(gè)圖層放起點(diǎn)終點(diǎn)標(biāo)記互不干擾。畫軌跡線我用的是GMapRoute它本質(zhì)上是一個(gè)點(diǎn)的集合會(huì)被繪制成一條線。基本代碼是這樣的private GMapOverlay _trackOverlay; void DrawTrackLine(ListTrackPoint trackPoints) { gMapControl.Overlays.Clear(); _trackOverlay new GMapOverlay(track); ListPointLatLng points trackPoints .Select(p new PointLatLng(p.Lat, p.Lng)) .ToList(); var route new GMapRoute(points, track-route) { Stroke new Pen(Color.FromArgb(255, 64, 84), 4), IsHitTestVisible true }; _trackOverlay.Routes.Add(route); gMapControl.Overlays.Add(_trackOverlay); gMapControl.ZoomAndCenterMarkers(track); }這里有兩個(gè)細(xì)節(jié)容易出錯(cuò)。第一Pen的寬度要給大一點(diǎn)至少 3 到 4 像素尤其是縮放到比較小的比例尺時(shí)太細(xì)的線幾乎看不見但也不要超過 6太粗會(huì)遮擋底圖。第二IsHitTestVisible這個(gè)屬性要設(shè)為true否則后面想加鼠標(biāo)點(diǎn)擊選中軌跡線的交互就做不了。我做的這個(gè)功能里客戶要求回放過程中已經(jīng)走過的部分顯示為深色實(shí)線還沒走過的部分顯示為淺色虛線。這個(gè)效果不能靠一條GMapRoute實(shí)現(xiàn)我拆成了兩條線一條畫整條軌跡的淺色背景線另一條只畫從起點(diǎn)到當(dāng)前播放位置的前景色線。播放時(shí)只需要更新前景線的點(diǎn)集視覺上就有了軌跡漸漸畫出來的效果。這個(gè)方案簡(jiǎn)單穩(wěn)定實(shí)測(cè)效果很好。3. 回放動(dòng)畫核心實(shí)現(xiàn)3.1 播放機(jī)制用 UI 定時(shí)器驅(qū)動(dòng)而不是后臺(tái)線程軌跡回放的動(dòng)畫本質(zhì)就是按照時(shí)間順序不斷更新地圖上那個(gè)當(dāng)前位置點(diǎn)。實(shí)現(xiàn)方式有很多我最推薦也一直在用的是System.Windows.Forms.Timer。private System.Windows.Forms.Timer _playTimer; private int _currentIndex; private double _speed 1.0; void InitTimer() { _playTimer new System.Windows.Forms.Timer(); _playTimer.Interval 100; // 100ms 刷新一次即每秒10幀 _playTimer.Tick OnPlayTick; } void OnPlayTick(object sender, EventArgs e) { if (_currentIndex _trackPoints.Count - 1) { StopPlay(); return; } // 按倍速推進(jìn)索引 _currentIndex (int)Math.Max(1, _speed); var currentPoint _trackPoints[_currentIndex]; UpdateMarkerPosition(currentPoint); UpdateForegroundRoute(); UpdateProgressBar(); UpdateMapCenter(currentPoint); }為什么不用Thread.Sleep配合while循環(huán)因?yàn)榛胤胚^程中要實(shí)時(shí)更新地圖控件和進(jìn)度條這些操作必須在 UI 線程上執(zhí)行。Timer的回調(diào)函數(shù)本身就在 UI 線程上觸發(fā)可以直接操作控件不用寫Invoke。如果用獨(dú)立線程做循環(huán)每一幀都要Invoke回 UI 線程跨線程調(diào)用的性能損耗和代碼復(fù)雜度都不劃算。Timer的Interval我通常設(shè)成 100 毫秒也就是每秒刷新 10 次。這個(gè)頻率對(duì)軌跡動(dòng)畫來說足夠平滑又不會(huì)因?yàn)樗⑿绿l繁導(dǎo)致 CPU 占用過高。倍速功能不是去改Timer.Interval而是每次 Tick 多推進(jìn)幾個(gè)點(diǎn)位索引。這個(gè)邏輯要理解清楚加快回放速度的本質(zhì)是每次刷新走更多的點(diǎn)而不是讓刷新的頻率更快。3.2 位置插值與車輛圖標(biāo)朝向GPS 采樣點(diǎn)之間的間隔可能很稀疏比如設(shè)備 30 秒上報(bào)一次位置。如果直接讓圖標(biāo)從 A 點(diǎn)跳到 B 點(diǎn)動(dòng)畫就像瞬移一樣非常不連貫。解決辦法是在兩個(gè)軌跡點(diǎn)之間做插值讓圖標(biāo)平滑過渡。private PointLatLng Interpolate(TrackPoint a, TrackPoint b, double factor) { return new PointLatLng( a.Lat (b.Lat - a.Lat) * factor, a.Lng (b.Lng - a.Lng) * factor ); }factor取值范圍是 0 到 1代表從 A 點(diǎn)向 B 點(diǎn)過渡的比例。在TimerTick里我維護(hù)了一個(gè)額外的插值進(jìn)度變量每幀累加一個(gè)固定步長(zhǎng)到 1 之后切換到下一對(duì)點(diǎn)重新開始。這樣兩個(gè)定點(diǎn)之間就可以拆出 10 到 20 個(gè)中間幀動(dòng)畫自然就順滑了。車輛圖標(biāo)朝向的計(jì)算用的是兩個(gè)連續(xù)點(diǎn)的方向角。這里最容易出錯(cuò)的是Atan2的參數(shù)順序。經(jīng)度差對(duì)應(yīng) X 軸緯度差對(duì)應(yīng) Y 軸但方向角定義是正北為 0 度、順時(shí)針增加和數(shù)學(xué)坐標(biāo)系里逆時(shí)針為正不一樣所以計(jì)算完要轉(zhuǎn)換一次private double CalcHeading(TrackPoint a, TrackPoint b) { double dx b.Lng - a.Lng; double dy b.Lat - a.Lat; double angle Math.Atan2(dx, dy) * 180.0 / Math.PI; return (angle 360.0) % 360.0; }圖標(biāo)本身是一個(gè)GMapMarker我通常會(huì)準(zhǔn)備一張車輛箭頭的 PNG 圖片根據(jù)計(jì)算出的方向角對(duì)圖片做旋轉(zhuǎn)再繪制到 Marker 上。有一個(gè)細(xì)節(jié)Marker 的Offset屬性要設(shè)置成圖片尺寸的一半否則圖標(biāo)中心點(diǎn)和定位點(diǎn)對(duì)不上視覺上車輛就是偏的。3.3 相機(jī)跟隨與播放控制回放的時(shí)候地圖要跟著車輛走不然看兩秒車就跑出屏幕了。我實(shí)現(xiàn)了一個(gè)跟隨模式開關(guān)播放時(shí)自動(dòng)開啟用戶手動(dòng)拖動(dòng)地圖時(shí)可以臨時(shí)取消跟隨。private void UpdateMapCenter(PointLatLng pos) { if (_isFollowMode) { gMapControl.Position pos; } }這個(gè)邏輯很簡(jiǎn)單但有一個(gè)性能坑需要注意gMapControl.Position的賦值會(huì)觸發(fā)整個(gè)地圖的重新渲染如果每一幀都調(diào)用性能消耗非常大尤其是在低配電腦上會(huì)出現(xiàn)明顯的掉幀。我實(shí)際測(cè)試后做了優(yōu)化只在車輛位置距離地圖中心超過一定閾值比如半個(gè)屏幕時(shí)才刷新中心否則只更新 Marker 的位置。這樣地圖不會(huì)一直頻繁重繪動(dòng)畫反而更流暢。播放控制條我用了一套Button TrackBar的組合。播放、暫停、停止是三個(gè)常規(guī)按鈕速度切換用了一個(gè)ComboBox進(jìn)度條TrackBar綁定到軌跡點(diǎn)的總數(shù)量。用戶在拖動(dòng)進(jìn)度條時(shí)要暫停 Timer松開之后再?gòu)男碌奈恢美^續(xù)播放不然會(huì)出現(xiàn)進(jìn)度條來回跳的沖突。private void ProgressBar_Scroll(object sender, EventArgs e) { bool wasPlaying _isPlaying; StopPlay(); _currentIndex progressBar.Value; UpdateMarkerPosition(_trackPoints[_currentIndex]); if (wasPlaying) StartPlay(); }4. 性能優(yōu)化與大數(shù)據(jù)量軌跡處理4.1 軌跡抽稀道格拉斯-普克算法設(shè)備一天如果每 10 秒上報(bào)一個(gè)點(diǎn)24 小時(shí)就是 8640 個(gè)點(diǎn)。如果是高頻設(shè)備每 3 秒上報(bào)一次那就是接近三萬點(diǎn)。GMap.NET 的GMapRoute畫幾千個(gè)點(diǎn)的軌跡線還好但一旦突破一萬繪制和刷新的卡頓感就會(huì)非常明顯。解決思路是做軌跡抽稀。保留軌跡整體形狀的同時(shí)去掉冗余的中間點(diǎn)。最常用的算法是道格拉斯-普克Douglas-Peucker它用一個(gè)距離閾值判斷哪些點(diǎn)可以丟棄保留起點(diǎn)和終點(diǎn)找到離這條連線最遠(yuǎn)的點(diǎn)如果距離大于閾值就保留然后遞歸處理兩段如果小于閾值就刪除中間所有點(diǎn)。簡(jiǎn)化實(shí)現(xiàn)如下private ListTrackPoint Simplify(ListTrackPoint points, double epsilon) { if (points.Count 3) return points; double maxDist 0; int index 0; var first points[0]; var last points[points.Count - 1]; for (int i 1; i points.Count - 1; i) { double dist PerpendicularDistance(points[i], first, last); if (dist maxDist) { maxDist dist; index i; } } if (maxDist epsilon) { var left Simplify(points.GetRange(0, index 1), epsilon); var right Simplify(points.GetRange(index, points.Count - index), epsilon); left.AddRange(right.Skip(1)); return left; } else { return new ListTrackPoint { first, last }; } }PerpendicularDistance是求點(diǎn)到直線的垂直距離網(wǎng)上有很多現(xiàn)成的數(shù)學(xué)實(shí)現(xiàn)就不貼了。epsilon的取值我建議用經(jīng)驗(yàn)值在 0.0002 到 0.0005 之間經(jīng)緯度單位對(duì)應(yīng)的實(shí)際距離大約是 20 到 50 米?;胤耪故居?50 米的抽稀閾值細(xì)節(jié)信息用 20 米性能和效果能取得比較好的平衡。4.2 避免 UI 卡頓的其他關(guān)鍵做法抽稀是解決大數(shù)據(jù)量軌跡問題的核心手段但實(shí)際操作中還有幾個(gè)容易踩的卡頓點(diǎn)。第一加載海量歷史軌跡時(shí)不要在 UI 線程里從數(shù)據(jù)庫(kù)讀取和解析。我一般會(huì)用一個(gè)后臺(tái)線程或者Task.Run把數(shù)據(jù)讀出來解析成TrackPoint列表然后再通過Invoke回到 UI 線程繪制。否則界面會(huì)直接假死好幾秒。第二GMapControl有一個(gè)HoldInvalidation屬性批量更新 Marker 和 Route 的時(shí)候先設(shè)為true全部更新完再設(shè)回false并調(diào)用一次Refresh。這樣能避免每一幀的改變都觸發(fā)地圖重繪這個(gè)優(yōu)化在回放時(shí)非常明顯。gMapControl.HoldInvalidation true; // 批量更新多個(gè) marker / route gMapControl.HoldInvalidation false; gMapControl.Refresh();第三回放過程中不要每次都重新創(chuàng)建 Marker 對(duì)象。一個(gè) Marker 包含圖標(biāo)資源和命中測(cè)試邏輯創(chuàng)建和銷毀的成本不低。正確做法是提前創(chuàng)建好然后在 TimerTick 里只更新它的Position屬性。第四如果軌跡線確實(shí)非常長(zhǎng)可以考慮只繪制當(dāng)前視野范圍內(nèi)的點(diǎn)。每次地圖范圍改變時(shí)取gMapControl.ViewArea過濾掉不在范圍內(nèi)的點(diǎn)再繪制。這個(gè)做法能大幅減少繪制量但實(shí)現(xiàn)邏輯上要處理邊框處的線段切斷工作量略大多數(shù)項(xiàng)目做完抽稀之后就達(dá)不到這一步了。5. 常見問題與排查技巧實(shí)錄做軌跡回放的過程中我把遇到過的典型問題整理成了一個(gè)速查表這些問題在論壇上也經(jīng)常有人問放在一起對(duì)照著排查會(huì)快很多。問題現(xiàn)象根本原因解決辦法地圖顯示一片空白地圖源未正確初始化或在線地圖源請(qǐng)求失敗檢查網(wǎng)絡(luò)切換到 OpenStreetMap 等內(nèi)置源確認(rèn)MapProvider賦值軌跡線加載后視野里看不到?jīng)]有調(diào)用ZoomAndCenterMarkers或者調(diào)用時(shí)機(jī)在 Overlay 為空時(shí)加載完所有 Route 和 Marker 后再調(diào)用坐標(biāo)和底圖對(duì)不上偏差幾百米地圖源使用的是 GCJ-02 坐標(biāo)而 GPS 原始數(shù)據(jù)是 WGS-84 坐標(biāo)按地圖源做坐標(biāo)轉(zhuǎn)換統(tǒng)一坐標(biāo)系回放時(shí)地圖頻繁閃爍每幀都更新地圖中心或 Overlay導(dǎo)致重繪風(fēng)暴用HoldInvalidation只在超出視覺閾值時(shí)才跟隨移動(dòng)車輛圖標(biāo)方向錯(cuò)亂方向角計(jì)算時(shí)Atan2參數(shù)順序?qū)懛椿驔]有把弧度轉(zhuǎn)角度校準(zhǔn)公式先用兩個(gè)已知點(diǎn)驗(yàn)證結(jié)果進(jìn)度條拖動(dòng)時(shí)播放位置亂跳拖動(dòng)事件和 TimerTick 同時(shí)修改索引拖動(dòng)時(shí)先暫停播放松手后再恢復(fù)跨線程更新控件報(bào)錯(cuò)在后臺(tái)線程里直接操作了 GMapControl用Invoke或TaskScheduler.FromCurrentSynchronizationContext回到 UI 線程這里額外說兩個(gè)容易被忽略的坑。第一個(gè)是 GPS 漂移。設(shè)備靜止的時(shí)候GPS 坐標(biāo)會(huì)在原地小幅跳動(dòng)回放的時(shí)候車輛圖標(biāo)就會(huì)出現(xiàn)莫名其妙的小抖動(dòng)軌跡線也會(huì)變得毛刺。處理思路是靜止?fàn)顟B(tài)下速度小于某個(gè)閾值對(duì)坐標(biāo)做平滑或者直接把連續(xù)且距離小于閾值的冗余點(diǎn)過濾掉。我是在抽稀之前先做一層去噪效果很明顯。第二個(gè)是地圖源的選擇問題。GMap.NET 默認(rèn)能連的在線地圖源不同網(wǎng)絡(luò)環(huán)境下穩(wěn)定性差異很大有的源在國(guó)內(nèi)訪問時(shí)快時(shí)慢。我會(huì)在程序里做一個(gè)地圖源切換的功能把常用的幾個(gè)源做成菜單項(xiàng)用戶遇到地圖加載不出來時(shí)可以自己切不需要改代碼重新編譯。最后說一個(gè)我做這個(gè)功能時(shí)的體會(huì)。軌跡回放表面上是技術(shù)問題實(shí)際上更考驗(yàn)對(duì)業(yè)務(wù)場(chǎng)景的理解。比如客戶看回放不只是想看一條線畫出來他更關(guān)心的是這臺(tái)設(shè)備在某個(gè)時(shí)間點(diǎn)停在了哪里、在那里停了多久。所以在回放過程中點(diǎn)擊車輛圖標(biāo)彈出當(dāng)前點(diǎn)和路徑信息——時(shí)間、速度、停留時(shí)長(zhǎng)——這個(gè)小功能的價(jià)值比把動(dòng)畫做得多么順滑都要高。開發(fā)之前先把自己的軌跡數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)好把播放鏈路想清楚再去寫代碼后面基本不會(huì)出現(xiàn)推倒重來的情況。另外還有一個(gè)小技巧值得推薦回放的進(jìn)度條上可以順手標(biāo)注出停車點(diǎn)的刻度標(biāo)記。這樣用戶不用等播放到那些位置拖進(jìn)度條的時(shí)候一眼就能看到哪個(gè)時(shí)間段車輛是停著的。這個(gè)擴(kuò)展實(shí)現(xiàn)不難但對(duì)使用體驗(yàn)的提升立竿見影。本文還有配套的精品資源點(diǎn)擊獲取