最近追《长安的荔枝》追得上头,看着李善德抱着账本算路线算到秃头,恨不得隔空扔给他一台笔记本电脑 —— 您瞧,当年要是有咱这「荔枝运输智能规划系统」,哪儿还需要在岭南烈日下暴走?直接坐在长安城胡商的酒肆里,喝着葡萄酿就把路线优化了!
想当年,圣人一句「要吃岭南鲜荔枝」,可把李善德坑惨了:马不停蹄赶路,算错一步荔枝就变「荔干」,还要防着沿途驿站使绊子。但咱现代人不一样啊!咱有「荔枝使」专属可视化工具:选起点「岭南(深圳)」,终点「长安(西安)」,调个「时间优先」权重拉满,再把可能被节度使截胡的路段设为「阻断」—— 滴,算法跑完,最优路线直接在地图上闪金光,比李善德手绘的羊皮卷靠谱十倍!
您看这城市数据里的「深圳→广州→韶关→长沙→武汉→洛阳→西安」路线,放古代得靠驿站换马接力,现在用 Dijkstra 算法一算,总时间、总费用一目了然。要是李善德有这工具,怕是能在长安城开连锁荔枝铺,顺便给杨贵妃搞个「荔枝订阅制」,妥妥的大唐商业鬼才!
设计一个城市路线可视化工具,可以帮助快速查找城市之间的最优路线,并以可视化的方式呈现。可以选择起点和终点城市,设置优化目标(时间优先或费用优先)和权重,还能添加路线阻断信息。工具会实时计算并展示最优路线、总运输时间和费用,同时提供时间分析和费用分布的图表。
咱这工具可不是闹着玩的,核心就俩字:「智能」。就像李善德得研究每段路的马速、驿站距离,咱的算法直接把「时间」和「费用」当砝码:
Dijkstra 算法:相当于给每个城市节点装了「荔枝保鲜计时器」,优先选耗时最短的路线,比李善德拿算盘算三天三夜还准;
可视化地图:把古代驿道换成 SVG 线条,高亮的最优路线用橙色标出来,像不像给荔枝铺了条「黄金传送带」?阻断的路段直接标红,比遇到劫匪还醒目;
权重调节:想省点运费?把「费用优先」勾上,算法自动找性价比高的路,比跟驿站老板砍价还管用。
最绝的是图表分析:时间分布柱状图一看,就知道长沙到武汉那段路最费时间;费用饼图一画,驿站马料钱占大头 —— 放古代,这数据能让户部尚书都惊掉官帽:「原来运荔枝的钱都花在这儿了!」
工具最终效果:
// 城市坐标数据
const cityCoordinates = {
'深圳': { x: 100, y: 400 },
'广州': { x: 200, y: 350 },
'东莞': { x: 150, y: 370 },
'惠州': { x: 200, y: 420 },
'韶关': { x: 300, y: 300 },
'长沙': { x: 400, y: 320 },
'武汉': { x: 500, y: 350 },
'郑州': { x: 550, y: 250 },
'洛阳': { x: 600, y: 200 },
'西安': { x: 700, y: 150 }
};
// 城市图数据(时间和费用)
const cityGraph = {
'深圳': { '广州': { time: 1.5, cost: 200 }, '东莞': { time: 1.0, cost: 150 } },
'广州': { '深圳': { time: 1.5, cost: 200 }, '韶关': { time: 2.5, cost: 300 }, '长沙': { time: 5.5, cost: 500 } },
'东莞': { '深圳': { time: 1.0, cost: 150 }, '惠州': { time: 1.2, cost: 180 } },
'惠州': { '东莞': { time: 1.2, cost: 180 }, '武汉': { time: 8.0, cost: 800 } },
'韶关': { '广州': { time: 2.5, cost: 300 }, '长沙': { time: 4.0, cost: 450 } },
'长沙': { '韶关': { time: 4.0, cost: 450 }, '武汉': { time: 3.0, cost: 350 }, '郑州': { time: 8.0, cost: 700 } },
'武汉': { '惠州': { time: 8.0, cost: 800 }, '长沙': { time: 3.0, cost: 350 }, '郑州': { time: 4.5, cost: 500 }, '西安': { time: 10.0, cost: 900 } },
'郑州': { '长沙': { time: 8.0, cost: 700 }, '武汉': { time: 4.5, cost: 500 }, '洛阳': { time: 2.0, cost: 250 } },
'洛阳': { '郑州': { time: 2.0, cost: 250 }, '西安': { time: 5.0, cost: 600 } },
'西安': { '武汉': { time: 10.0, cost: 900 }, '洛阳': { time: 5.0, cost: 600 } }
};
function initMap() {
const svg = document.getElementById('city-map');
// 清空地图
while (svg.lastChild) {
if (svg.lastChild.tagName === 'defs') {
break;
}
svg.removeChild(svg.lastChild);
}
// 添加图例
const legend = document.createElementNS('http://www.w3.org/2000/svg', 'g');
legend.setAttribute('transform', 'translate(20, 20)');
svg.appendChild(legend);
// 绘制所有城市节点和路线
Object.entries(cityCoordinates).forEach(([city, { x, y }]) => {
// 创建城市节点
const node = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
node.setAttribute('cx', x);
node.setAttribute('cy', y);
node.setAttribute('r', '12');
node.setAttribute('fill', '#165DFF');
node.setAttribute('class', 'node');
node.setAttribute('data-city', city);
svg.appendChild(node);
// 创建城市标签
const label = document.createElementNS('http://www.w3.org/2000/svg', 'text');
label.setAttribute('x', x);
label.setAttribute('y', y);
label.setAttribute('text-anchor', 'middle');
label.setAttribute('dominant-baseline', 'middle');
label.setAttribute('fill', 'white');
label.setAttribute('class', 'node-label');
label.textContent = city;
svg.appendChild(label);
});
Object.entries(cityGraph).forEach(([fromCity, neighbors]) => {
const fromCoord = cityCoordinates[fromCity];
Object.entries(neighbors).forEach(([toCity, { time, cost }]) => {
if (fromCity < toCity) {
const toCoord = cityCoordinates[toCity];
// 计算路线中点
const midX = (fromCoord.x + toCoord.x) / 2;
const midY = (fromCoord.y + toCoord.y) / 2;
// 创建路线
const edge = document.createElementNS('http://www.w3.org/2000/svg', 'line');
edge.setAttribute('x1', fromCoord.x);
edge.setAttribute('y1', fromCoord.y);
edge.setAttribute('x2', toCoord.x);
edge.setAttribute('y2', toCoord.y);
edge.setAttribute('stroke', '#165DFF');
edge.setAttribute('stroke-width', '2');
edge.setAttribute('marker-end', 'url(#arrowhead)');
edge.setAttribute('class', 'edge');
edge.setAttribute('data-from', fromCity);
edge.setAttribute('data-to', toCity);
svg.appendChild(edge);
// 添加距离标签
const distanceLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
distanceLabel.setAttribute('x', midX);
distanceLabel.setAttribute('y', midY);
distanceLabel.setAttribute('text-anchor', 'middle');
distanceLabel.setAttribute('dominant-baseline', 'middle');
distanceLabel.setAttribute('class', 'distance-label');
distanceLabel.textContent = `${time}h, ¥${cost}`;
svg.appendChild(distanceLabel);
}
});
});
}
function findShortestPath(start, end, blockedEdges = [], timeWeight = 70) {
const distances = {};
const previousNodes = {};
const priorityQueue = [];
// 初始化距离和前一个节点
Object.keys(cityGraph).forEach(city => {
distances[city] = Infinity;
previousNodes[city] = null;
});
distances[start] = 0;
priorityQueue.push({ distance: 0, city: start, time: 0, cost: 0, path: [start] });
while (priorityQueue.length > 0) {
// 找到距离最小的节点
priorityQueue.sort((a, b) => a.distance - b.distance);
const { city, time, cost, path } = priorityQueue.shift();
// 如果到达终点,返回路径
if (city === end) {
return { path, totalTime: time, totalCost: cost };
}
// 跳过已经处理过的节点
if (city !== start && distances[city] < time * (timeWeight/100) + cost * (1 - timeWeight/100)) {
continue;
}
// 处理所有邻居
for (const [neighbor, { time: edgeTime, cost: edgeCost }] of Object.entries(cityGraph[city])) {
// 检查路线是否被阻断
const isBlocked = blockedEdges.some(edge =>
(edge[0] === city && edge[1] === neighbor) ||
(edge[0] === neighbor && edge[1] === city)
);
if (isBlocked) {
continue;
}
// 计算新的加权距离
const newDistance = time * (timeWeight/100) + cost * (1 - timeWeight/100) +
edgeTime * (timeWeight/100) + edgeCost * (1 - timeWeight/100);
// 如果新路径更短,则更新
if (newDistance < distances[neighbor]) {
distances[neighbor] = newDistance;
const newTime = time + edgeTime;
const newCost = cost + edgeCost;
const newPath = [...path, neighbor];
priorityQueue.push({
distance: newDistance,
city: neighbor,
time: newTime,
cost: newCost,
path: newPath
});
}
}
}
// 如果没有找到路径
return { path: [], totalTime: Infinity, totalCost: Infinity };
}
// 高亮显示最优路线
function highlightRoute(path) {
// 移除之前的高亮
document.querySelectorAll('.route-edge').forEach(el => {
el.classList.remove('route-edge');
el.setAttribute('stroke', '#165DFF');
el.setAttribute('stroke-width', '2');
el.setAttribute('marker-end', 'url(#arrowhead)');
});
// 如果没有路径,直接返回
if (path.length <= 1) {
return;
}
// 高亮新路径
for (let i = 0; i < path.length - 1; i++) {
const fromCity = path[i];
const toCity = path[i + 1];
// 找到对应的边并高亮
const edge = document.querySelector(`.edge[data-from="${fromCity}"][data-to="${toCity}"]`) ||
document.querySelector(`.edge[data-from="${toCity}"][data-to="${fromCity}"]`);
if (edge) {
edge.classList.add('route-edge');
edge.setAttribute('stroke', '#F59E0B');
edge.setAttribute('stroke-width', '4');
edge.setAttribute('marker-end', 'url(#arrowhead-route)');
}
}
}
// 更新路径信息
function updatePathInfo(path, totalTime, totalCost) {
const pathInfo = document.getElementById('path-info');
const timeInfo = document.getElementById('time-info');
const costInfo = document.getElementById('cost-info');
if (path.length === 0) {
pathInfo.textContent = '没有找到可行路径';
timeInfo.textContent = '';
costInfo.textContent = '';
return;
}
pathInfo.textContent = `最优路径: ${path.join(' → ')}`;
timeInfo.textContent = `总运输时间: ${totalTime.toFixed(1)} 小时`;
costInfo.textContent = `总运输费用: ¥${totalCost.toFixed(0)}`;
// 更新图表
updateCharts(path, totalTime, totalCost);
}
看完《长安的荔枝》最大的感慨就是:古代人运荔枝靠命硬,现代人运荔枝靠聪明。咱这工具虽不能真把荔枝穿越时空送到长安,但从技术逻辑上,可是圆了李善德的「荔枝保鲜梦」:
要是把这工具打包送给剧中的李善德,说不定剧情会变成这样:他喝着茶在屏幕上点几点,最优路线出来了;随手调调权重,运输成本降了;最后生成图表递给圣人 —— 得,不仅不用被贬,说不定还能升职做「大唐物流总监」,顺便给荔枝申请个「非物质文化遗产冷链运输」称号!
所以说啊,技术才是最强「荔枝使」—— 毕竟,能让荔枝从岭南到长安保持新鲜的,除了杨贵妃的美貌,还有咱程序员的智慧呀~
附完整代码:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>城市路线可视化工具</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.8/dist/chart.umd.min.js"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#165DFF',
secondary: '#36D399',
accent: '#F59E0B',
dark: '#1E293B',
light: '#F8FAFC'
},
fontFamily: {
inter: ['Inter', 'sans-serif'],
},
}
}
}
</script>
<style type="text/tailwindcss">
@layer utilities {
.content-auto {
content-visibility: auto;
}
.node {
@apply transition-all duration-300 cursor-pointer;
}
.node:hover {
@apply scale-110;
}
.edge {
@apply transition-all duration-300;
}
.route-edge {
@apply stroke-accent stroke-4;
}
.node-label {
@apply font-semibold text-dark pointer-events-none;
}
.distance-label {
@apply text-xs text-gray-500 pointer-events-none;
}
.control-panel {
@apply bg-white/90 backdrop-blur-sm rounded-xl shadow-lg p-4 transition-all duration-300;
}
.btn {
@apply px-4 py-2 rounded-lg font-medium transition-all duration-200 transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-offset-2;
}
.btn-primary {
@apply bg-primary text-white hover:bg-primary/90 focus:ring-primary/50;
}
.btn-secondary {
@apply bg-secondary text-white hover:bg-secondary/90 focus:ring-secondary/50;
}
.btn-accent {
@apply bg-accent text-white hover:bg-accent/90 focus:ring-accent/50;
}
.btn-outline {
@apply border border-gray-300 hover:bg-gray-100 focus:ring-gray-200;
}
.input-field {
@apply w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all duration-200;
}
.select-field {
@apply w-full px-3 py-2 border border-gray-300 rounded-lg appearance-none bg-white focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all duration-200;
}
.form-group {
@apply mb-4;
}
.result-card {
@apply bg-white rounded-xl shadow-md p-4 transition-all duration-300 hover:shadow-lg;
}
.result-title {
@apply font-bold text-lg mb-2;
}
.result-content {
@apply text-gray-700;
}
.blocked-route {
@apply stroke-red-500 stroke-dashed;
}
}
</style>
</head>
<body class="font-inter bg-gradient-to-br from-light to-gray-100 min-h-screen">
<header class="bg-primary text-white shadow-md">
<div class="container mx-auto px-4 py-6">
<h1 class="text-[clamp(1.5rem,3vw,2.5rem)] font-bold flex items-center">
<i class="fa fa-map-marker mr-3"></i>城市路线可视化工具
<span class="ml-3 text-sm font-normal bg-white/20 px-2 py-1 rounded">Beta</span>
</h1>
<p class="text-white/80 mt-2">使用Dijkstra算法查找最优路线并可视化展示</p>
</div>
</header>
<main class="container mx-auto px-4 py-8">
<div class="flex flex-col lg:flex-row gap-8">
<!-- 控制面板 -->
<div class="lg:w-1/4">
<div class="control-panel sticky top-4">
<h2 class="text-xl font-bold mb-4 flex items-center">
<i class="fa fa-sliders mr-2"></i>路径控制
</h2>
<div class="form-group">
<label for="start-city" class="block text-sm font-medium text-gray-700 mb-1">起点城市</label>
<select id="start-city" class="select-field">
<option value="深圳">深圳</option>
<option value="广州">广州</option>
<option value="东莞">东莞</option>
<option value="惠州">惠州</option>
<option value="韶关">韶关</option>
<option value="长沙">长沙</option>
<option value="武汉">武汉</option>
<option value="郑州">郑州</option>
<option value="洛阳">洛阳</option>
<option value="西安">西安</option>
</select>
</div>
<div class="form-group">
<label for="end-city" class="block text-sm font-medium text-gray-700 mb-1">终点城市</label>
<select id="end-city" class="select-field">
<option value="深圳">深圳</option>
<option value="广州">广州</option>
<option value="东莞">东莞</option>
<option value="惠州">惠州</option>
<option value="韶关">韶关</option>
<option value="长沙">长沙</option>
<option value="武汉">武汉</option>
<option value="郑州">郑州</option>
<option value="洛阳">洛阳</option>
<option value="西安">西安</option>
</select>
</div>
<div class="form-group">
<label class="block text-sm font-medium text-gray-700 mb-1">优化目标</label>
<div class="flex items-center space-x-4">
<label class="inline-flex items-center">
<input type="radio" name="optimization" value="time" class="form-radio text-primary h-4 w-4" checked>
<span class="ml-2">时间优先</span>
</label>
<label class="inline-flex items-center">
<input type="radio" name="optimization" value="cost" class="form-radio text-primary h-4 w-4">
<span class="ml-2">费用优先</span>
</label>
</div>
</div>
<div class="form-group">
<label class="block text-sm font-medium text-gray-700 mb-1">权重设置</label>
<div class="flex items-center">
<span class="text-xs text-gray-500">时间</span>
<input type="range" id="time-weight" min="0" max="100" value="70" class="w-full mx-2 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-primary">
<span class="text-xs text-gray-500">费用</span>
</div>
<div class="flex justify-between text-xs text-gray-500">
<span>0%</span>
<span id="weight-value">70% : 30%</span>
<span>100%</span>
</div>
</div>
<div class="form-group">
<label class="block text-sm font-medium text-gray-700 mb-1">路线阻断</label>
<div id="blocked-routes" class="space-y-2 max-h-40 overflow-y-auto p-2 border border-gray-200 rounded-lg">
<!-- 动态生成的阻断路线选项 -->
</div>
<button id="add-blocked-route" class="btn btn-outline text-sm w-full mt-2 flex items-center justify-center">
<i class="fa fa-plus mr-1"></i> 添加阻断路线
</button>
</div>
<button id="find-route" class="btn btn-primary w-full flex items-center justify-center">
<i class="fa fa-search mr-2"></i> 查找最优路线
</button>
</div>
<div class="result-card mt-6">
<h3 class="result-title flex items-center">
<i class="fa fa-info-circle mr-2"></i> 路径信息
</h3>
<div class="result-content">
<p id="path-info">请选择起点和终点城市,然后点击"查找最优路线"</p>
<p id="time-info" class="text-sm text-gray-500 mt-1"></p>
<p id="cost-info" class="text-sm text-gray-500"></p>
</div>
</div>
</div>
<!-- 地图可视化区域 -->
<div class="lg:w-3/4">
<div class="bg-white rounded-xl shadow-lg p-4 h-[600px] relative overflow-hidden">
<div class="absolute top-4 right-4 z-10 flex space-x-2">
<button id="zoom-in" class="btn btn-outline p-2">
<i class="fa fa-search-plus"></i>
</button>
<button id="zoom-out" class="btn btn-outline p-2">
<i class="fa fa-search-minus"></i>
</button>
<button id="reset-view" class="btn btn-outline p-2">
<i class="fa fa-refresh"></i>
</button>
</div>
<h2 class="text-xl font-bold mb-4 flex items-center">
<i class="fa fa-map mr-2"></i>城市路线图
</h2>
<div class="relative h-[calc(100%-2rem)]">
<svg id="city-map" viewBox="0 0 800 500" class="w-full h-full cursor-move">
<!-- 城市节点和路线将通过JavaScript动态生成 -->
<defs>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#165DFF"/>
</marker>
<marker id="arrowhead-route" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#F59E0B"/>
</marker>
<marker id="arrowhead-blocked" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#EF4444"/>
</marker>
</defs>
<!-- 图例 -->
<g transform="translate(20, 20)">
<text x="0" y="0" class="text-sm font-medium">图例:</text>
<line x1="0" y1="20" x2="30" y2="20" stroke="#165DFF" stroke-width="2" marker-end="url(#arrowhead)"></line>
<text x="40" y="24" class="text-xs">常规路线</text>
<line x1="0" y1="40" x2="30" y2="40" stroke="#F59E0B" stroke-width="4" marker-end="url(#arrowhead-route)"></line>
<text x="40" y="44" class="text-xs">最优路线</text>
<line x1="0" y1="60" x2="30" y2="60" stroke="#EF4444" stroke-width="2" stroke-dasharray="5,3" marker-end="url(#arrowhead-blocked)"></line>
<text x="40" y="64" class="text-xs">阻断路线</text>
</g>
</svg>
</div>
</div>
<div class="mt-6 grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="bg-white rounded-xl shadow-lg p-4">
<h3 class="text-lg font-bold mb-3 flex items-center">
<i class="fa fa-bar-chart mr-2"></i> 时间分析
</h3>
<canvas id="time-chart" height="200"></canvas>
</div>
<div class="bg-white rounded-xl shadow-lg p-4">
<h3 class="text-lg font-bold mb-3 flex items-center">
<i class="fa fa-pie-chart mr-2"></i> 费用分布
</h3>
<canvas id="cost-chart" height="200"></canvas>
</div>
</div>
</div>
</div>
</main>
<footer class="bg-dark text-white mt-12">
<div class="container mx-auto px-4 py-6">
<div class="flex flex-col md:flex-row justify-between items-center">
<div class="mb-4 md:mb-0">
<h3 class="text-lg font-bold">城市路线可视化工具</h3>
<p class="text-gray-400 text-sm mt-1">使用Dijkstra算法查找最优路线</p>
</div>
<div class="flex space-x-4">
<a href="#" class="text-gray-400 hover:text-white transition-colors">
<i class="fa fa-github text-xl"></i>
</a>
<a href="#" class="text-gray-400 hover:text-white transition-colors">
<i class="fa fa-twitter text-xl"></i>
</a>
<a href="#" class="text-gray-400 hover:text-white transition-colors">
<i class="fa fa-linkedin text-xl"></i>
</a>
</div>
</div>
<div class="border-t border-gray-700 mt-6 pt-6 text-center text-gray-400 text-sm">
© 2025 城市路线可视化工具 | 由豆包编程助手提供技术支持
</div>
</div>
</footer>
<script>
// 城市坐标数据
const cityCoordinates = {
'深圳': { x: 100, y: 400 },
'广州': { x: 200, y: 350 },
'东莞': { x: 150, y: 370 },
'惠州': { x: 200, y: 420 },
'韶关': { x: 300, y: 300 },
'长沙': { x: 400, y: 320 },
'武汉': { x: 500, y: 350 },
'郑州': { x: 550, y: 250 },
'洛阳': { x: 600, y: 200 },
'西安': { x: 700, y: 150 }
};
// 城市图数据(时间和费用)
const cityGraph = {
'深圳': { '广州': { time: 1.5, cost: 200 }, '东莞': { time: 1.0, cost: 150 } },
'广州': { '深圳': { time: 1.5, cost: 200 }, '韶关': { time: 2.5, cost: 300 }, '长沙': { time: 5.5, cost: 500 } },
'东莞': { '深圳': { time: 1.0, cost: 150 }, '惠州': { time: 1.2, cost: 180 } },
'惠州': { '东莞': { time: 1.2, cost: 180 }, '武汉': { time: 8.0, cost: 800 } },
'韶关': { '广州': { time: 2.5, cost: 300 }, '长沙': { time: 4.0, cost: 450 } },
'长沙': { '韶关': { time: 4.0, cost: 450 }, '武汉': { time: 3.0, cost: 350 }, '郑州': { time: 8.0, cost: 700 } },
'武汉': { '惠州': { time: 8.0, cost: 800 }, '长沙': { time: 3.0, cost: 350 }, '郑州': { time: 4.5, cost: 500 }, '西安': { time: 10.0, cost: 900 } },
'郑州': { '长沙': { time: 8.0, cost: 700 }, '武汉': { time: 4.5, cost: 500 }, '洛阳': { time: 2.0, cost: 250 } },
'洛阳': { '郑州': { time: 2.0, cost: 250 }, '西安': { time: 5.0, cost: 600 } },
'西安': { '武汉': { time: 10.0, cost: 900 }, '洛阳': { time: 5.0, cost: 600 } }
};
// 初始化地图
function initMap() {
const svg = document.getElementById('city-map');
// 清空地图
while (svg.lastChild) {
if (svg.lastChild.tagName === 'defs') {
break;
}
svg.removeChild(svg.lastChild);
}
// 添加图例
const legend = document.createElementNS('http://www.w3.org/2000/svg', 'g');
legend.setAttribute('transform', 'translate(20, 20)');
svg.appendChild(legend);
// 添加图例文本
const legendText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
legendText.setAttribute('x', '0');
legendText.setAttribute('y', '0');
legendText.setAttribute('class', 'text-sm font-medium');
legendText.textContent = '图例:';
legend.appendChild(legendText);
// 添加图例项 - 常规路线
const normalLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
normalLine.setAttribute('x1', '0');
normalLine.setAttribute('y1', '20');
normalLine.setAttribute('x2', '30');
normalLine.setAttribute('y2', '20');
normalLine.setAttribute('stroke', '#165DFF');
normalLine.setAttribute('stroke-width', '2');
normalLine.setAttribute('marker-end', 'url(#arrowhead)');
legend.appendChild(normalLine);
const normalLineText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
normalLineText.setAttribute('x', '40');
normalLineText.setAttribute('y', '24');
normalLineText.setAttribute('class', 'text-xs');
normalLineText.textContent = '常规路线';
legend.appendChild(normalLineText);
// 添加图例项 - 最优路线
const routeLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
routeLine.setAttribute('x1', '0');
routeLine.setAttribute('y1', '40');
routeLine.setAttribute('x2', '30');
routeLine.setAttribute('y2', '40');
routeLine.setAttribute('stroke', '#F59E0B');
routeLine.setAttribute('stroke-width', '4');
routeLine.setAttribute('marker-end', 'url(#arrowhead-route)');
legend.appendChild(routeLine);
const routeLineText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
routeLineText.setAttribute('x', '40');
routeLineText.setAttribute('y', '44');
routeLineText.setAttribute('class', 'text-xs');
routeLineText.textContent = '最优路线';
legend.appendChild(routeLineText);
// 添加图例项 - 阻断路线
const blockedLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
blockedLine.setAttribute('x1', '0');
blockedLine.setAttribute('y1', '60');
blockedLine.setAttribute('x2', '30');
blockedLine.setAttribute('y2', '60');
blockedLine.setAttribute('stroke', '#EF4444');
blockedLine.setAttribute('stroke-width', '2');
blockedLine.setAttribute('stroke-dasharray', '5,3');
blockedLine.setAttribute('marker-end', 'url(#arrowhead-blocked)');
legend.appendChild(blockedLine);
const blockedLineText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
blockedLineText.setAttribute('x', '40');
blockedLineText.setAttribute('y', '64');
blockedLineText.setAttribute('class', 'text-xs');
blockedLineText.textContent = '阻断路线';
legend.appendChild(blockedLineText);
// 绘制所有城市节点
Object.entries(cityCoordinates).forEach(([city, { x, y }]) => {
const node = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
node.setAttribute('cx', x);
node.setAttribute('cy', y);
node.setAttribute('r', '12');
node.setAttribute('fill', '#165DFF');
node.setAttribute('class', 'node');
node.setAttribute('data-city', city);
svg.appendChild(node);
const label = document.createElementNS('http://www.w3.org/2000/svg', 'text');
label.setAttribute('x', x);
label.setAttribute('y', y);
label.setAttribute('text-anchor', 'middle');
label.setAttribute('dominant-baseline', 'middle');
label.setAttribute('fill', 'white');
label.setAttribute('class', 'node-label');
label.textContent = city;
svg.appendChild(label);
});
// 绘制所有路线
Object.entries(cityGraph).forEach(([fromCity, neighbors]) => {
const fromCoord = cityCoordinates[fromCity];
Object.entries(neighbors).forEach(([toCity, { time, cost }]) => {
// 只绘制一次路线(避免重复)
if (fromCity < toCity) {
const toCoord = cityCoordinates[toCity];
// 计算路线的中点
const midX = (fromCoord.x + toCoord.x) / 2;
const midY = (fromCoord.y + toCoord.y) / 2;
// 创建路线
const edge = document.createElementNS('http://www.w3.org/2000/svg', 'line');
edge.setAttribute('x1', fromCoord.x);
edge.setAttribute('y1', fromCoord.y);
edge.setAttribute('x2', toCoord.x);
edge.setAttribute('y2', toCoord.y);
edge.setAttribute('stroke', '#165DFF');
edge.setAttribute('stroke-width', '2');
edge.setAttribute('marker-end', 'url(#arrowhead)');
edge.setAttribute('class', 'edge');
edge.setAttribute('data-from', fromCity);
edge.setAttribute('data-to', toCity);
svg.appendChild(edge);
// 添加距离标签
const distanceLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
distanceLabel.setAttribute('x', midX);
distanceLabel.setAttribute('y', midY);
distanceLabel.setAttribute('text-anchor', 'middle');
distanceLabel.setAttribute('dominant-baseline', 'middle');
distanceLabel.setAttribute('class', 'distance-label');
distanceLabel.textContent = `${time}h, ¥${cost}`;
svg.appendChild(distanceLabel);
}
});
});
}
// 使用Dijkstra算法查找最短路径
function findShortestPath(start, end, blockedEdges = [], timeWeight = 70) {
const distances = {};
const previousNodes = {};
const priorityQueue = [];
// 初始化距离和前一个节点
Object.keys(cityGraph).forEach(city => {
distances[city] = Infinity;
previousNodes[city] = null;
});
distances[start] = 0;
priorityQueue.push({ distance: 0, city: start, time: 0, cost: 0, path: [start] });
while (priorityQueue.length > 0) {
// 找到距离最小的节点
priorityQueue.sort((a, b) => a.distance - b.distance);
const { city, time, cost, path } = priorityQueue.shift();
// 如果到达终点,返回路径
if (city === end) {
return { path, totalTime: time, totalCost: cost };
}
// 跳过已经处理过的节点
if (city !== start && distances[city] < time * (timeWeight/100) + cost * (1 - timeWeight/100)) {
continue;
}
// 处理所有邻居
for (const [neighbor, { time: edgeTime, cost: edgeCost }] of Object.entries(cityGraph[city])) {
// 检查路线是否被阻断
const isBlocked = blockedEdges.some(edge =>
(edge[0] === city && edge[1] === neighbor) ||
(edge[0] === neighbor && edge[1] === city)
);
if (isBlocked) {
continue;
}
// 计算新的加权距离
const newDistance = time * (timeWeight/100) + cost * (1 - timeWeight/100) +
edgeTime * (timeWeight/100) + edgeCost * (1 - timeWeight/100);
// 如果新路径更短,则更新
if (newDistance < distances[neighbor]) {
distances[neighbor] = newDistance;
const newTime = time + edgeTime;
const newCost = cost + edgeCost;
const newPath = [...path, neighbor];
priorityQueue.push({
distance: newDistance,
city: neighbor,
time: newTime,
cost: newCost,
path: newPath
});
}
}
}
// 如果没有找到路径
return { path: [], totalTime: Infinity, totalCost: Infinity };
}
// 高亮显示最优路线
function highlightRoute(path) {
// 移除之前的高亮
document.querySelectorAll('.route-edge').forEach(el => {
el.classList.remove('route-edge');
el.setAttribute('stroke', '#165DFF');
el.setAttribute('stroke-width', '2');
el.setAttribute('marker-end', 'url(#arrowhead)');
});
// 如果没有路径,直接返回
if (path.length <= 1) {
return;
}
// 高亮新路径
for (let i = 0; i < path.length - 1; i++) {
const fromCity = path[i];
const toCity = path[i + 1];
// 找到对应的边并高亮
const edge = document.querySelector(`.edge[data-from="${fromCity}"][data-to="${toCity}"]`) ||
document.querySelector(`.edge[data-from="${toCity}"][data-to="${fromCity}"]`);
if (edge) {
edge.classList.add('route-edge');
edge.setAttribute('stroke', '#F59E0B');
edge.setAttribute('stroke-width', '4');
edge.setAttribute('marker-end', 'url(#arrowhead-route)');
}
}
}
// 高亮显示阻断路线
function highlightBlockedRoutes(blockedEdges) {
// 移除之前的阻断高亮
document.querySelectorAll('.blocked-route').forEach(el => {
el.classList.remove('blocked-route');
el.setAttribute('stroke', '#165DFF');
el.setAttribute('stroke-dasharray', '');
el.setAttribute('marker-end', 'url(#arrowhead)');
});
// 高亮新的阻断路线
blockedEdges.forEach(([city1, city2]) => {
const edge = document.querySelector(`.edge[data-from="${city1}"][data-to="${city2}"]`) ||
document.querySelector(`.edge[data-from="${city2}"][data-to="${city1}"]`);
if (edge) {
edge.classList.add('blocked-route');
edge.setAttribute('stroke', '#EF4444');
edge.setAttribute('stroke-dasharray', '5,3');
edge.setAttribute('marker-end', 'url(#arrowhead-blocked)');
}
});
}
// 更新路径信息
function updatePathInfo(path, totalTime, totalCost) {
const pathInfo = document.getElementById('path-info');
const timeInfo = document.getElementById('time-info');
const costInfo = document.getElementById('cost-info');
if (path.length === 0) {
pathInfo.textContent = '没有找到可行路径';
timeInfo.textContent = '';
costInfo.textContent = '';
return;
}
pathInfo.textContent = `最优路径: ${path.join(' → ')}`;
timeInfo.textContent = `总运输时间: ${totalTime.toFixed(1)} 小时`;
costInfo.textContent = `总运输费用: ¥${totalCost.toFixed(0)}`;
// 更新图表
updateCharts(path, totalTime, totalCost);
}
// 更新图表
function updateCharts(path, totalTime, totalCost) {
// 如果没有路径,清空图表
if (path.length <= 1) {
document.getElementById('time-chart').getContext('2d').clearRect(0, 0, 400, 200);
document.getElementById('cost-chart').getContext('2d').clearRect(0, 0, 400, 200);
return;
}
// 准备时间数据
const timeLabels = [];
const timeData = [];
let cumulativeTime = 0;
for (let i = 0; i < path.length - 1; i++) {
const fromCity = path[i];
const toCity = path[i + 1];
const time = cityGraph[fromCity][toCity].time;
timeLabels.push(`${fromCity} → ${toCity}`);
timeData.push(time);
cumulativeTime += time;
}
// 更新时间图表
const timeCtx = document.getElementById('time-chart').getContext('2d');
new Chart(timeCtx, {
type: 'bar',
data: {
labels: timeLabels,
datasets: [{
label: '运输时间 (小时)',
data: timeData,
backgroundColor: '#165DFF',
borderColor: '#165DFF',
borderWidth: 1
}]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: '小时'
}
}
}
}
});
// 准备费用数据
const costLabels = [];
const costData = [];
let cumulativeCost = 0;
for (let i = 0; i < path.length - 1; i++) {
const fromCity = path[i];
const toCity = path[i + 1];
const cost = cityGraph[fromCity][toCity].cost;
costLabels.push(`${fromCity} → ${toCity}`);
costData.push(cost);
cumulativeCost += cost;
}
// 更新费用图表
const costCtx = document.getElementById('cost-chart').getContext('2d');
new Chart(costCtx, {
type: 'pie',
data: {
labels: costLabels,
datasets: [{
label: '运输费用 (元)',
data: costData,
backgroundColor: [
'#165DFF',
'#36D399',
'#F59E0B',
'#EF4444',
'#8B5CF6',
'#EC4899',
'#06B6D4',
'#10B981'
],
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'right'
}
}
}
});
}
// 生成所有可能的路线选项
function generateRouteOptions() {
const routeOptions = [];
const cities = Object.keys(cityGraph);
for (let i = 0; i < cities.length; i++) {
for (let j = i + 1; j < cities.length; j++) {
const city1 = cities[i];
const city2 = cities[j];
// 检查这两个城市之间是否有直接连接
if (cityGraph[city1].hasOwnProperty(city2) || cityGraph[city2].hasOwnProperty(city1)) {
routeOptions.push([city1, city2]);
}
}
}
return routeOptions;
}
// 初始化阻断路线选择器
function initBlockedRoutesSelector() {
const blockedRoutesContainer = document.getElementById('blocked-routes');
const routeOptions = generateRouteOptions();
// 清空容器
blockedRoutesContainer.innerHTML = '';
// 为每个路线选项创建一个复选框
routeOptions.forEach(([city1, city2]) => {
const routeId = `block-${city1}-${city2}`;
const div = document.createElement('div');
div.className = 'flex items-center';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = routeId;
checkbox.name = 'blocked-routes';
checkbox.value = `${city1},${city2}`;
checkbox.className = 'h-4 w-4 text-primary focus:ring-primary border-gray-300 rounded';
const label = document.createElement('label');
label.htmlFor = routeId;
label.className = 'ml-2 block text-sm text-gray-700';
label.textContent = `${city1} ↔ ${city2}`;
div.appendChild(checkbox);
div.appendChild(label);
blockedRoutesContainer.appendChild(div);
});
}
// 初始化事件监听器
function initEventListeners() {
// 查找最优路线按钮
document.getElementById('find-route').addEventListener('click', () => {
const startCity = document.getElementById('start-city').value;
const endCity = document.getElementById('end-city').value;
// 获取选中的阻断路线
const blockedRoutes = Array.from(
document.querySelectorAll('input[name="blocked-routes"]:checked')
).map(el => el.value.split(','));
// 获取优化目标和权重
const optimization = document.querySelector('input[name="optimization"]:checked').value;
const timeWeight = optimization === 'time' ? 90 :
optimization === 'cost' ? 10 :
document.getElementById('time-weight').value;
// 查找最短路径
const { path, totalTime, totalCost } = findShortestPath(startCity, endCity, blockedRoutes, timeWeight);
// 高亮显示路径
highlightRoute(path);
// 更新路径信息
updatePathInfo(path, totalTime, totalCost);
// 高亮显示阻断路线
highlightBlockedRoutes(blockedRoutes);
});
// 时间权重滑块
const timeWeightSlider = document.getElementById('time-weight');
const weightValue = document.getElementById('weight-value');
timeWeightSlider.addEventListener('input', () => {
const timePercent = parseInt(timeWeightSlider.value);
const costPercent = 100 - timePercent;
weightValue.textContent = `${timePercent}% : ${costPercent}%`;
});
// 地图缩放控制
const cityMap = document.getElementById('city-map');
let scale = 1;
let translateX = 0;
let translateY = 0;
document.getElementById('zoom-in').addEventListener('click', () => {
scale = Math.min(scale * 1.2, 3);
cityMap.setAttribute('transform', `scale(${scale}) translate(${translateX/scale}, ${translateY/scale})`);
});
document.getElementById('zoom-out').addEventListener('click', () => {
scale = Math.max(scale / 1.2, 0.5);
cityMap.setAttribute('transform', `scale(${scale}) translate(${translateX/scale}, ${translateY/scale})`);
});
document.getElementById('reset-view').addEventListener('click', () => {
scale = 1;
translateX = 0;
translateY = 0;
cityMap.setAttribute('transform', '');
});
// 地图拖拽功能
let isDragging = false;
let startX, startY;
cityMap.addEventListener('mousedown', (e) => {
if (e.target === cityMap) {
isDragging = true;
startX = e.clientX - translateX;
startY = e.clientY - translateY;
}
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
e.preventDefault();
translateX = e.clientX - startX;
translateY = e.clientY - startY;
cityMap.setAttribute('transform', `translate(${translateX}, ${translateY}) scale(${scale})`);
});
document.addEventListener('mouseup', () => {
isDragging = false;
});
}
// 初始化页面
function init() {
initMap();
initBlockedRoutesSelector();
initEventListeners();
// 初始设置终点为西安
document.getElementById('end-city').value = '西安';
}
// 页面加载完成后初始化
window.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。