目录
实现内容
以南京地铁运营示意图为模板,实现任意两个站点之间最优路径导航的规划与动态展示效果。具体模板图片以及要求如下:
图1 南京地铁运营示意图
1. 存储南京地铁线路站点信息。
2. 给定起点站和终点站,假设相邻站点路径长度相等,求路径最短的地铁乘坐方案;
3. 给定起点站和终点站,假设相邻站点路径长度相等,求换乘次数最少的地铁乘坐方案,若存在多条换乘次数相同的乘坐方案,则给出换乘次数最少且路径长度最短的乘坐方案。
4. 在实际应用中,相邻站点的距离并不相等,假设中转站地铁停留时间为t1,非中转站地铁停留时间为T2,地铁换乘一次的时间消耗为T3(不考虑等待地铁的时间),地铁平均速度为v,相邻站点的路径长度已知,试求:在给定起点站和终点站的情况下,求乘坐时间最短的地铁乘坐方案。
5. 设计可视化的查询界面,对以上内容进行动态化展示。
地铁站点信息存储
分析需要的数据结构Station类:
Station类成员变量需要包含的有如下几个方面信息: 站名、逻辑地址、所属线路编号(表)、[临接站点]。对于这个类而言静态变量需要记录几张表项:所有站点目录、所有线路目录、逻辑地图、[临接站点的实际距离],具体临接站点实际距离是针对计算用时短单独设定的,暂时不做考虑。
对于该类对象的初始化需要生成相应的站点,并更新类的所有表项,较为繁琐,对于外界用户而言很容易考虑不周而造成表项更新重、漏现象,不利于数据维护。因此最好的方法是“仿照”单例模式,私有化构造器然后使用NewInstance(...)来生成。在NewInstance(...)函数中具体分析相应的参数是否需要生成新对象,是否需要对其参数表修改,是否需要去更新信息,提高了程序的健壮性。
对于类对象的判断是否相等采用了只判别站名的方式,因此站名成为了站的唯一标识符,这对于一个城市(几乎没有相同站名)而言是可行的(否则要加id)。因此重写equals方法和hashcode算法(对这个实现没啥用,可以删去),得到Station类。
其他一些set get方法设计等暂且隐去。
/**
* @author Kksp993
*/
public class Station {
/**
* station Map's width
*/
public static final int MAP_WIDTH = 40;
/**
* station name
*/
private String name = "";
/**
* station position on the screen
*/
private LogicalPoint loc;
/*
* the line number of the station
*/
private ArrayList<Integer> lineNums = new ArrayList<>();
/**
* register all the stations connected to this
*/
private ArrayList<Station> connStations = new ArrayList<>();
/**
* all station registered list
*/
private static ArrayList<Station> stations = new ArrayList<Station>();
/**
* all station registered logical address map
*/
private static Station[][] stationsMap = new Station[MAP_WIDTH][MAP_WIDTH];
/**
* Entry<key,value>==<lineNum,lineList>
*/
private static HashMap<Integer, ArrayList<Station>> line_Map = new HashMap<>();
//....
private Station(String name, LogicalPoint loc, int line_Num, ArrayList<Station> connStations) {
super();
this.name = name;
this.loc = loc;
this.connStations = connStations;
lineNums.add(line_Num);
stations.add(this);
stationsMap[loc.getX()][loc.getY()] = this;
}
/**
* do not add to list
*/
private Station(String name) {
this.name = name;
}
/**
* To getInstance forName.
*/
public static Station getInstance(String name) {
int idx = -1;
if (-1 != (idx = stations.indexOf(new Station(name)))) {
return stations.get(idx);
}
return new Station(name);
}
/**
* Singleton Pattern;To initial station without duplicated items.
* duplicated,update information and return the instance
* else,generate and return a new instance based on the information
* @return a station instance
*/
public static Station newInstance(String name, LogicalPoint loc, int line_Num, ArrayList<Station> connStations) {
int idx = -1;
if (-1 != (idx = stations.indexOf(new Station(name)))) {
Station station = stations.get(idx);
if (!station.getLineNums().contains(line_Num)) {
station.getLineNums().add(line_Num);
}
station.setLoc(loc);
for (Station connStation : connStations) {
if (!station.getConnStations().contains(connStation)) {
station.getConnStations().add(connStation);
}
}
if (loc.getX() != station.getLoc().getX() || loc.getY() !=
station.getLoc().getY()) {
System.err.println(station + "两次线路不一致!");
System.out.println("line_Num:" + line_Num);
System.out.println("pre:" + station.loc);
System.out.println("cur:" + loc);
}
return station;
}
return new Station(name, loc, line_Num, connStations);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Station other = (Station) obj;
if (name == null) {
if (other.name != null) {
return false;
}
} else if (!name.equals(other.name)) {
return false;
}
return true;
}
//....省略其他set get方法以及其他功能性方法
}
数据导入方案:
由于题目所给的站点信息均存放在图片中,难以像Excel、mySql等导入数据,需要手动编辑,工作量非常大。这里采用了分步导入的思想,通过名称和走向两个数据量,逐步、快捷地导入了所有站点的信息、坐标等,具体如下:
①按线路顺序依次导入所有站点的名字;
②按照数组顺序确定线路走向;
③根据起始站坐标和走向确定各个站点具体位置
④依照加入顺序双绑定确定相连站点和线路编号。
这样比起通常思维的直接导入数据,大大减少了输入的数据数量,降低了不必要的错误输入概率。由图知,一共有157个站点,上述数据最少也需要如下数据向量表:
而上述的优化输入方式仅需要输入157站点和方向信息,在站点信息n的数量级上,仅有两种数据——方向和名称(少于三种)。因此在处理更大范围数据的时候,该算法具有更大的优势。
方向信息的记录为:从北顺时针转一圈,分别为0,1,2,...,7(如下图所示)。其中表示原名称数组中第
个节点去第
个节点的路由方向。
* <li>7 0 1</li>
* <li>6 M 2</li>
* <li>5 4 3</li>
数据处理过程:
首先数据从好友获取敲好的数据(如左下图),是一个所有站名的数组。由于中转站只录入一次,不好配合方向信息,更改数组信息过于麻烦,索性先读入所有数据,转化为ArrayList的形式(如下图)。确定导入站点正确后,根据地图加入方向信息(如右下图)。
这个时候,所需最少数据已经导入系统中了,再创建几个标志型的参数表:
private static ArrayList<String> stationNames = new ArrayList<>();
private static int[] tags = new int[]{1, 2, 3, 4, 10, -1, -3, -7, -8, -9};
/**
* <li>7 0 1</li>
* <li>6 M 2</li>
* <li>5 4 3</li>
* 记录所有站点连接方向
*/
private static ArrayList<Integer> direction = new ArrayList<>();
/**
* 记录所有线路变更的跳变点
*/
private static ArrayList<Integer> breaks = new ArrayList<>();
/**
* 记录线路的起点逻辑坐标,依次是1, 2, 3, 4, 10, S1, S3, S7, S8, S9
*/
private static LogicalPoint[] staPoints = new LogicalPoint[]{new
LogicalPoint(14, 3), new LogicalPoint(26, 6),
new LogicalPoint(3, 4), new LogicalPoint(8, 8), new LogicalPoint(1, 18), new
LogicalPoint(13, 25),
new LogicalPoint(2, 25), new LogicalPoint(18, 28), new LogicalPoint(6, 5),
new LogicalPoint(13, 24)};
经过简单的计算、操作就可以得到完整的Station类表了。
private static void generateStationXMl() throws IOException {
Init();
//依照线路存储数据
for (int i = 0; i < breaks.size(); i++)
initialStation(breaks.get(i), i == breaks.size() - 1 ? direction.size() - 1 : breaks.get(i + 1) - 1, staPoints[i].getX(), staPoints[i].getY(), tags[i]);
//...
}
/**
* 按线路初始化站点信息
*/
private static void initialStation(int sta, int end, int px, int py, int
line_Num) {
LogicalPoint point = new LogicalPoint(px, py);
Station lastStation = Station.newInstance(stationNames.get(sta), point, line_Num, new ArrayList<Station>());
Station.setLine_Map(line_Num, 0, lastStation);
for (int i = sta + 1; i <= end; i++) {
// new a Station Instance, bind the lastStation and Overlay it.
lastStation = Station.newInstance(stationNames.get(i), LogicalPoint.forDirection(lastStation.getLoc(), direction.get(i)),
line_Num, new ArrayList<Station>()).bindStation(lastStation, line_Num);
Station.setLine_Map(line_Num, i - sta, lastStation);
}
}
打印以下Station类对象个数,可以看到有数字,说明导入成功了。
站点信息验证:
为了更加确切验证这157是不是真正想要的站点,是否算法存在不合理的地方,造成一些站点错位,这里最好做一下验证:
使用图像法检验,发现无法绘制,发现报了站点地址不一致的错误(getInstance(...)方法检测到了站点信息异常)。为什么会有错呢?仔细观察图片,发现是在下图箭头所示处:原图像为了美观,在个别站点一次绘制了两格的长度来连接线路。因为个别站点的不对,导致该线路后续所有站点均发生错位。具体加完节点后图像如下:(绘制方式详见后续GUI设计部分)
使用#1,#2...占位符事先添加所有虚节点(跨格子的点),在加入所有节点信息后,删除这些占位虚节点,并对受影响的所有编号、绑定信息等进行更新代码如下:
private static void generateStationXMl() throws IOException {
Init();
System.out.println("\t//" + 1);
for (int i = 0; i < breaks.size(); i++)
initialStation(breaks.get(i), i == breaks.size() - 1 ? direction.size() - 1 : breaks.get(i + 1) - 1, staPoints[i].getX(), staPoints[i].getY(), tags[i]);
ArrayList<Station> nullStation = new ArrayList<>();
for (Station station : Station.getStations())
nullStation.add(station);
for (Station station : nullStation) {
if (station.isTS())
Utils.printAllField(station);
if (station.getName().startsWith("#"))
for (int line_Num : station.getLineNums()) {
station.getConnStations().get(0).bindStation(station.getConnStations().get(1),
line_Num);
station.deleteStation();
}
}
}
再次检验所有节点是否异常,确认无误。
站点信息的xml读写:
这里主要刚学了xml操作,拿这个项目练一练手,所以选用了比较简单规则的xml记录方式。
dom4j学习相关链接:【Dom4j】Dom4j完整教程详解_Cyber-Drunker-CSDN博客_dom4j
dom4j包下载链接:dom4j
读取xml
读取xml需要按照从根元素依次去找标签的方式读取,建议对照xml里的DOC文件或者是xml的属性特征读取数据。
关键操作只有以下两个:
使用.element(name):访问当前变量下的标签名为name的变量。
使用.elementText(name):访问当前变量下的标签名为name变量的标签值。
一个返回的Element对象(可以循环调用);另一个是它的标签,是个String类型的变量。其他字符串操作最好使用StringBuilder,以免生成大量无用的字符串常量。
/**
* read XML file and get all station infomation
*
* @throws Exception
*/
public static void parseXml() throws Exception {
Document document = new SAXReader().read(new File("src/db/MetroInfo.xml"));
Element root = document.getRootElement();
Iterator<Element> iterator = root.elementIterator();
int px = 0, py = 0;
while (iterator.hasNext()) {
Element staElem = iterator.next();
px = Integer.parseInt(staElem.element("point").elementText("px"));
py = Integer.parseInt(staElem.element("point").elementText("py"));
String[] line_Nums = staElem.elementText("line_Num").split("#");
String[] connString = staElem.elementText("connStation").split("#");
String[] idxString = staElem.elementText("line_idx").split("#");
ArrayList<Station> connStation = new ArrayList<>();
for (String string : connString) {
connStation.add(Station.getInstance(string));
}
for (int i = 0; i < line_Nums.length; i++) {
Station station = Station.newInstance(staElem.elementText("name"),
new LogicalPoint(px, py), Integer.parseInt(line_Nums[i]), connStation);
Station.setLine_Map(Integer.parseInt(line_Nums[i]),
Integer.parseInt(idxString[i]), station);
}
}
HashMap<Integer, ArrayList<Station>> lm = Station.getLine_Map();
for (int lineNum : lm.keySet()) {
ArrayList<Station> line = lm.get(lineNum);
line.removeIf(Objects::isNull);
}
}
写入xml
写入xml和读基本是一致的,只是反过来,生成一个Element Tree。依旧有两个关键操作,具体如下:
使用.addElement(...):在该xml表中增加一个元素,返回添加的元素。
使用.addTest(...):为当前元素添加标签值。
生成完Element Tree后写入新的xml文件里,以备检验是否正确。写入xml选用prettyFormat就可以写出整齐的xml文件了,如果不用直接写入会造成所有xml信息在一行的问题(虽然IDE格式化一下就好了)。
/**
* update the changes to XML if possible
*
* @throws IOException
*/
public static void writeXml() throws IOException {
Document document = DocumentHelper.createDocument();
Element root = document.addElement("root");
for (Station station : Station.getStations()) {
// generate line number string
StringBuilder line_Num_string = new StringBuilder();
StringBuilder conn_Station_string = new StringBuilder();
StringBuilder line_idx_string = new StringBuilder();
for (int line_Num : station.getLineNums()) {
line_Num_string.append("#").append(line_Num);
line_idx_string.append("#").append(station.lineIdxOf(line_Num));
}
// remove the first "#"
line_Num_string = new StringBuilder(line_Num_string.substring(1));
line_idx_string = new StringBuilder(line_idx_string.substring(1));
for (Station connStation : station.getConnStations()) {
conn_Station_string.append("#").append(connStation);
}
// remove the first "#"
conn_Station_string = new StringBuilder(conn_Station_string.substring(1));
// generate the station element
Element staElem = root.addElement("Station");
staElem.addElement("name").addText(station.getName());
Element point = staElem.addElement("point");
point.addElement("px").addText(station.getLoc().getX() + "");
point.addElement("py").addText(station.getLoc().getY() + "");
staElem.addElement("line_Num").addText(line_Num_string.toString());
staElem.addElement("line_idx").addText(line_idx_string.toString());
staElem.addElement("connStation").addText(conn_Station_string.toString());
}
XMLWriter writer = new XMLWriter(new OutputStreamWriter(
new FileOutputStream(new File("src/db/MetroInfo2.xml")), "UTF-8"), OutputFormat.createPrettyPrint());
writer.write(document);
writer.close();
}
最后写入结果如下图:
好了,以上就是这一小节讲解的如何将地铁信息写入xml文件保存,方便后续读取的部分了。感谢大家的阅读,喜欢的朋友一键三连哦,下一节我们接着说路线推荐的算法实现。