简介
最近学习了svg,想着使用svg实现地铁线路图
其中黄色是1号线,蓝色是2号线,橙色是3号线
实现:react+svg+数据结构-图。
考虑范围包括了每站时间,但未包括了换站时间。考虑到换站时间可以加到每2个交换站的路程里
功能
功能:选择2个地铁站,标出最短路程。
求最少换站路线,暂未做
实现思路
- 简化问题,先将所有地铁站分2类,交换站和非交换站。那么交换站可以充当图中的。那么从a=>b, 变成a=>交换站=>交换站=>b的问题,需要写死的是非交换站(a,b)能到达的交换站(下面的adjcent数组), 其中a=>交换站 和b=>交换站 相对静止,但是我这里也考虑到了非交换站到交换站需要的时间(time)
地铁线路图
图
- 首先根据每条地铁图数据绘制出地铁线路图,并添加上点击事件,这里要处理好地铁线路图的数据,数据需要相对准确,因为后面需要计算出最短路径。
- 求最短距离,使用的是Floyd最短路算法(全局/多源最短路)。 其中原理:计算a->b的最短路径,遍历所有,查找是否有最捷径路径 a->x x->b
for(k=1;k<=n;k++)
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
if(e[i][j]>e[i][k]+e[k][j]) // i->j i->k k->j
e[i][j]=e[i][k]+e[k][j];
然而拿到最短路程后,但是并未拿到路程,拿到的是比如,a点到所有点的最短路程。你们可以思考一下如果获取最短路径。
大概长这样
- 求最短路径 使用一个对象,存储每次找到较短路径。
changeRodePath[
${is}to${js}] = [ [is, ks], [ks, js], ]
function getAllPointShortest(n, e) {
let changeRodePath = {};
for (let k = 0; k < n; k++) {
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
if (e[i][j] > e[i][k] + e[k][j]) {
e[i][j] = e[i][k] + e[k][j];
console.log("-------------------------");
const is = changeStation[i];
const ks = changeStation[k];
const js = changeStation[j];
changeRodePath[`${is}to${js}`] = [
[is, ks],
[ks, js],
];
console.log(changeStation[i], changeStation[j]);
console.log(changeStation[i], changeStation[k]);
console.log(changeStation[k], changeStation[j]);
// 2_2 2_5
//2_2 1_2
//1_2 2_5
}
}
}
}
setChangeRodePath(changeRodePath);
return e;
}
当选中2个站时,先取出adjacent,然后求出最短路程,
let path = {};
adjacent0.forEach((p0,i1) => {
adjacent1.forEach((p1,i2) => {
const index0 = changeStation.indexOf(p0);
const index1 = changeStation.indexOf(p1);
let t=time0[i1]+time1[i2]
if ((rodePath[index0][index1]+t) < minPath) {
minPath = rodePath[index0][index1];
path = { p0, p1};
}
});
});
具体多少不重要,重要的是通过
let pathm = changeRodePath[${path.p0}to${path.p1}],递归查找是否有更短的捷径,因为,2_1 =>3_9
的路径是:2_1 =>1_3=>1_5=>1_8,所以不一定有捷径a->c c—b, 可能是 a->c c->b, 然后发现有更短路径,c->d d->b,那么a-b 路程就变成了a->c->d->b。回到正题,递归之后就能取到最短路径了,然后通过2个交换点取得路径。
没有就更简单了
5.取对应的line,去渲染,这里分2类,交换站之间的路径(最短路径),头和尾。然后分别渲染polyline(使用对应line 的颜色)
function getPl(item, attr, listen) {
return (
<g {...attr} {...listen}>
<polyline //绘制line
{...item}
fill="none"
color={item.colorH}
strokeWidth="5"
strokeLinecap="round"
strokeLinejoin="round"
/>
{item.usePointn.map((point) => { // line 上的站
return (
<use
x={point.x}
onClick={() => choosePoint(point)}
y={point.y}
fill={point.color}
href="#point"
></use>
);
})}
</g>
);
}
代码准备
// 上图所示,数据随便造,需要合理时间,不然得到的路程奇奇怪怪的
代码部分
html
<div style={{ width: "80vw", height: "100vh" }}>
<svg
id="passWay"
viewBox="0 0 800 600"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<g id="point">
<circle r="4"></circle>
<circle r="3" fill="#fff"></circle>
</g>
</defs>
// 所有地铁线路图
{polyline.map((item) => {
return getPl(
item,
{},
{
onMouseEnter: (e) => onMouseEnterShow(e, item),
onMouseOut: () => {
clearTimeout(t1.current);
t1.current = null;
},
}
);
})}
// mask
{ choosePoints.length==2 && (
<rect
x="0"
y="0"
width={"100%"}
height={"100%"}
fillOpacity={0.9}
fill="white"
></rect>
)}
// 最短路程
{choosePoints && choosePoints.length==2 && showReduLine.map(line=>{
return getPl(line, {}, {})
})
}
</svg>
</div>
通过line 获取 polyline
function getLineP(line) {
const usePointn = [];
let path = "";
line.points.forEach((item, index) => {
const { x, y, isStart, isChange, isEnd } = item;
usePointn.push({ ...item, color: line.color });
if (index == 0) {
path = `${x},${y} `;
} else {
path += `${x},${y} `;
}
});
const polylinen = {
usePointn,
stroke: line.color,
...line,
pointStation: line.points,
points: path,
};
return polylinen;
}
选出2站绘制路程
function comfirPath(point0, point1, p0, p1, pathm) {
let pShow0= getLines(point0,p0)
let pShow1= getLines(point1,p1)
let pathsCenter=[]
if (pathm) {
function recursion(pathm){
pathm.map(([p0,p1])=>{
let pathn = changeRodePath[`${p0}to${p1}`];
if(pathn){
recursion(pathn)
}else{
// 中间的line 不用按顺序
pathsCenter.push(getChangeStationLine(p0,p1))
}
})
}
recursion(pathm)
}else{
pathsCenter=[getChangeStationLine(p0,p1)]
}
const pyAll= [pShow0,pShow1,...pathsCenter].map(line=>{
const py= getLineP({
points:line,
})
py.stroke=line.color
return py
})
setShowReduLine(pyAll); // 绘制
}
完整代码
import { useEffect, useRef, useState } from "react";
import "./App.css";
import { lineWidth, lines, changeStation, e } from "./dataEm";
function App() {
const [polyline, setPolyline] = useState([]);
const [showHover, setShowHover] = useState(false);
const [showReduLine, setShowReduLine] = useState([]);
const [choosePoints, setChoosePoints] = useState([]);
useEffect(() => {
drawPath(lines);
}, []);
const [changeRodePath, setChangeRodePath] = useState({});
function getAllPointShortest(n, e) {
let changeRodePath = {};
for (let k = 0; k < n; k++) {
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
if (e[i][j] > e[i][k] + e[k][j]) {
e[i][j] = e[i][k] + e[k][j];
console.log("-------------------------");
const is = changeStation[i];
const ks = changeStation[k];
const js = changeStation[j];
changeRodePath[`${is}to${js}`] = [
[is, ks],
[ks, js],
];
console.log(changeStation[i], changeStation[j]);
console.log(changeStation[i], changeStation[k]);
console.log(changeStation[k], changeStation[j]);
}
}
}
}
setChangeRodePath(changeRodePath);
console.log(e,'e');
return e;
}
const [rodePath, setRodePath] = useState([]);
useEffect(() => {
const ne = getAllPointShortest(
changeStation.length,
JSON.parse(JSON.stringify(e)),
e
);
setRodePath(ne);
}, []);
function getLineP(line) {
const usePointn = [];
let path = "";
line.points.forEach((item, index) => {
const { x, y, isStart, isChange, isEnd } = item;
usePointn.push({ ...item, color: line.color });
if (index == 0) {
path = `${x},${y} `;
} else {
path += `${x},${y} `;
}
});
const polylinen = {
usePointn,
stroke: line.color,
...line,
pointStation: line.points,
points: path,
};
return polylinen;
}
function drawPath(lines) {
const points = [];
const edges = [];
const polylinen = lines.map(getLineP);
setPolyline(polylinen);
}
function drawChangeStation({ x, y }) {}
const t1 = useRef(-1);
function onMouseEnterShow(e, item) {
t1.current = setTimeout(() => {
setShowHover(item);
}, 100);
}
function returnStart(){
setChoosePoints([])
setShowReduLine([])
}
function choosePoint(point) {
console.log(point, "point");
let n =[...choosePoints]
n.push(point);
setChoosePoints(n)
const point0 = n[0];
if (n.length == 2) {
let pl0=point0.adjacent.length==0
let pl1=point.adjacent.length==0
const adjacent0 = pl0? [point0.id]:point0.adjacent
const time0 = pl0? [0]:point0.time
const adjacent1 = pl1? [point.id]:point.adjacent
const time1 = pl1? [0]:point.time
let minPath = Infinity;
let path = {};
console.log(adjacent0, adjacent1, "adjacent0");
adjacent0.forEach((p0,i1) => {
adjacent1.forEach((p1,i2) => {
const index0 = changeStation.indexOf(p0);
const index1 = changeStation.indexOf(p1);
let t=time0[i1]+time1[i2]
if ((rodePath[index0][index1]+t) < minPath) {
minPath = rodePath[index0][index1]+t
path = {
p0,
p1,
};
}
});
});
console.log(changeRodePath, "changeRodePath");
let pathm = changeRodePath[`${path.p0}to${path.p1}`];
console.log(minPath, path, "minPath");
comfirPath(point0, point, path.p0, path.p1, pathm);
return;
}
}
// 根据2点 获取截断的line, 按顺序返回
function getLineOrder(l0,index0,index0c){
let pShow0=[]
if(index0>index0c){
[index0,index0c]=[index0c,index0]
}
pShow0=l0.points.slice(index0,index0c+1)
pShow0.color=l0.colorH
return pShow0
}
//根据point0和交换站id,获取截断的line, 按顺序返回
function getLines(point0,p0){
let l0 = lines[point0.parentId-1]
let index0 =getPointInLineIndex(l0, point0.id)
let index0c =getPointInLineIndex(l0, p0)
return getLineOrder(l0,index0,index0c)
}
//id获取交换站
function getChangeStationByStationId(id){
let changeStation=null
lines.find(line=>{
return line.points.find((item,index)=>{
if(item.id==id){
changeStation=item
return true
}
})
})
return changeStation
}
// 获取2个交换站的 line
function getChangeStationLine(p0,p1){
let sameParentId=getSameParentId(getChangeStationByStationId(p0), getChangeStationByStationId(p1))
const line=lines[sameParentId-1]
let index0= line.points.findIndex(item=>item.id==p0)
let index1= line.points.findIndex(item=>item.id==p1)
return getLineOrder(line,index0,index1)
}
// 站1 站2 交换站1 交换站2
function comfirPath(point0, point1, p0, p1, pathm) {
// 换站点一样,是同一条线
if(p0==p1 && point0.parentId == point1.parentId){
let l0 = lines[point0.parentId-1]
let index0 =getPointInLineIndex(l0, point0.id)
let index1 =getPointInLineIndex(l0, point1.id)
let indexChangeStation =getPointInLineIndex(l0, p0)
// 获取index 判断是否在同一侧
if((index0<indexChangeStation && index1<indexChangeStation)|| (index0>indexChangeStation && index1>indexChangeStation)){
const showLine = getLines(point0,point1.id)
setShowLineByLinePath([showLine])
return
}
}
let pShow0= getLines(point0,p0)
let pShow1= getLines(point1,p1)
let pathsCenter=[]
if (pathm) {
function recursion(pathm){
pathm.map(([p0,p1])=>{
let pathn = changeRodePath[`${p0}to${p1}`];
if(pathn){
recursion(pathn)
}else{
// 中间的line 不用按顺序
pathsCenter.push(getChangeStationLine(p0,p1))
}
})
}
recursion(pathm)
}else{
pathsCenter=[getChangeStationLine(p0,p1)]
}
// const pyAll= [pShow0,pShow1,...pathsCenter].map(line=>{
// const py= getLineP({
// points:line,
// })
// py.stroke=line.color
// return py
// })
// setShowReduLine(pyAll);
setShowLineByLinePath([pShow0,pShow1,...pathsCenter])
}
function setShowLineByLinePath(pyAll){
const showReduLine=pyAll.map(line=>{
const py= getLineP({
points:line,
})
py.stroke=line.color
return py
})
setShowReduLine(showReduLine);
}
function getPointInLineIndex(line, id) {
return line.points.findIndex((item) => id == item.id);
}
function getSameParentId({parentId,parentId2}, {parentId:p,parentId2:p2}) {
let parentResult=[parentId,parentId2].find(item=>{
return [p,p2].some(item2=>{
return item==item2
})
})
return parentResult
}
function getPl(item, attr, listen) {
return (
<g {...attr} {...listen}>
<polyline
{...item}
fill="none"
color={item.colorH}
strokeWidth="5"
strokeLinecap="round"
strokeLinejoin="round"
/>
{item.usePointn&&item.usePointn.map((point) => {
return (
<use
x={point.x}
onClick={() => choosePoint(point)}
y={point.y}
fill={point.color}
href="#point"
></use>
);
})}
</g>
);
}
function showLine(line) {
const p = polyline.find((item) => item.id == line.id);
setShowHover(p);
}
return (
<div style={{ display: "flex" }}>
<div style={{ width: "80vw", height: "100vh" }}>
<svg
id="passWay"
viewBox="0 0 800 600"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<g id="point">
<circle r="4"></circle>
<circle r="3" fill="#fff"></circle>
</g>
<symbol id="change" viewBox="0 0 1024 1024">
<path
d="M784.673811 364.080302a207.891321 207.891321 0 0 0-147.958339-61.304755H417.173736v-41.984c0-13.389283-9.177358-18.412679-20.518642-11.148075l-101.047547 65.226868c-11.283321 7.322566-11.186717 19.030943 0.231849 26.102339l100.622491 62.309434c11.341283 7.071396 20.711849 1.874113 20.711849-11.53449V349.898868h219.599698c89.33917 0 162.023849 72.742642 162.023849 162.023849 0 14.819019-1.951396 29.464151-5.873509 43.51034-3.497057 12.577811 3.864151 25.542038 16.441962 28.981132 2.105962 0.676226 4.211925 0.966038 6.317887 0.966037 10.336604 0 19.803774-6.878189 22.721207-17.272754 5.023396-18.219472 7.515774-37.11517 7.515774-56.146114a208.084528 208.084528 0 0 0-61.246793-147.881056zM745.703849 682.988679l-100.564528-62.309434c-11.437887-7.071396-20.808453-1.912755-20.808453 11.534491v41.752151H387.32317c-89.33917 0-162.023849-72.684679-162.023849-162.023849a161.521509 161.521509 0 0 1 25.831849-87.79351 23.687245 23.687245 0 0 0-6.974793-32.671396 23.648604 23.648604 0 0 0-32.613434 7.032755 208.664151 208.664151 0 0 0-33.424905 113.470792c0 55.894943 21.75517 108.408755 61.266113 147.919698a207.717434 207.717434 0 0 0 147.95834 61.304755h237.046339v41.984c0 13.447245 9.235321 18.451321 20.518642 11.148076l101.047547-65.284831c11.264-7.264604 11.167396-18.972981-0.25117-26.063698zM512 28.981132c-266.76166 0-483.018868 216.257208-483.018868 483.018868s216.257208 483.018868 483.018868 483.018868 483.018868-216.257208 483.018868-483.018868-216.257208-483.018868-483.018868-483.018868z m0 917.735849C271.920302 946.716981 77.283019 752.079698 77.283019 512S271.920302 77.283019 512 77.283019 946.716981 271.920302 946.716981 512 752.079698 946.716981 512 946.716981z"
fill="#838383"
p-id="10791"
data-spm-anchor-id="a313x.search_index.0.i5.5b9b3a81AsGYCG"
className="selected"
></path>
</symbol>
</defs>
{polyline.map((item) => {
return getPl(
item,
{},
{
onMouseEnter: (e) => onMouseEnterShow(e, item),
onMouseOut: () => {
clearTimeout(t1.current);
t1.current = null;
},
}
);
})}
{ choosePoints.length==2 && (
<rect
x="0"
y="0"
width={"100%"}
height={"100%"}
fillOpacity={0.9}
fill="white"
></rect>
)}
{choosePoints && choosePoints.length==2 && showReduLine.map(line=>{
return getPl(line, {}, {})
})
}
</svg>
</div>
<div style={{ flex: 1, minWidth: "100px" }}>
{lines.map((item) => {
return <div onClick={() => showLine(item)}>{item.lineName}号线</div>;
})}
<div>
<input type="radio"/>时间最短
<input type="radio"/>换站最少
</div>
<div onClick={returnStart}>重选</div>
</div>
</div>
);
}
export default App;
dataEm文件
export const pace = 10;
export const lineWidth = 8;
export const pointR = 4;
export const pointSR = 3;
export const changeStation = ["1_2", "1_3", "1_5", "1_8", "2_2", "2_5"];
export const changeStationPoint = ["1_2", "1_3", "1_5", "1_8", "2_2", "2_5"];
export const e = [
[0, 4, 1000, 1000, 2, 16],
[4, 0, 7, 1000, 1, 10],
[1000, 7, 0, 13, 1000, 6],
[1000, 1000, 13, 0, 1000, 12],
[2, 1, 1000, 1000, 0, 1000],
[16, 10, 6, 12, 1000, 0],
];
export const lines = [
{
lineName: 1,
color: "rgba(250, 228, 28,.8)",
colorH: "rgba(250, 228, 28,1)",
id: 1,
points: [ {
x: 45,
y: 5,
isStart: true,
parentId: 1,
station: "1_0",
id: "1_0",
adjacent: ["1_2"],
time: [8],
},
{
x: 5,
y: 5,
isStart: true,
parentId: 1,
station: "1_1",
id: "1_1",
adjacent: ["1_2"],
time: [3],
},
{
x: 50,
y: 50,
isChange: true,
changeStationTime: 2,
parentId: 1,
parentId2: 2,
id: "1_2",
station: "1_2",
adjacent: [],
},
{
x: 100,
y: 100,
isChange: true,
changeStationTime: 2,
parentId: 1,
parentId2: 3,
id: "1_3",
station: "1_3",
adjacent: [],
},
{
x: 100,
y: 150,
parentId: 1,
id: "1_4",
station: "1_4",
adjacent: ["1_3", "1_5"],
time: [3, 4],
},
{
x: 150,
y: 200,
isChange: true,
changeStationTime: 4,
parentId: 1,
parentId2: 2,
station: "1_5",
id: "1_5",
adjacent: [],
},
{
x: 130,
y: 300,
parentId: 1,
station: "1_6",
id: "1_6",
adjacent: ["1_5", "1_8"],
time: [6, 7],
},
{
x: 120,
y: 350,
parentId: 1,
station: "1_7",
id: "1_7",
adjacent: ["1_8", "1_5"],
time: [2, 11],
},
{
x: 90,
y: 380,
parentId: 1,
parentId2: 3,
isChange: true,
changeStationTime: 5,
station: "1_8",
id: "1_8",
adjacent: [],
},
{
x: 80,
y: 420,
parentId: 1,
station: "1_9",
id: "1_9",
adjacent: ["1_8"],
time: [4],
},
{
x: 40,
y: 440,
parentId: 1,
station: "1_10",
id: "1_10",
adjacent: ["1_8"],
time: [6],
},
],
},
{
lineName: 2,
color: "rgba(25, 117, 209,.8)",
colorH: "rgba(25, 117, 209,1)",
id: 2,
points: [
{
x: 200,
y: 105,
isStart: true,
parentId: 2,
station: "2_1",
id: "2_1",
adjacent: ["2_2"],
time: [3],
},
{
x: 130,
y: 80,
isChange: true,
changeStationTime: 4,
parentId: 2,
parentId2: 3,
station: "2_2",
id: "2_2",
adjacent: [],
},
{
x: 50,
y: 50,
isChange: true,
changeStationTime: 2,
parentId: 2,
parentId2: 1,
station: "1_2",
id: "1_2",
adjacent: [],
},
{
x: 10,
y: 150,
parentId: 2,
station: "2_4",
id: "2_4",
adjacent: ["1_2", "2_5"],
time: [8, 8],
},
{
x: 50,
y: 250,
isChange: true,
changeStationTime: 7,
parentId: 2,
parentId2: 3,
station: "2_5",
id: "2_5",
adjacent: [],
},
{
x: 100,
y: 220,
parentId: 2,
station: "2_6",
id: "2_6",
adjacent: ["2_5", "1_5"],
time: [3, 3],
},
{
x: 150,
y: 200,
isChange: true,
changeStationTime: 4,
parentId: 2,
parentId2: 1,
station: "1_5",
id: "1_5",
adjacent: [],
},
{
x: 250,
y: 200,
parentId: 2,
isEnd: true,
station: "2_8",
id: "2_8",
adjacent: ["1_5"],
time: [6],
},
],
},
{
lineName: 3,
color: "rgba(236, 117, 70,.8)",
colorH: "rgba(236, 117, 70,1)",
id: 3,
points: [
{
x: 200,
y: 10,
isStart: true,
parentId: 3,
station: "3_1",
id: "3_1",
adjacent: ["2_2"],
time: [6],
},
{
x: 130,
y: 80,
parentId: 3,
parentId2: 1,
isChange: true,
changeStationTime: 4,
station: "2_2",
id: "2_2",
adjacent: [],
},
{
x: 100,
y: 100,
isChange: true,
changeStationTime: 2,
parentId: 3,
parentId2: 1,
station: "1_3",
id: "1_3",
adjacent: [],
},
{
x: 60,
y: 180,
parentId: 3,
station: "3_4",
id: "3_4",
adjacent: ["1_3", "2_5"],
time: [4, 6],
},
{
x: 50,
y: 250,
isChange: true,
changeStationTime: 7,
parentId: 3,
parentId2: 2,
station: "2_5",
id: "2_5",
adjacent: [],
},
{
x: 50,
y: 280,
parentId: 3,
station: "3_6",
id: "3_6",
adjacent: ["1_8", "2_5"],
time: [10, 2],
},
{
x: 80,
y: 300,
parentId: 3,
station: "3_7",
id: "3_7",
adjacent: ["1_8", "2_5"],
time: [4, 8],
},
{
x: 85,
y: 340,
parentId: 3,
station: "3_8",
id: "3_8",
adjacent: ["1_8", "2_5"],
time: [2, 10],
},
{
x: 90,
y: 380,
parentId: 3,
parentId2: 1,
isChange: true,
station: "1_8",
id: "1_8",
adjacent: [],
},
{
x: 120,
y: 380,
parentId: 3,
station: "3_10",
id: "3_10",
adjacent: ["1_8"],
time: [2],
},
{
x: 150,
y: 380,
isEnd: true,
parentId: 3,
station: "3_11",
id: "3_11",
adjacent: ["1_8"],
time: [4],
},
],
},
// 3:'#ec7546',
// 4:'#19a319',
// 5:"#ff1919",
// 6:'#96356c',
// 7:'#52ac8b',
// 8:'#19aad1',
// 9:'#a3cd67',
// 13:'#b0c055',
// 14:'#803036',
// 18:"#1967bb",
// 21:"#22307a",
// 22:'#8c443d',
];
参考: 1.# [数据结构拾遗]图的最短路径算法