個人博客:CSDN 博客-滿分觀察網友 z
演示地址:嗶哩嗶哩-滿分觀察網友 z
這是一個用 Cesium.js 做的公交車軌跡漫游,實現的功能有加載站點和道路軌跡點數據、監聽車輛的實時位置、車輛控制器。滾動屏等等。
文章目錄
- 1. 地圖初始化
- 2. 數據渲染
- 2.1. 軌跡點
- 2.2. 車牌
- 2.3. 站牌標注
- 3. 計算時間
- 3.1. 時間總和
- 3.2. 系統時間賦值
- 3.3. 采樣位置
- 3.4. 加載公交車
- 3.5. 氣泡窗跟隨
- 4. 面板展示
- 4.1. 起點與目的地
- 4.2. 站牌名字
- 4.3. 車輛控制器
- 4.4. 視角切換
- 4.5. 后端數據模擬
- 4.6. 調整速度
- 4.7. 滾動屏
- 5. 事件監聽器
- 6. 移除事件
1. 地圖初始化
加載高德地圖。
const layer = new Cesium.UrlTemplateImageryProvider({url: "http://webrd02.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}",minimumLevel: 4,maximumLevel: 18,
});
對高德地圖進行濾色處理。
onMounted(() => {// viewer是操控地圖api的開始viewer = new Cesium.Viewer("cesiumContainer", {imageryProvider: layer,baseLayerPicker: false, //是否顯示圖層控件animation: false, //是否顯示動畫控件timeline: false, //是否顯示時間軸控件fullscreenButton: false, //是否顯示全屏控件geocoder: false, //是否顯示搜索控件homeButton: false, //是否顯示主頁按鈕navigationHelpButton: false, //是否顯示幫助提示按鈕sceneModePicker: false, //是否顯示投影方式控件shouldAnimate: true, // 是否自動播放selectionIndicator: false, //隱藏選中框infoBox: false, //隱藏右上角信息框shadows: true,});// 設置濾色modifyMap(viewer, {invertColor: true,filterRGB: [70, 110, 120],});initData();
});
load.json 道路數據分析
front_name:公交站起點
terminal_name:公交站終點
xs,ys:是軌跡點的坐標經緯度,需要進行處理
start_time:開始時間
end_time:結束時間
length:路程(公里)
stations:每個站點
- name:站點的名字
- xy_coords:站點的坐標
2. 數據渲染
2.1. 軌跡點
首先處理每個軌跡點的坐標,它們的經緯度坐標分開,需要拼接起來,使用 forEach 循環進行拼接,這里只需要循環一次即可,因為經度和緯度的坐標數組都是一樣長的,另一個可以根據循環的索引獲得。再將經緯度坐標轉為笛卡爾坐標,最后推入一個數組中保存。
let xArr = loadData.xs.split(",");
let yArr = loadData.ys.split(",");
xArr.forEach((item, index) => {positions.push(Cesium.Cartesian3.fromDegrees(Number(item), Number(yArr[index])));xyArr.push([Number(item), Number(yArr[index])]);
});
將軌跡點數據渲染到頁面上。
const line = viewer.entities.add({polyline: {positions,width: 10,material: Cesium.Color.WHITE.withAlpha(0.8),clampGround: true,},
});
2.2. 車牌
處理每個車牌的數據,先遍歷 loadData.stations,再獲得里面 xy_coords 數據,處理完后轉為笛卡爾坐標
// 獲得站牌坐標 并轉為笛卡爾坐標
let position = Cesium.Cartesian3.fromDegrees(...item.xy_coords.split(";").map((a) => Number(a))
);
加載對應的模型,,并改變站牌的大小和方向,進來的時候可以對著我們。
viewer.entities.add({position,orientation,model: {uri: "/src/assets/model.gltf",scale: 0.07,minimumPixelSize: 10,},
});
// 公交車牌的朝向
let orientation = Cesium.Transforms.headingPitchRollQuaternion(position,Cesium.HeadingPitchRoll.fromDegrees(90, 0, 0)
);
2.3. 站牌標注
展示站牌的標注,它的坐標跟站牌一樣,但是高度卻不一樣,需要額外進行處理。
let positionLabel = Cesium.Cartesian3.fromDegrees(...item.xy_coords.split(";").map((a) => Number(a)),24
);
添加站牌標注到地圖上。
viewer.entities.add({position: positionLabel,id: "label" + index,label: {text: item.name,font: "10px Helvetica",style: Cesium.LabelStyle.FILL_AND_OUTLINE,fillColor: Cesium.Color.WHITE, //字體顏色backgroundColor: Cesium.Color.BLACK.withAlpha(0.5), //背景顏色showBackground: true, //是否顯示背景顏色},
});
3. 計算時間
獲得開始的時間和結束時間,開始時間可以隨便給,根據公式 s=vt,s 保持不變,v 可以自己設定,這樣就可以計算得到結束的時間了。
3.1. 時間總和
這里封裝一個數據來計算時間總數和每個軌跡點對應的系統時間,它接受兩個參數:pArr(軌跡點數組)和 speed(速度)。函數的實現原理是計算軌跡點之間的距離,利用 Cesium 自帶的distance
計算,并將這些距離除以速度以得到每個軌跡點所對應的時間。最后,函數返回一個包含時間總和每個軌跡點對應的系統時間。
const getSiteTimes = (pArr, speed) => {let timeSum = 0,times = []; //timeSum花費時間總和,每一個軌跡點對應的時間for (let i = 0; i < pArr.length; i++) {if (i == 0) {times.push(0);continue;}// 計算時間總數timeSum += spaceDistance(pArr[i - 1], pArr[i]) / speed;// 每個軌跡點所對應的系統時間times.push(timeSum);}return { timeSum: timeSum, siteTime: times };
};// 計算兩點的距離
const spaceDistance = (a, b) => {return Cesium.Cartesian3.distance(a, b).toFixed(2);
};
設置速度為 20,可以獲得時間總和每個軌跡點對應的系統時間。
3.2. 系統時間賦值
這時可以設置開始時間和結束時間。
// 設置時間邊界
const start = Cesium.JulianDate.fromDate(new Date(2015, 1, 1, 11));
const stop = Cesium.JulianDate.addSeconds(start,timeObj.timeSum,new Cesium.JulianDate()
);
光是拿到時間沒有用,需要給系統時間賦值。
// 設置時間段
viewer.clock.startTime = start.clone();
viewer.clock.stopTime = stop.clone();
viewer.clock.currentTime = start.clone(); //當前時間
3.3. 采樣位置
定義了一個名為 getSampleData 的函數,該函數接受三個參數:pArr(軌跡點數組)、start(開始時間)和 siteTime(時間戳數組)。函數的實現原理是將軌跡點數組 pArr 中的每個點按照時間戳數組 siteTime 中的對應時間添加到 position 對象中,從而形成一個采樣位置屬性。也就是將軌跡點數據轉換為 Cesium 可以理解的格式,從而在地圖上繪制軌跡。
為什么要傳入siteTime時間節點傳過來呢?
因為我們算出的時間節點都是從 0 開始算的,但是我們要把時間換算到系統里面去,應該是開始的時間加上時間節點。
這個案例最重點的地方:計算每個軌跡點對應的系統時間
for (let i = 0; i < pArr.length; i++) {//每一個軌跡點所對應的系統時間const time = Cesium.JulianDate.addSeconds(start,siteTime[i],new Cesium.JulianDate());position.addSample(time, pArr[i]);
}
console.log(position);
如果能在_property._values 中拿到 996 個坐標以及 332 個所對應的系統時間,那就是沒問題了,因為笛卡爾是三個坐標。
3.4. 加載公交車
根據實體的速度來計算其朝向。
orientation: new Cesium.VelocityOrientationProperty(position);
將創建的實體設置為視圖的跟蹤實體。跟蹤實體是視圖中的一個實體,當我們在視圖中移動時,它會自動跟隨我們的位置。
viewer.trackedEntity = entity;
如何使公交車在站牌停下?
- 需要監聽時鐘
- 將當前的公交車坐標和站牌的坐標做比較
- 獲取它們之間的距離,在一定范圍內,則停止
// 監聽公交車的運動
viewer.clock.onTick.addEventListener(tickEventHandler);
// 獲取當前的時間公交車的坐標
let startPosition = entity.position.getValue(viewer.clock.currentTime);
// console.log(a);
// 獲取站牌的坐標 比較第二個站點
let endPosition = Cesium.Cartesian3.fromDegrees(...loadData.stations[data.index + 1].xy_coords.split(";").map((a) => Number(a))
);// 得到他們之間的距離
let distance = spaceDistance(startPosition, endPosition);
這里的距離應該是車的中心點到站牌的距離
如果小于一定的距離,就讓公交車停下來,也就是 viewer.clock.shouldAnimate = false;
3.5. 氣泡窗跟隨
獲取當前的坐標,將氣泡窗的位置進行更新。
4. 面板展示
第一欄是起點與目的地,第二欄是編號和站牌名字,第三欄是車輛控制器:暫停,減速,加速,重置,跟隨視角,車內視角,自由視角,速度以及進度條。
4.1. 起點與目的地
獲取站牌的名稱,包括起點和目的地。
data.fromName = loadData.stations[data.index].name;
data.toName = loadData.stations[data.index + 1].name;
4.2. 站牌名字
加載站牌名字到面板上,這里有一些動畫效果,到站時,展示面板也要高亮顯示,并挪動圖標。
if (data.index >= 12 && data.index < 24) {img.value.style.top = "195px";img.value.style.left = 17 + (data.index - 12) * 32.85 + "px";
} else if (data.index >= 24) {img.value.style.top = "385px";img.value.style.left = 17 + (data.index - 24) * 32.85 + "px";
} else {img.value.style.left = 17 + data.index * 32.85 + "px";
}
點擊站牌,也可以實現跳轉到相應的站牌。
// 跳轉到站牌
const toStation = (index) => {changeView("free");if (pickLabel) {pickLabel.label.fillColor = Cesium.Color.WHITE;}pickLabel = viewer.entities.getById("label" + index);// console.log(pickLabel);pickLabel.label.fillColor = Cesium.Color.YELLOW;viewer.flyTo(pickLabel);
};
4.3. 車輛控制器
播放按鈕,設置播放狀態,點擊后播放,變為暫停按鈕,再次點擊,變為播放按鈕。
if (!isBegin) {isBegin = true;toBegin();
}
data.control.play = true;
viewer.clock.shouldAnimate = true;
暫停就是把時間進行暫停。
viewer.clock.shouldAnimate = false;
重播時間就是讓系統的當前時間再等于一下系統的開始時間,圖標,進度條,站牌都還原。
img.value.style.top = "10px";
img.value.style.left = "17px";
data.control.percentage = 0;
data.index = 0;
viewer.clock.currentTime = viewer.clock.startTime;
data.fromName = loadData.stations[0].name;
data.toName = loadData.stations[1].name;
公交車的進度條,根據 s=vt,我們需要計算的是時間,用當前花費的時間除以總時間,可以得到進度條,花費的時間可以用當前時間戳減去開始時間戳。
4.4. 視角切換
自由視角就是取消跟隨視角 viewer.trackedEntity = null;
和擺脫觀察者模式 viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY);
。
車內視角就是行駛時更新相機的朝向,使其始終朝向前方。
根據車內的坐標系,可以確定相機放的位置。相機的位置不能寫(0,0,0),不能跟放的東西是同一個點,會報錯的。
viewer.camera.lookAtTransform(transform, new Cesium.Cartesian3(-0.001, 0, 0)); //將相機向后面放一點
不管你如何修改上面的三個值,你都無法實現車內的視角,原因是我們所看向的點永遠在車底,我們拿到的實時坐標是沒有高度的,永遠是 0。我們應該要把中心點提一下,x 軸往后一點,這樣就可以實現了。
// 改變視角 公交車內
let cartographic = Cesium.Cartographic.fromCartesian(startPosition);
let lon = Cesium.Math.toDegrees(cartographic.longitude);
let lat = Cesium.Math.toDegrees(cartographic.latitude);
let newPosition = Cesium.Cartesian3.fromDegrees(lon, lat, 2);
if (isInCar) {viewer.trackedEntity = null;var ori = entity.orientation.getValue(viewer.clock.currentTime); //獲取偏向角// var center = entity.position.getValue(viewer.clock.currentTime); //獲取位置var transform = Cesium.Matrix4.fromRotationTranslation(Cesium.Matrix3.fromQuaternion(ori),newPosition); //將偏向角轉為3*3矩陣,利用實時點位轉為4*4矩陣viewer.camera.lookAtTransform(transform, new Cesium.Cartesian3(-0.001, 0, 0)); //將相機向后面放一點
}
車內視角參考:cesium 巡邏獲取對象實時信息并實現切換第一人稱視角
4.5. 后端數據模擬
模擬后端給你一個點,從這個點開始跑,[114.406979, 30.462812]
。
第一個思路:從所有的軌跡點鐘中選出后端的點,但是后端提供的點不一定在軌跡點上,也有可能有軌跡點的連線上。
第二個思路:算出后端給的點跑了多久,就當時間給回系統時間。可以用該點到起始點的距離除以總的路程,再乘以時間綜合。
使用 turf 庫的線段截取,獲取一條線、起點和終點,并返回這些點之間的線段。起止點不需要正好落在直線上。注意都是 geojson 數據!
let xyArr = [];
let xArr = loadData.xs.split(",");
let yArr = loadData.ys.split(",");
xArr.forEach((item, index) => {positions.push(Cesium.Cartesian3.fromDegrees(Number(item), Number(yArr[index])));xyArr.push([Number(item), Number(yArr[index])]);
});
lineGeoJson = turf.lineString(xyArr);
const start = turf.point(lineGeoJson.geometry.coordinates[0]); //起點
const stop = turf.point([114.406979, 30.462812]); //終點
const sliced = turf.lineSlice(start, stop, lineGeoJson); //線
// console.log(sliced);
計算截取的長度/總長度的百分比,并求出相應的時間
return turf.length(sliced) / turf.length(lineGeoJson);
const proportion = getProportion(); //getProportion是返回截取的長度/總長度的百分比
const newTime = Cesium.JulianDate.addSeconds(start,timeObj.timeSum * proportion,new Cesium.JulianDate()
);
同時還要考慮公交車是否到站了
第一個思路;,后端的點需要跟所有的站點進行比較,拿到最小的距離。但是當線路是 U 型彎回來的時候,如果馬路對面的站點比較近,就和實際效果不一樣。
第二個思路:遍歷所有的站點到起始點的距離,用來跟截取的線段長度做比較,如果截取的線段長度小于站點到起始點線段長度,說明找到了更接近的站點,將當前點的索引(i)賦值給 index。
// 截取站牌長度比較
loadData.stations.forEach((item, i) => {const stationsStop = item.xy_coords.split(";").map((a) => Number(a));const stationsSliced = turf.lineSlice(start, stationsStop, lineGeoJson);// console.log(stationsSliced);if (turf.length(stationsSliced) < turf.length(sliced)) {index = i;}
});
4.6. 調整速度
根據 s=vt,s 不變,如果要更改 v 的話,其他都會發生改變,只有當時間變快時,速度就可以變得快。
4.7. 滾動屏
根據公交車與站牌的距離,顯示滾動屏的內容,距離小于一定的值,就會顯示前方到站 xxx,到了站點起步,就會顯示下一站 xxx,然后過一會時間就會恢復初始的內容。
5. 事件監聽器
當監聽網頁切走后,公交車將會暫停。
// 監聽用戶是否離開頁面 離開 公交車停止運動
document.addEventListener("visibilitychange", function () {if (document.hidden) {viewer.clock.shouldAnimate = false;data.control.play = false;}
});
6. 移除事件
當公交車到達終點后,會清除所有的事件。
const removeEvent = () => {bubble.windowClose();viewer.clock.onTick.removeEventListener(tickEventHandler);viewer.entities.remove(entity);changeView("free");isBegin = false;data.control.play = false;viewer.clock.shouldAnimate = false;
};
也會彈出到達終點。
if (data.index == loadData.stations.length - 1) {alert("We are arrive the terminal.乘客們,感謝您一路對我們工作理解和支持的,下車前請檢查一下自己的行李物品,請不要遺忘在車廂內。");removeEvent();return;
}
參考案例:Cesium 官方車輛軌跡漫游