23 changed files with 605 additions and 109 deletions
@ -0,0 +1,360 @@ |
|||||
|
<!doctype html> |
||||
|
<html lang="zh"> |
||||
|
<head> |
||||
|
<meta charset="utf-8" /> |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
||||
|
<title>轨迹点实现</title> |
||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ol/ol.css" /> |
||||
|
<style> |
||||
|
#map { |
||||
|
width: 100%; |
||||
|
height: 500px; |
||||
|
} |
||||
|
#controls { |
||||
|
padding: 10px; |
||||
|
background: #f5f5f5; |
||||
|
} |
||||
|
#info-panel { |
||||
|
padding: 10px; |
||||
|
background: white; |
||||
|
border: 1px solid #ddd; |
||||
|
} |
||||
|
.timeline { |
||||
|
display: flex; |
||||
|
overflow-x: auto; |
||||
|
padding: 10px 0; |
||||
|
} |
||||
|
.timeline-marker { |
||||
|
padding: 5px 10px; |
||||
|
margin: 0 5px; |
||||
|
background: #1890ff; |
||||
|
color: white; |
||||
|
border-radius: 3px; |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
</style> |
||||
|
</head> |
||||
|
<body> |
||||
|
<div id="controls"> |
||||
|
<button id="btn-play">播放轨迹</button> |
||||
|
<button id="btn-pause">暂停</button> |
||||
|
<div id="info-panel">轨迹信息将显示在这里</div> |
||||
|
</div> |
||||
|
<div id="map"></div> |
||||
|
<div class="timeline" id="timeline"></div> |
||||
|
<script src="https://unpkg.com/ol@9.2.4/dist/ol.js"></script> |
||||
|
<script> |
||||
|
// 轨迹数据(示例数据 - 北京到天津的轨迹) |
||||
|
const trackData = [ |
||||
|
{ time: '09:00', lng: 116.404, lat: 39.915, speed: 60, name: '起点' }, |
||||
|
{ time: '09:15', lng: 116.704, lat: 39.915, speed: 65, name: '大兴区' }, |
||||
|
{ time: '09:30', lng: 116.904, lat: 39.815, speed: 70, name: '廊坊' }, |
||||
|
{ time: '09:45', lng: 117.204, lat: 39.715, speed: 75, name: '武清区' }, |
||||
|
{ time: '10:00', lng: 117.404, lat: 39.615, speed: 80, name: '天津市区' } |
||||
|
] |
||||
|
|
||||
|
// 全局变量 |
||||
|
let map, trackSource, trackPlayer |
||||
|
|
||||
|
// 初始化地图 |
||||
|
function initMap() { |
||||
|
const source = new ol.source.XYZ({ |
||||
|
url: 'http://qtbj.icpcdev.site/roadmap/{z}/{x}/{y}.png', |
||||
|
maxZoom: 16, |
||||
|
minZoom: 10 |
||||
|
}) |
||||
|
|
||||
|
map = new ol.Map({ |
||||
|
target: 'map', |
||||
|
layers: [ |
||||
|
new ol.layer.Tile({ |
||||
|
source: source |
||||
|
}) |
||||
|
], |
||||
|
view: new ol.View({ |
||||
|
center: ol.proj.fromLonLat([116.404, 39.915]), |
||||
|
zoom: 10 |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
// 创建轨迹图层 |
||||
|
trackSource = new ol.source.Vector() |
||||
|
const trackLayer = new ol.layer.Vector({ |
||||
|
source: trackSource, |
||||
|
style: function (feature) { |
||||
|
const type = feature.get('type') |
||||
|
if (type === 'track-line') { |
||||
|
return new ol.style.Style({ |
||||
|
stroke: new ol.style.Stroke({ |
||||
|
color: '#1890FF', |
||||
|
width: 4, |
||||
|
lineDash: [5, 5] |
||||
|
}) |
||||
|
}) |
||||
|
} else if (type === 'track-point') { |
||||
|
const speed = feature.get('speed') |
||||
|
return new ol.style.Style({ |
||||
|
image: new ol.style.Circle({ |
||||
|
radius: 6, |
||||
|
fill: new ol.style.Fill({ |
||||
|
color: speed > 70 ? '#FF4D4F' : '#52C41A' |
||||
|
}), |
||||
|
stroke: new ol.style.Stroke({ |
||||
|
color: 'white', |
||||
|
width: 2 |
||||
|
}) |
||||
|
}), |
||||
|
text: new ol.style.Text({ |
||||
|
text: feature.get('name') || feature.get('time'), |
||||
|
offsetY: -15, |
||||
|
fill: new ol.style.Fill({ color: '#000' }), |
||||
|
stroke: new ol.style.Stroke({ color: '#fff', width: 3 }) |
||||
|
}) |
||||
|
}) |
||||
|
} else if (type === 'moving-point') { |
||||
|
return new ol.style.Style({ |
||||
|
image: new ol.style.Circle({ |
||||
|
radius: 10, |
||||
|
fill: new ol.style.Fill({ color: '#FAAD14' }), |
||||
|
stroke: new ol.style.Stroke({ |
||||
|
color: 'white', |
||||
|
width: 3 |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
map.addLayer(trackLayer) |
||||
|
} |
||||
|
|
||||
|
// 轨迹播放器类 |
||||
|
class TrackPlayer { |
||||
|
constructor(trackData, source, map) { |
||||
|
this.trackData = trackData |
||||
|
this.source = source |
||||
|
this.map = map |
||||
|
this.currentIndex = 0 |
||||
|
this.isPlaying = false |
||||
|
this.animationId = null |
||||
|
this.movingPoint = null |
||||
|
|
||||
|
this.initMovingPoint() |
||||
|
this.createTimeline() |
||||
|
} |
||||
|
|
||||
|
// 初始化移动点 |
||||
|
initMovingPoint() { |
||||
|
this.movingPoint = new ol.Feature({ |
||||
|
geometry: new ol.geom.Point( |
||||
|
ol.proj.fromLonLat([this.trackData[0].lng, this.trackData[0].lat]) |
||||
|
), |
||||
|
type: 'moving-point' |
||||
|
}) |
||||
|
this.source.addFeature(this.movingPoint) |
||||
|
} |
||||
|
|
||||
|
// 创建时间轴 |
||||
|
createTimeline() { |
||||
|
const timeline = document.getElementById('timeline') |
||||
|
timeline.innerHTML = '' |
||||
|
|
||||
|
this.trackData.forEach((point, index) => { |
||||
|
const pointElement = document.createElement('div') |
||||
|
pointElement.className = 'timeline-point' |
||||
|
pointElement.innerHTML = ` |
||||
|
<div>${point.time}</div> |
||||
|
<div>${point.name}</div> |
||||
|
` |
||||
|
pointElement.addEventListener('click', () => { |
||||
|
this.jumpToPoint(index) |
||||
|
}) |
||||
|
timeline.appendChild(pointElement) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// 播放轨迹 |
||||
|
play() { |
||||
|
if (this.isPlaying) return |
||||
|
this.isPlaying = true |
||||
|
this.currentIndex = 0 |
||||
|
this.updateTimeline() |
||||
|
this.animate() |
||||
|
} |
||||
|
|
||||
|
// 暂停播放 |
||||
|
pause() { |
||||
|
this.isPlaying = false |
||||
|
if (this.animationId) { |
||||
|
cancelAnimationFrame(this.animationId) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 跳转到指定点 |
||||
|
jumpToPoint(index) { |
||||
|
this.pause() |
||||
|
this.currentIndex = Math.max(0, Math.min(index, this.trackData.length - 1)) |
||||
|
this.updateMovingPoint() |
||||
|
this.updateTimeline() |
||||
|
} |
||||
|
|
||||
|
// 动画循环 |
||||
|
animate() { |
||||
|
if (!this.isPlaying || this.currentIndex >= this.trackData.length - 1) { |
||||
|
this.isPlaying = false |
||||
|
this.updateInfo('轨迹播放完成') |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
const start = this.trackData[this.currentIndex] |
||||
|
const end = this.trackData[this.currentIndex + 1] |
||||
|
this.moveToNextPoint(start, end, 0, 1500) // 1.5秒移动到下一点 |
||||
|
} |
||||
|
|
||||
|
// 移动到下一点 |
||||
|
moveToNextPoint(start, end, progress, duration) { |
||||
|
if (!this.isPlaying) return |
||||
|
|
||||
|
if (progress >= 1) { |
||||
|
this.currentIndex++ |
||||
|
this.updateTimeline() |
||||
|
this.animate() |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// 计算插值坐标 |
||||
|
const lng = start.lng + (end.lng - start.lng) * progress |
||||
|
const lat = start.lat + (end.lat - start.lat) * progress |
||||
|
|
||||
|
// 更新移动点位置 |
||||
|
this.movingPoint.getGeometry().setCoordinates(ol.proj.fromLonLat([lng, lat])) |
||||
|
|
||||
|
// 更新信息面板 |
||||
|
this.updateInfo(end, progress) |
||||
|
|
||||
|
// 平滑跟随移动点 |
||||
|
if (progress > 0.3) { |
||||
|
// 延迟一点开始跟随 |
||||
|
this.map.getView().animate({ |
||||
|
center: ol.proj.fromLonLat([lng, lat]), |
||||
|
duration: 200, |
||||
|
easing: ol.easing.linear |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// 继续动画 |
||||
|
progress += 16 / duration // 基于60fps |
||||
|
this.animationId = requestAnimationFrame(() => { |
||||
|
this.moveToNextPoint(start, end, progress, duration) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// 更新移动点位置(直接跳转) |
||||
|
updateMovingPoint() { |
||||
|
const point = this.trackData[this.currentIndex] |
||||
|
this.movingPoint.getGeometry().setCoordinates(ol.proj.fromLonLat([point.lng, point.lat])) |
||||
|
|
||||
|
this.map.getView().animate({ |
||||
|
center: ol.proj.fromLonLat([point.lng, point.lat]), |
||||
|
zoom: 14, |
||||
|
duration: 500 |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// 更新信息面板 |
||||
|
updateInfo(point, progress) { |
||||
|
const progressText = progress ? `进度: ${Math.round(progress * 100)}%` : '' |
||||
|
document.getElementById('info-panel').innerHTML = ` |
||||
|
<div><strong>当前位置:</strong> ${point.name}</div> |
||||
|
<div><strong>时间:</strong> ${point.time}</div> |
||||
|
<div><strong>速度:</strong> ${point.speed} km/h</div> |
||||
|
<div><strong>${progressText}</strong></div> |
||||
|
` |
||||
|
} |
||||
|
|
||||
|
// 更新时间轴高亮 |
||||
|
updateTimeline() { |
||||
|
const points = document.querySelectorAll('.timeline-point') |
||||
|
points.forEach((point, index) => { |
||||
|
point.classList.toggle('active', index === this.currentIndex) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// 重置轨迹 |
||||
|
reset() { |
||||
|
this.pause() |
||||
|
this.currentIndex = 0 |
||||
|
this.updateMovingPoint() |
||||
|
this.updateTimeline() |
||||
|
this.updateInfo(this.trackData[0], 0) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 添加轨迹到地图 |
||||
|
function addTrackToMap() { |
||||
|
// 清空现有轨迹 |
||||
|
trackSource.clear() |
||||
|
|
||||
|
// 添加轨迹点 |
||||
|
trackData.forEach((point, index) => { |
||||
|
const feature = new ol.Feature({ |
||||
|
geometry: new ol.geom.Point(ol.proj.fromLonLat([point.lng, point.lat])), |
||||
|
type: 'track-point', |
||||
|
time: point.time, |
||||
|
speed: point.speed, |
||||
|
name: point.name, |
||||
|
index: index |
||||
|
}) |
||||
|
trackSource.addFeature(feature) |
||||
|
}) |
||||
|
|
||||
|
// 添加轨迹线 |
||||
|
const lineCoords = trackData.map((p) => ol.proj.fromLonLat([p.lng, p.lat])) |
||||
|
const lineFeature = new ol.Feature({ |
||||
|
geometry: new ol.geom.LineString(lineCoords), |
||||
|
type: 'track-line' |
||||
|
}) |
||||
|
trackSource.addFeature(lineFeature) |
||||
|
|
||||
|
// 自适应缩放 |
||||
|
const extent = trackSource.getExtent() |
||||
|
map.getView().fit(extent, { |
||||
|
padding: [50, 50, 50, 50], |
||||
|
duration: 1000 |
||||
|
}) |
||||
|
|
||||
|
// 初始化播放器 |
||||
|
trackPlayer = new TrackPlayer(trackData, trackSource, map) |
||||
|
} |
||||
|
|
||||
|
// 页面加载完成后初始化 |
||||
|
document.addEventListener('DOMContentLoaded', function () { |
||||
|
// 初始化地图 |
||||
|
initMap() |
||||
|
|
||||
|
// 绑定按钮事件 |
||||
|
// document.getElementById('btn-add-track').addEventListener('click', addTrackToMap); |
||||
|
document.getElementById('btn-play').addEventListener('click', function () { |
||||
|
if (trackPlayer) trackPlayer.play() |
||||
|
}) |
||||
|
|
||||
|
// document.getElementById('btn-play') |
||||
|
// document.getElementById('btn-pause').addEventListener('click', function() { |
||||
|
// if (trackPlayer) trackPlayer.pause(); |
||||
|
// }); |
||||
|
// document.getElementById('btn-reset').addEventListener('click', function() { |
||||
|
// if (trackPlayer) trackPlayer.reset(); |
||||
|
// }); |
||||
|
// document.getElementById('btn-clear').addEventListener('click', function() { |
||||
|
// trackSource.clear(); |
||||
|
// document.getElementById('timeline').innerHTML = ''; |
||||
|
// document.getElementById('info-panel').innerHTML = '轨迹已清空'; |
||||
|
// }); |
||||
|
// trackPlayer.play(); |
||||
|
// 页面加载后自动添加示例轨迹 |
||||
|
setTimeout(addTrackToMap, 1500) |
||||
|
}) |
||||
|
</script> |
||||
|
</body> |
||||
|
</html> |
||||
Loading…
Reference in new issue