一 前言
??近期翻閱博客,看到社區(qū)大神一休哥的一篇《canvas 奇巧淫技(二)繪制箭頭路徑效果》文章裸影,同樣柄粹,該大神還展示過一個(gè)使用rbush庫如何在前端快速從海量數(shù)據(jù)進(jìn)行空間檢索的案例:https://alex2wong.github.io/mapbox-plugins/examples/rbush/,很有分享精神的前端GIS專家昵宇,更多關(guān)于前端GIS檢索數(shù)據(jù)的技術(shù)可參考搜狐的干貨專訪:《深入理解空間搜索算法 ——數(shù)百萬數(shù)據(jù)中的瞬時(shí)搜索》逗宁。關(guān)于軌跡樣式帶導(dǎo)航箭頭這種常見問題柄慰,筆者基于興趣和朋友們的總結(jié),也試著用熟悉的OpenLayers的StyleFunction去實(shí)現(xiàn)一個(gè)這樣的玩具哩都,在此分享給大家魁兼。
??基于已知的一條軌跡,實(shí)現(xiàn)這樣的一個(gè)導(dǎo)航軌跡箭頭漠嵌,需要解決三個(gè)問題:
- 在軌跡上根據(jù)固定像素間隔咐汞,計(jì)算當(dāng)前地圖分辨率下箭頭總數(shù)量。
- 計(jì)算當(dāng)前地圖分辨率下儒鹿,每個(gè)箭頭的繪制位置化撕。
- 計(jì)算好箭頭的數(shù)量和位置后,要確定箭頭的方向约炎。
一 箭頭數(shù)量
??由高德軌跡箭頭圖可知植阴,每隔固定像素,打上一個(gè)箭頭圾浅。假設(shè)當(dāng)前的線LineString地理長度為length掠手,當(dāng)前固定像素間隔stpes=n像素,在當(dāng)前地圖比例尺res已知的情況下狸捕,n像素地理距離是resn喷鸽,那么箭頭總數(shù)count=length/(resn):
let length=line_geom.getLength();//線圖形的地理長度
const steps=40;//每隔40像素打一個(gè)箭頭點(diǎn)
let geo_steps=map_res*steps;//40像素長度在當(dāng)前地圖比例尺下地理長度。
let arrow_count=length*1.0/geo_steps;
多么淺顯易懂的道理啊灸拍,第一個(gè)問題很順利的解決了做祝。
二 箭頭位置
??第一步得到了箭頭的總數(shù),在獲取箭頭位置時(shí)鸡岗,一個(gè)重要的API是線條LineString的getCoordinateAt混槐,利用它我們在軌跡線上獲取箭頭點(diǎn)的位置。
/*
fraction:參考點(diǎn)的百分比轩性,如0就是LineString的起點(diǎn)声登,1就是LineString的終點(diǎn),0.5就是LineString的中點(diǎn)。
*/
linestring.getCoordinateAt(fraction, opt_dest)
??假如箭頭總數(shù)為arrowsNum捌刮,那么arrowsNum個(gè)箭頭的數(shù)量分別是
for(let i=1;i<arrowsNum;i++){
let arraw_coor=geometry.getCoordinateAt(i*1.0/arrowsNum);
console.log(arraw_coor);//輸出每個(gè)箭頭的坐標(biāo)
}
??得到每個(gè)箭頭的位置后,我們先可視化下吧舒岸,OpenLayers的地圖樣式完全由StyleFunction實(shí)現(xiàn)的绅作,完整樣式代碼如下:
/*
feature:地圖上的要素對象,既有屬性蛾派,也有坐標(biāo)圖形俄认。
res:當(dāng)前地圖分辨率參數(shù)。
return:返回一個(gè)定制的渲染樣式
*/
var styleFunction = function(feature,res){
//軌跡線圖形
var trackLine= feature.getGeometry();
var styles = [
new ol.style.Style({
stroke: new ol.style.Stroke({
color: '#2E8B57',
width: 10
})
})
];
//軌跡地理長度
let length=trackLine.getLength();
//像素間隔步長
let stpes=40;//像素步長間隔
//將像素步長轉(zhuǎn)實(shí)際地理距離步長
let geo_steps=stpes*res;
//箭頭總數(shù)
let arrowsNum=parseInt(length/geo_steps);
for(let i=1;i<arrowsNum;i++){
let arraw_coor=trackLine.getCoordinateAt(i*1.0/arrowsNum);
styles.push(new ol.style.Style({
geometry: new ol.geom.Point(arraw_coor),
image: new ol.style.Circle({
radius: 7,
fill: new ol.style.Fill({
color: '#ffcc33'
})
})
}));
}
return styles;
}
三 箭頭方向
??之前的邏輯洪乍,我們已經(jīng)計(jì)算了一個(gè)軌跡樣式的雛形了眯杏,把地圖上箭頭位置的黃點(diǎn)改成一個(gè)箭頭圖標(biāo),做下方向旋轉(zhuǎn)就可以了壳澳。在說明此前岂贩,需要說明下軌跡,segment線段巷波,箭頭點(diǎn)之間的關(guān)系萎津,如下圖:
??觀察示意圖抹镊,總結(jié)如下:
- 一條完整的軌跡由多個(gè)連續(xù)的segment組成锉屈。
- 通過getCoordinateAt方法計(jì)算得到的箭頭點(diǎn),一定是在軌跡線上的某個(gè)點(diǎn)垮耳。
- 每個(gè)箭頭點(diǎn)的方向是由箭頭點(diǎn)落在的segment的方向決定的颈渊。
很顯然,計(jì)算箭頭方向其實(shí)就是計(jì)算每個(gè)箭頭點(diǎn)到底落在了哪個(gè)segment上终佛,將segme方向賦予箭頭點(diǎn)俊嗽。這里我們引入了rbush庫構(gòu)建空間索引,計(jì)算軌跡點(diǎn)與segment對應(yīng)關(guān)系铃彰。
??之所以我要引入rbush庫乌询,是解決循環(huán)計(jì)算問題,想象下如果不引入rbush庫豌研,只能使用如下的偽代碼暴力計(jì)算了:
for(let i=0;i<arrows.length;i++){
for(let j=0;j<segments.length;j++){
if(instersects(arrows[i],segments[j])===true){
// arrows[i]對應(yīng)的segments是segments[j]
break;
}
}
}
感覺邏輯很簡單啊妹田,這樣做難道不可以嗎?想象下鹃共,箭頭數(shù)量鬼佣,segment的數(shù)量其實(shí)都是不可控的,一個(gè)復(fù)雜的軌跡線可能由成百上千的近萬的segments霜浴,這樣一個(gè)個(gè)循環(huán)去匹配晶衷,效率是不是就有問題了?所以引入了空間索引。這里查詢晌纫,使用了rbush進(jìn)行btree查詢税迷,查詢的結(jié)果后再詳細(xì)比對是否和箭頭相交,累了锹漱,直接貼代碼了箭养,不詳述了:
var styleFunction = function(feature,res){
//軌跡線圖形
var trackLine= feature.getGeometry();
var styles = [
new ol.style.Style({
stroke: new ol.style.Stroke({
color: '#2E8B57',
width: 10
})
})
];
//對segments建立btree索引
let tree= rbush();//路段數(shù)
trackLine.forEachSegment(function(start, end) {
var dx = end[0] - start[0];
var dy = end[1] - start[1];
//計(jì)算每個(gè)segment的方向,即箭頭旋轉(zhuǎn)方向
let rotation = Math.atan2(dy, dx);
let geom=new ol.geom.LineString([start,end]);
let extent=geom.getExtent();
var item = {
minX: extent[0],
minY: extent[1],
maxX: extent[2],
maxY: extent[3],
geom: geom,
rotation:rotation
};
tree.insert(item);
});
//軌跡地理長度
let length=trackLine.getLength();
//像素間隔步長
let stpes=40;//像素步長間隔
//將像素步長轉(zhuǎn)實(shí)際地理距離步長
let geo_steps=stpes*res;
//箭頭總數(shù)
let arrowsNum=parseInt(length/geo_steps);
for(let i=1;i<arrowsNum;i++){
let arraw_coor=trackLine.getCoordinateAt(i*1.0/arrowsNum);
let tol=10;//查詢設(shè)置的點(diǎn)的容差哥牍,測試地圖單位是米毕泌。如果是4326坐標(biāo)系單位為度的話,改成0.0001.
let arraw_coor_buffer=[arraw_coor[0]-tol,arraw_coor[1]-tol,arraw_coor[0]+tol,arraw_coor[1]+tol];
//進(jìn)行btree查詢
var treeSearch = tree.search({
minX: arraw_coor_buffer[0],
minY: arraw_coor_buffer[1],
maxX: arraw_coor_buffer[2],
maxY: arraw_coor_buffer[3]
});
let arrow_rotation;
//只查詢一個(gè)嗅辣,那么肯定是它了撼泛,直接返回
if(treeSearch.length==1)
arrow_rotation=treeSearch[0].rotation;
else if(treeSearch.length>1){
let results=treeSearch.filter(function(item){
//箭頭點(diǎn)與segment相交,返回結(jié)果澡谭。該方法實(shí)測不是很準(zhǔn)愿题,可能是計(jì)算中間結(jié)果
//保存到小數(shù)精度導(dǎo)致查詢有點(diǎn)問題
// if(item.geom.intersectsCoordinate(arraw_coor))
// return true;
//換一種方案,設(shè)置一個(gè)稍小的容差蛙奖,消除精度問題
let _tol=1;//消除精度誤差的容差
if(item.geom.intersectsExtent([arraw_coor[0]-_tol,arraw_coor[1]-_tol,arraw_coor[0]+_tol,arraw_coor[1]+_tol]))
return true;
})
if(results.length>0)
arrow_rotation=results[0].rotation;
}
styles.push(new ol.style.Style({
geometry: new ol.geom.Point(arraw_coor),
image: new ol.style.Icon({
src: '../static/content/images/arrowright.png',
anchor: [0.75, 0.5],
rotateWithView: true,
rotation: -arrow_rotation
})
}));
}
return styles;
}
看著還湊合吧抠忘,但其實(shí)要做到高德那個(gè)精細(xì)的樣式,才萬里第一步外永,祝諸君繼續(xù)研究崎脉,期待更好的效果。