关于如何存储信息、 最短路径算法的实现已经在之前章节说过,这里就不重复了,简单回顾一下实现内容。麻烦有需求的看官Tp到前一节:
【Java实现】南京地铁导航系统的简单实现(一)—— 存储站点信息_kksp993的博客-CSDN博客
【Java实现】南京地铁导航系统的简单实现(二)—— 最短路径算法的实现_kksp993的博客-CSDN博客
以南京地铁运营示意图为模板,实现任意两个站点之间最优路径导航的规划与动态展示效果。具体模板图片以及要求如下:
1. 存储南京地铁线路站点信息。
2. 给定起点站和终点站,假设相邻站点路径长度相等,求路径最短的地铁乘坐方案;
3. 给定起点站和终点站,假设相邻站点路径长度相等,求换乘次数最少的地铁乘坐方案,若存在多条换乘次数相同的乘坐方案,则给出换乘次数最少且路径长度最短的乘坐方案。
4. 在实际应用中,相邻站点的距离并不相等,假设中转站地铁停留时间为T1,非中转站地铁停留时间为T2,地铁换乘一次的时间消耗为T3(不考虑等待地铁的时间),地铁平均速度为v,相邻站点的路径长度已知,试求:在给定起点站和终点站的情况下,求乘坐时间最短的地铁乘坐方案。
5. 设计可视化的查询界面,对以上内容进行动态化展示。
(注明:此例使用Swing包写java图形化界面)
很多初学者(emm其实我也是初学者)会认为图形化界面很难做,然后在网上找到一些给了个JFrame生成了最基础窗体的程序然后只会改改宽高,对于监听、事件响应、布局管理器了解很少。这里想通过这个例子阐释这一部分的实现技巧。
首先说下原理,为了实现程序解耦,需要每个部件各司其职,对于我们这个项目而言,可以分为三个部分:窗口、地图组件、控制组件,基本上就可以按照功能简单分类。这样编程可以尽量减少维护运营的复杂程度,把大问题化成小问题而逐个击破,很方便。
(emmm,不要和我之前一样直接在JFrame上画图。。。)
实现功能:
(1)可以像高德地图/百度地图/腾讯地图/....完成基本的地图缩放、平移、选择等操作
(2)完成业务的可视化表示:
①用户选择站点,在下面红框中显示表示选为起始站/终点站
②根据右边按钮可以导航/清楚所选
③调用相关程序(上一节的程序)完成路径导航,获得导航路径
④通知地图模块绘制路线信息。
(3)其他特效功能。
先上代码,再进行解释:
package gui;
import db.ParseDom4J;
import javax.swing.*;
import java.awt.*;
@SuppressWarnings("serial")
public class MGFrame extends JFrame {
private static int frame_width = 640;
private static int frame_height = 800;
private static int frame_sX = (int) ((Toolkit.getDefaultToolkit().getScreenSize().getWidth() - frame_width) / 2);
private static int frame_sY = (int) ((Toolkit.getDefaultToolkit().getScreenSize().getHeight() - frame_height) / 2);
private static MapPanel mapPanel = new MapPanel(540, 540);
private static ControlPanel controlPanel = new ControlPanel();
public MGFrame() {
setLocation(frame_sX, frame_sY);
setSize(frame_width, frame_height);
setTitle("MetroGuide");
setResizable(false);
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
setVisible(true);
setLayout(new FlowLayout(FlowLayout.CENTER, 5, 40));
add(mapPanel);
add(controlPanel);
}
public static void main(String[] args) throws Exception {
ParseDom4J.main(args);
JFrame mGFrame = new MGFrame();
}
public static ControlPanel getControlPanel() {
return controlPanel;
}
public static MapPanel getMapPanel() {
return mapPanel;
}
}
继承自JFrame的类,GUI最好与核心包分开。不要写在一起,很混乱不易维护。
public class MGFrame extends JFrame {}
这一部分是表示窗体的宽高,(sX,sY)表示窗体左上角顶点的坐标。由于Java是以左上角屏幕点为(0,0),横向构成x轴,竖向构成y轴,以1像素为分度值,屏幕上的点坐标值均为非负数。下面的公式(其实正常拿个笔画一画就可以明白,把(sX,sY)设置为使得窗体上下左右留白对称即可)能够使得窗体居中。
private static int frame_width = 640; private static int frame_height = 800; private static int frame_sX = (int) ((Toolkit.getDefaultToolkit().getScreenSize().getWidth() - frame_width) / 2); private static int frame_sY = (int) ((Toolkit.getDefaultToolkit().getScreenSize().getHeight() - frame_height) / 2);
窗体类的构造函数。由于该类是窗体,一旦该类生成了,程序员就一定想使它以最优的形态展现在用户面前,因此把初始化部分写在构造函数里就好了。
这部分可写的代码其实很多,也很复杂,但是常用的就这些:
setLocation(frame_sX, frame_sY);设置左上角顶点的坐标值(这个之前已经已经写好了field值了,所以直接可以用(这样做可以方便程序更改窗体大小))。
setSize(frame_width, frame_height);设置宽高(其实这两步可以由setBounds(...)一步实现)
(如果你设置完没有反应,换成setPreferredSize(...),这个函数优先级会高一些)
setTitle("MetroGuide");设置窗体标题栏的名称,一般是应用名。
setResizable(false);使得窗体不能改变大小,什么意思?就是说你不能通过把鼠标放在窗口边界上,然后鼠标拖拽使得窗体的大小发生改变。这个是默认置true的,但是一般情况下改变大小不利于内部组件的大小设置,可能会有横向纵向拉伸,就会不好看。对于我这个不想让用户改变窗体大小的程序,置false。
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);这句你写上就好了。什么意思呢?写上这个是如果把窗体关了,就是点击右上角的红叉叉把窗口关了,会同时把程序也关了,否则如果不写的话,程序会还在运行。
setVisible(true);这句你写上就好了。什么意思?就是说设置窗口可见,默认是不可见的,也就是说,你不写,这个窗口你就看不见,也点不着。
setLayout(new FlowLayout(FlowLayout.CENTER, 5, 40));这个是设置了一个流式布局,流式布局听起来高大上难以理解,其实不然。流式布局关键在“流”,“流”译之为水流。夫水迂回而前进,遇礁石而折返。流式布局就好比水流,向着某一个方向摆放组件直到撞到该容器的容器壁,然后折返回来(如果是横着的,那么就好比你在word中打字,从左往右,到行尾(也就是容器壁),折返回下一行开头,那么横向的流式布局就是这样的布局规定)。
FlowLayout.CENTER每行中的组件居中,属于align属性。具体参见:
关于java中FlowLayout(流布局管理器)中的常量LEADING等问题_星月昭铭的博客-CSDN博客
5,40表示horizontal gap(hgap)与vertical gap(vgap)的值,hgap是横向组件间距为5像素,vgap表示行间距为40像素。
add(mapPanel); add(controlPanel);加入组件,这个时候是后面需要用的组件,都用add方法加入容器。
public MGFrame() { setLocation(frame_sX, frame_sY); setSize(frame_width, frame_height); setTitle("MetroGuide"); setResizable(false); setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); setVisible(true); setLayout(new FlowLayout(FlowLayout.CENTER, 5, 40)); add(mapPanel); add(controlPanel); }
主函数 首先执行业务逻辑,然后绘制UI界面,因为JFrame不点关它不关,所以这里不需要设置死循环之类的。
public static void main(String[] args) throws Exception { ParseDom4J.main(args); JFrame mGFrame = new MGFrame(); }
其他setget方法就不说了。
emmm,代码有点长,我先放上来,挑重点讲:
(ps.虽然我也明白,很多人见到代码就溜了....emmm,再看看?)
package gui;
import core.LogicalPoint;
import core.Station;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;
import java.util.HashMap;
public class MapPanel extends JPanel {
private static int blockwidth = 20;
private static final int BST_BLOCK_WIDTH = 20;
private static final int MAX_BLOCK_WIDTH = 60;
private static final int MIN_BLOCK_WIDTH = 15;
private int width;
private int height;
private static final Point BST_BASEPOINT = new Point(0, 0);
private Point basePoint = new Point(0, 0);
private static HashMap colors = new HashMap<>();
private static boolean isShowing = false;
private static boolean isViewPort = false;
private static ArrayList path;
private static int process = 0;
public MapPanel(int width, int height) {
setPreferredSize(new Dimension(width, height));
setBackground(Color.white);
setAllcolor();
addMouseListener(new MouseListener() {
@Override
public void mouseClicked(MouseEvent e) {
endShowing();
Point phyClickP = e.getPoint();
LogicalPoint lgcClickP = getLogicalPoint(phyClickP);
Station station = Station.getStationforAddr(lgcClickP);
if (station == null) return;
System.out.println(station);
ControlPanel.selectStation(station);
}
@Override
public void mousePressed(MouseEvent e) {}
@Override
public void mouseReleased(MouseEvent e) {}
@Override
public void mouseEntered(MouseEvent e) {}
@Override
public void mouseExited(MouseEvent e) {}
});
addMouseMotionListener(new MouseMotionListener() {
private Point lastPoint;
@Override
public void mouseDragged(MouseEvent e) {
Point curPoint = e.getPoint();
int dx = curPoint.x - lastPoint.x;
int dy = curPoint.y - lastPoint.y;
basePoint.x += dx;
basePoint.y += dy;
lastPoint = curPoint;
repaint();
}
@Override
public void mouseMoved(MouseEvent e) {
lastPoint = e.getPoint();
}
});
addMouseWheelListener(new MouseWheelListener() {
@Override
public void mouseWheelMoved(MouseWheelEvent e) {
// 如果车轮旋转值为负,则表示向上旋转,而
// 正值表示向下旋转
if (e.getWheelRotation() < 0) {
ModifyView(1, e.getPoint());
} else if (e.getWheelRotation() > 0) {
ModifyView(-1, e.getPoint());
}
}
});
}
/**
* 缩放屏幕窗口
* @param dx 缩放力度,>0放大,<0缩小,dx值影响blockWidth大小
* @param center 缩放中心
*/
private void ModifyView(int dx, Point center) {
if (dx == 0) return;
if (blockwidth > MAX_BLOCK_WIDTH && dx > 0) return;
if (blockwidth < MIN_BLOCK_WIDTH && dx < 0) return;
basePoint.x += dx / Math.abs(dx) * (basePoint.x - center.getX()) / blockwidth;
basePoint.y += dx / Math.abs(dx) * (basePoint.y - center.getY()) / blockwidth;
blockwidth += dx;
repaint();
}
/**
* 开启isViewPort标志,对视口进行平移缩放,得到最佳观测视角。
* 当前设置的缩放倍率为2/3,级数求和能够缩放到指定位置,误差为1像素。
*/
private void viewPort() {
isViewPort = true;
int dx = blockwidth - BST_BLOCK_WIDTH;
if (dx != 0)
blockwidth -= dx / 1.5;
if (!basePoint.equals(BST_BASEPOINT)) {
basePoint.translate((int) ((BST_BASEPOINT.x - basePoint.x) / 1.5), (int) ((BST_BASEPOINT.y - basePoint.y) / 1.5));
}
if (Math.abs(dx) < 2 && Math.abs(basePoint.x - BST_BASEPOINT.x) < 2 && Math.abs(basePoint.y - BST_BASEPOINT.y) < 2) {
isViewPort = false;
}
}
/**
* 动态显示path路线
* @param path 需要显示的path路线
*/
public static void showPathNavigation(ArrayList path) {
isShowing = true;
MapPanel.path = path;
MGFrame.getMapPanel().repaint();
}
private void setAllcolor() {
colors.put(1, new Color(0x2897E6));
colors.put(2, new Color(0xE82A2A));
colors.put(3, new Color(0x15B612));
colors.put(4, new Color(0xA513C0));
colors.put(10, new Color(0xE6BE80));
colors.put(-1, new Color(0x27D4C1));
colors.put(-3, new Color(0xDA60CD));
colors.put(-7, new Color(0xDD8699));
colors.put(-8, new Color(0xFB631A));
colors.put(-9, new Color(0xFFC500));
}
@Override
public void paint(Graphics g) {
super.paint(g);
if (isShowing)
viewPort();
paintMap(g);
}
private void paintMap(Graphics g) {
Station[][] stationMap = Station.getStationsMap();
for (int lineNum : Station.getLine_Map().keySet()) {
drawStationLine(g, lineNum, !isShowing);
if (isShowing) {
drawShowingline(g, path);
}
}
for (Station[] stations : stationMap) {
for (Station station : stations) {
drawStation(g, station);
}
}
}
/**
* 绘制一个站点
* @param g 画笔
* @param station 站点
*/
private void drawStation(Graphics g, Station station) {
if (station != null) {
int smallOvalRadium = 2 + blockwidth / 18;
Point phyPoint = getPhysicalPoint(station.getLoc());
//中转站绘制白圈,其他绘制黑点
if (!station.isTS()) {
g.fillOval((int) phyPoint.getX() - smallOvalRadium, (int) phyPoint.getY() - smallOvalRadium
, smallOvalRadium * 2, smallOvalRadium * 2);
} else {
g.setColor(Color.white);
g.fillOval((int) phyPoint.getX() - smallOvalRadium, (int) phyPoint.getY() - smallOvalRadium
, smallOvalRadium * 2, smallOvalRadium * 2);
g.setColor(Color.black);
g.drawOval((int) phyPoint.getX() - smallOvalRadium, (int) phyPoint.getY() - smallOvalRadium
, smallOvalRadium * 2, smallOvalRadium * 2);
}
//判断上下左右,绘制站名
if (station.getStationRight() == null)
g.drawString(station.toString().substring(0, Math.min(blockwidth / 15, station.toString().length())), (int) phyPoint.getX() + 5,
(int) phyPoint.getY() + 5);
else if (!station.isOccpyUpLeft())
g.drawString(station.toString().substring(0, Math.min(blockwidth / 15, station.toString().length())), (int) phyPoint.getX() - smallOvalRadium - 5,
(int) phyPoint.getY() - 2 * smallOvalRadium);
else if (station.getStationLeft() == null)
g.drawString(station.toString().substring(0, Math.min(blockwidth / 15, station.toString().length())), (int) phyPoint.getX() - blockwidth + 5,
(int) phyPoint.getY() + 5);
else if (station.getStationDowm() == null)
g.drawString(station.toString().substring(0, Math.min(blockwidth / 15, station.toString().length())), (int) phyPoint.getX() - smallOvalRadium - 5,
(int) phyPoint.getY() + 2 * smallOvalRadium + 10);
}
}
/**
* 绘制线路
* @param g 画笔
* @param lineNum 线路编号
* @param isColored 是否上色
*/
private void drawStationLine(Graphics g, int lineNum, boolean isColored) {
ArrayList line = Station.getLine_Map().get(lineNum);
Point curPoint, lastPoint = getPhysicalPoint(line.get(0).getLoc());
g.setColor(isColored ? colors.get(lineNum) : Color.gray);
drawEdge(g, line, lastPoint);
}
/**
* 绘制选中路线
* @param g 画笔
* @param line 选中的线路
*/
private void drawShowingline(Graphics g, ArrayList line) {
Point curPoint, lastPoint = getPhysicalPoint(line.get(0).getLoc());
drawProcessEdge(g, line, lastPoint, 1.0 * process / 100);
if (process < 99 && !isViewPort) process++;
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
repaint();
}
/**
* 绘制一条边
* @param g 画笔
* @param line 线路
* @param lastPoint 路由节点,用于判断是几号线
*/
private void drawEdge(Graphics g, ArrayList line, Point lastPoint) {
drawProcessEdge(g, line, lastPoint, 1);
}
/**
* 绘制一条渐进的线
* @param g 画笔
* @param line 绘制数组
* @param lastPoint 上一路由节点
* @param process 进度条
*/
private void drawProcessEdge(Graphics g, ArrayList line, Point lastPoint, double process) {
Point curPoint;
((Graphics2D) g).setStroke(new BasicStroke(blockwidth / 6, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
for (int i = 1; i < line.size() * process && line.get(i) != null; i++) {
if (isShowing & process < 1) {
g.setColor(colors.get(line.get(i).commonLineNum(line.get(i - 1))));
((Graphics2D) g).setStroke(new BasicStroke(blockwidth / 4, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
}
curPoint = getPhysicalPoint(line.get(i).getLoc());
g.drawLine(lastPoint.x, lastPoint.y, curPoint.x, curPoint.y);
lastPoint = curPoint;
}
((Graphics2D) g).setStroke(new BasicStroke(1.0f));
g.setColor(Color.BLACK);
}
/**
* 结束当前高亮显示
*/
public void endShowing() {
if (isShowing) {
isShowing = false;
process = 0;
}
}
/**
* 获得具体画在panel上的点
* @param lgPoint 站点矩阵的逻辑点
* @return 具体画在panel上的点
*/
private Point getPhysicalPoint(LogicalPoint lgPoint) {
return new Point((int) (lgPoint.getX() * blockwidth + basePoint.getX()),
(int) (lgPoint.getY() * blockwidth + basePoint.getY()));
}
/**
* 获得panel上相应点对应的逻辑点
* @param point 具体画在panel上的点
* @return 站点矩阵对应的逻辑点
*/
private LogicalPoint getLogicalPoint(Point point) {
return new LogicalPoint((int) Math.floor((point.getX() - basePoint.getX()) / blockwidth + 0.5),
(int) Math.floor((point.getY() - basePoint.getY()) / blockwidth + 0.5));
}
}
首先讲一下这种地图绘制的原理。首先一个组件在窗口上显示,需要有明确的参考系,对于参考系默认是该组件左上角点为(0,0)。但是,很显然,这个参考点动不了,不够灵活。例如:中国地图,如果我想聚焦北京市,那么我应当只显示北京市的地图,而不会把从新疆一直到北京都显示出来。因为你的参考系是死的,也就是(0,0)动不了,如果非要这样调整就需要调整整个图片各点坐标。(北京以西的地方x坐标都是负的,对于事件监听需要重新定位比较复杂)。
这里采用的是基于参考点的坐标系,也就是说所有图形绘制基于的是参考点BasePoint,那么问题就简单了:(这个参考点相对于(0,0)而言,是可以改变的可以变换的)
(1)对于题目中棋盘状的点阵形式,可以使用(m,n)一一映射到具体的参考位置上:
其中表示棋盘上的逻辑点位置,比如迈皋桥是
,对应初始状态下的物理坐标为
如果
为
的话;同样,点击时,只要x,y都在该点左右1/2间距以内都算点到这个格点的,因此四舍五入需要加个0.5。这样可以一一对应了,后面就不需要再为转化而烦恼了。
(2)对于用户平移类操作,可以直接在上修改,绘制时所有点自动向
对齐:
(3)对于用户缩放类操作,需要基于鼠标位置和滚轮进度对和blockwidth进行修正:
当用户以鼠标某一位置缩放地图时,用户实际想要看清楚鼠标所指之处——以鼠标为中心进行缩放。这样会造成基准点的移动,需要以中心点为参考点更改基准点的坐标。
对于鼠标在图像C点,基准点在,由于缩放(以blockwidth自增为例),
的
长度增加了原来的1/blockwidth倍(例如:一开始是blockwidth是20,那么当所有间距扩大一格时,变为原来的1.05倍),但相对方向不变,因此可以以此更新基准点坐标。
具体而言,新的基准点满足如下公式:
其中,表示鼠标位置,
表示基准点位置,
表示缩放完成后的
。(emmm,上面公式好像打成Pb了,和bp一个意思。)
这个交互是通过监听实现的,采用匿名内部类的形式重写相关抽象方法,实现相应功能:
(1)添加鼠标点击
addMouseListener(new MouseListener() { @Override public void mouseClicked(MouseEvent e) { endShowing(); Point phyClickP = e.getPoint(); LogicalPoint lgcClickP = getLogicalPoint(phyClickP); Station station = Station.getStationforAddr(lgcClickP); if (station == null) return; System.out.println(station); ControlPanel.selectStation(station); } @Override public void mousePressed(MouseEvent e) {} @Override public void mouseReleased(MouseEvent e) {} @Override public void mouseEntered(MouseEvent e) {} @Override public void mouseExited(MouseEvent e) {} });
由于没法不写其他4个方法,所以就写个空方法。对于点击,我们的操作可以获得该站点。由于之前说过,我们是以名称相同为站点的唯一标识符,因此通过上述定位方法,获取站点,看是哪一站。之后就是相应的处理了(这里点完交给控制模块处理(控制模块用点击的信息作为导航起点/终点,交由上一节的最短路线程序寻找路线,再在此地图上绘制出来,即可完成人机交互))
(2)添加鼠标拖拽监听
addMouseMotionListener(new MouseMotionListener() { private Point lastPoint; @Override public void mouseDragged(MouseEvent e) { Point curPoint = e.getPoint(); int dx = curPoint.x - lastPoint.x; int dy = curPoint.y - lastPoint.y; basePoint.x += dx; basePoint.y += dy; lastPoint = curPoint; repaint(); } @Override public void mouseMoved(MouseEvent e) { lastPoint = e.getPoint(); } });
这一段两个重写方法分别对应如下两个鼠标操作:
①: mouseDragged鼠标拖拽:当你摁下鼠标,并拽动的时候,它会每隔一会采样一次鼠标的位置,执行该方法。
②: mouseMoved 鼠标移动:与上面相对应,如果你鼠标移动,但你没有摁下,那么会执行这个方法;当鼠标摁下后,就不会执行此方法。
那么拖拽功能实现如下:
拖拽的时候需要实时更新,所以需要记录上一帧鼠标的位置。两者的偏移量表示用户想要把地图拽到鼠标当前位置,由于地图发生的变化是线性的,不会发生畸变,因此对于地图上每个点都会跟着鼠标移动到目标位置,移动量就是鼠标偏移量。那么很显然基准点也是地图上的点,所以只需要将基准点加上这个便宜即可。
对于第一下拖拽,由于lastPoint没有实时更新,所以点一下就会将地图瞬间闪到很远的地方,这种情况需要解决。一般的方法是第一帧舍弃,但不好编写。这里在mouseMoved函数中一直默认更新当前点,由于程序启动瞬间用户不可能直接拖拽(人毕竟是人,有反应时间的),所以相当于lastPoint一直是更新的了,不会有bug。
注意repaint()
(3)添加鼠标滚轮监听
addMouseWheelListener(new MouseWheelListener() { @Override public void mouseWheelMoved(MouseWheelEvent e) { // 如果车轮旋转值为负,则表示向上旋转,而 // 正值表示向下旋转 if (e.getWheelRotation() < 0) { ModifyView(1, e.getPoint()); } else if (e.getWheelRotation() > 0) { ModifyView(-1, e.getPoint()); } } });
/** * 缩放屏幕窗口 * @param dx 缩放力度,>0放大,<0缩小,dx值影响blockWidth大小 * @param center 缩放中心 */ private void ModifyView(int dx, Point center) { if (dx == 0) return; if (blockwidth > MAX_BLOCK_WIDTH && dx > 0) return; if (blockwidth < MIN_BLOCK_WIDTH && dx < 0) return; basePoint.x += dx / Math.abs(dx) * (basePoint.x - center.getX()) / blockwidth; basePoint.y += dx / Math.abs(dx) * (basePoint.y - center.getY()) / blockwidth; blockwidth += dx; repaint(); }
这里由于放大缩小基本上是同构的,因此用一个方法就可以同意求解。
mouseWheelMoved:当滚轮运动时,响应事件。
e.getWheelRotation:如果车轮旋转值为负,则表示向上旋转,而正值表示向下旋转
e.getScrollAmount():滚轮滚动值。这个由操作系统决定,一般没改过就是3.
缩放函数已经在上面讲过了,这就不说了。
注意repaint()
@Override public void paint(Graphics g) { super.paint(g); if (isShowing) viewPort(); paintMap(g); }
绘图主要在这个方法里写。这个函数不要把整个所有代码全写进去,因为这样你很快就会乱掉了,应该写成一个个子函数,然后调用他们,这样能够看到画图的所有流水过程。
一般绘图有以下一些方法供参考:
g.setColor(Color.white);
设置画笔颜色为白色,画笔属性会影响到整个绘制过程,比如画了个线,线会变颜色变粗细!
(用之前记得保存原来的画笔,用完再把它洗掉,还原成原来的样子,否则你其他地方也用了这样的笔触,就很烦)
((Graphics2D) g).setStroke(new BasicStroke(3.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
设置粗细 3.0f,使用BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND参数。这个基本上就改改粗细就好了,就是那个3.0f(具体看链接)
g.drawLine(lastPoint.x, lastPoint.y, curPoint.x, curPoint.y);
划线
g.fillOval(100,100,40,40);g.drawOval(100,100,40,40);
fill族和draw族分别是画全涂色和边缘涂色的图形。
前面的是图形(外接正方形)左上角的点,后面的是宽高。
相关参考:
BasicStroke的用法_李腾飞的专栏-CSDN博客_basicstroke
其他的程序设计就是你自己想画什么画什么了/^.^/
这部分就比较简单了,没什么特别的,完全根据自己的想法一个个放置就好了。
package gui;
import core.PathHelper;
import core.Station;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
public class ControlPanel extends JPanel {
private static Station originStation;
private static Station terminal;
private static JLabel osLabel;
private static JLabel tsLabel;
private JButton ClearBtn;
private JButton NaviBtn;
private JButton lstTsBtn;
private JButton weightBtn;
public ControlPanel() {
setPreferredSize(new Dimension(600, 130));
setBackground(Color.red);
setLayout(new FlowLayout(FlowLayout.LEADING, 25, 15));
osLabel = initLabel("", 180, 45);
osLabel.setBorder(BorderFactory.createLineBorder(Color.black));
tsLabel = initLabel("", 180, 45);
tsLabel.setBorder(BorderFactory.createLineBorder(Color.black));
ClearBtn = initButton("清除", 100, 45);
NaviBtn = initButton("站点少", 100, 45);
lstTsBtn = initButton("换乘少", 100, 45);
weightBtn = initButton("时间短", 100, 45);
add(initLabel("起始站:", 90, 45));
add(osLabel);
add(NaviBtn);
add(lstTsBtn);
add(initLabel("终点站:", 90, 45));
add(tsLabel);
add(weightBtn);
add(ClearBtn);
ClearBtn.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
MGFrame.getMapPanel().endShowing();
clearInfo();
repaint();
}
});
NaviBtn.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
NaviEventPerformed(ClearBtn, 1);
}
});
lstTsBtn.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
NaviEventPerformed(ClearBtn, 100);
}
});
weightBtn.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
NaviEventPerformed(ClearBtn, 2);
}
});
}
/**
* 导航开始
* @param ClearBtn 清楚按钮对象,方便运行玩清除
* @param TS_Non 导航功能参数
*/
private static void NaviEventPerformed(JButton ClearBtn, int TS_Non) {
MGFrame.getMapPanel().endShowing();
if (originStation == null || terminal == null || originStation.equals(terminal)) {
ClearBtn.doClick();
return;
}
PathHelper.PathNavigation(originStation, terminal, TS_Non);
ArrayList path = PathHelper.getPath();
int showConfirmDialog = JOptionPane.showConfirmDialog(null, "导航已完成:共" + path.size() + "站\n是否查看导航", "MetroGuide为您导航", JOptionPane.YES_NO_OPTION);
//当我们点击"是",返回值为0;
//当我们点击"否",返回值为1;
//当我们点击"×",关闭了选择框,此时返回值为-1.
if (showConfirmDialog == 0)
MapPanel.showPathNavigation(path);
else
ClearBtn.doClick();
}
/**
* 提供MapPanel 选取站点的函数
* @param station 被选择的站,第一次选入的是起始站,第二次选入的是终点站
* ,第三次则会认为是误操作,清楚所有站。
*/
public static void selectStation(Station station) {
if (originStation == null) {
originStation = station;
osLabel.setText(station.toString());
} else if (terminal == null) {
terminal = station;
tsLabel.setText(station.toString());
} else {
MGFrame.getControlPanel().clearInfo();
}
}
/**
* 生成一个JLabel
* @param s 标题
* @param width 宽
* @param height 高
* @return JLabel
*/
private JLabel initLabel(String s, int width, int height) {
JLabel label = new JLabel(s, JLabel.CENTER);
label.setBackground(Color.red);
label.setPreferredSize(new Dimension(width, height));
label.setFont(new Font("微软雅黑", Font.BOLD, 24));
return label;
}
/**
* 生成一个Jutton
* @param s 标题
* @param width 宽
* @param height 高
* @return JButton
*/
private JButton initButton(String s, int width, int height) {
JButton button = new JButton(s);
button.setPreferredSize(new Dimension(width, height));
button.setFont(new java.awt.Font("华文行楷", 1, 20));
button.setBackground(Color.red);
return button;
}
@Override
public void paint(Graphics g) {
super.paint(g);
}
public void clearInfo() {
originStation = null;
terminal = null;
osLabel.setText("");
tsLabel.setText("");
}
}
主要说说这部分:
ClearBtn.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { MGFrame.getMapPanel().endShowing(); clearInfo(); repaint(); } }); NaviBtn.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { NaviEventPerformed(ClearBtn, 1); } }); lstTsBtn.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { NaviEventPerformed(ClearBtn, 100); } }); weightBtn.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { NaviEventPerformed(ClearBtn, 2); } });
这边是每个摁键给它装上自己的监听,对应摁下之后会执行actionPerformed函数
/** * 导航开始 * @param ClearBtn 清楚按钮对象,方便运行玩清除 * @param TS_Non 导航功能参数 */ private static void NaviEventPerformed(JButton ClearBtn, int TS_Non) { MGFrame.getMapPanel().endShowing(); if (originStation == null || terminal == null || originStation.equals(terminal)) { ClearBtn.doClick(); return; } PathHelper.PathNavigation(originStation, terminal, TS_Non); ArrayListpath = PathHelper.getPath(); int showConfirmDialog = JOptionPane.showConfirmDialog(null, "导航已完成:共" + path.size() + "站\n是否查看导航", "MetroGuide为您导航", JOptionPane.YES_NO_OPTION); //当我们点击"是",返回值为0; //当我们点击"否",返回值为1; //当我们点击"×",关闭了选择框,此时返回值为-1. if (showConfirmDialog == 0) MapPanel.showPathNavigation(path); else ClearBtn.doClick(); }
这个部分抽出来写,否则很多代码是一样的。首先进行一些简单的提升健壮性的判断。是否正在导航?(这里直接给它摁了,你没结束我也强制结束)是否起点终点都有且不相同?都符合,我们进行导航——调用章节(二)提供的接口(emmm不是interface),得到最佳路径,弹出如下人性化对话框:
点击是了,我们就提供导航,在之前地图中动态显示路线,否则我们就不管它。无论如何,都需要清空该次导航信息,方便下次导航。(如果有心也可以做个保存历史的,存在某个info文件里,每次读就好了)
这里的动态效果做的一般,就不说了。准确来说只要给用户慢慢显示路线延伸方向就好。
以下为控制台打印信息:
(注:其中dp_cost分别表示站点数量、换乘加权得分、用时分钟,因数据不足,模型假设地铁配速60km/h,上下站1分钟,中转站等待3分钟,可能与实际有所偏差)
仙林中心
步月路
------------start--------------1.0
仙林中心= [ 金马路 , 大行宫 , 新街口 , 元通 , 油坊桥 ]
步月路= [ 油坊桥 , 南京南站 ]
dp_tags= [ 仙林中心 , 金马路 , 大行宫 , 新街口 , 元通 , 油坊桥 , 油坊桥 , 南京南站 , 步月路 ]
dp_path= [ 仙林中心 , 仙林中心 , 仙林中心 , 仙林中心 , 仙林中心 , 仙林中心 , 新街口 , 大行宫 , 油坊桥 ]
dp_cost= [ 0.0 , 3.0 , 11.0 , 12.0 , 20.0 , 22.0 , 21.0 , 19.0 , 29.0 ]
path= [ 仙林中心 , 学则路 , 仙鹤门 , 金马路 , 马群 , 钟灵街 , 孝陵卫 , 下马坊 , 苜蓿园 , 明故宫 , 西安门 , 大行宫 , 新街口 , 张府园 , 三山街 , 中华门 , 安德门 , 小行 , 中胜 , 元通 , 雨润大街 , 油坊桥 , 中和街 , 黄河路 , 天河路 , 新梗街 , 天保路 ,生态科技园 , 滨江村 , 步月路 ]
-------------end---------------
------------start--------------10.0
仙林中心= [ 金马路 , 大行宫 , 新街口 , 元通 , 油坊桥 ]
步月路= [ 油坊桥 , 南京南站 ]
dp_tags= [ 仙林中心 , 金马路 , 大行宫 , 新街口 , 元通 , 油坊桥 , 油坊桥 , 南京南站 , 步月路 ]
dp_path= [ 仙林中心 , 仙林中心 , 仙林中心 , 仙林中心 , 仙林中心 , 仙林中心 , 油坊桥 , 大行宫 , 油坊桥 ]
dp_cost= [ 0.0 , 21.0 , 29.0 , 30.0 , 38.0 , 40.0 , 40.0 , 55.0 , 48.0 ]
path= [ 仙林中心 , 学则路 , 仙鹤门 , 金马路 , 马群 , 钟灵街 , 孝陵卫 , 下马坊 , 苜蓿园 , 明故宫 , 西安门 , 大行宫 , 新街口 , 上海路 , 汉中门 , 莫愁湖 , 云锦路 ,集庆门大街 , 兴隆大街 , 奥体东 , 元通 , 雨润大街 , 油坊桥 , 中和街 , 黄河路 , 天河路 , 新梗街 , 天保路 ,生态科技园 , 滨江村 , 步月路 ]
-------------end---------------
------------start--------------2.0
仙林中心= [ 金马路 , 大行宫 , 新街口 , 元通 , 油坊桥 ]
步月路= [ 油坊桥 , 南京南站 ]
dp_tags= [ 仙林中心 , 金马路 , 大行宫 , 新街口 , 元通 , 油坊桥 , 油坊桥 , 南京南站 , 步月路 ]
dp_path= [ 仙林中心 , 仙林中心 , 仙林中心 , 仙林中心 , 仙林中心 , 仙林中心 , 金马路 , 大行宫 , 南京南站 ]
dp_cost= [ 0.0 , 9.2 , 27.9 , 32.2 , 55.7 , 63.5 , 61.1 , 46.8 , 89.0 ]
path= [ 仙林中心 , 学则路 , 仙鹤门 , 金马路 , 马群 , 钟灵街 , 孝陵卫 , 下马坊 , 苜蓿园 , 明故宫 , 西安门 , 大行宫 , 常府街 , 夫子庙 , 武定门 , 雨花门 , 卡子门 , 大明路 , 明发广场 , 南京南站 , 景明佳园 ,铁心桥大街 , 春江新城 , 华新路 , 油坊桥 , 中和街 , 黄河路 , 天河路 , 新梗街 , 天保路 ,生态科技园 , 滨江村 , 步月路 ]
-------------end---------------
以下为控制台打印信息:
仙林中心
柳州东路
------------start--------------1.0
仙林中心= [ 金马路 , 大行宫 , 新街口 , 元通 , 油坊桥 ]
柳州东路= [ 泰冯路 , 南京站 , 鸡鸣寺 , 大行宫 , 南京南站 ]
dp_tags= [ 仙林中心 , 金马路 , 大行宫 , 新街口 , 元通 , 油坊桥 , 泰冯路 , 南京站 , 鸡鸣寺 , 大行宫 , 南京南站 , 柳州东路 ]
dp_path= [ 仙林中心 , 仙林中心 , 仙林中心 , 仙林中心 , 仙林中心 , 仙林中心 , 金马路 , 金马路 , 金马路 , 大行宫 , 大行宫 , 鸡鸣寺 ]
dp_cost= [ 0.0 , 3.0 , 11.0 , 12.0 , 20.0 , 22.0 , 18.0 , 12.0 , 10.0 , 11.0 , 19.0 , 16.0 ]
path= [ 仙林中心 , 学则路 , 仙鹤门 , 金马路 ,苏宁总部·徐庄 , 聚宝山 , 王家湾 , 蒋王庙 , 岗子村 , 九华山 , 鸡鸣寺 , 新庄 , 南京站 , 小市 , 五塘广场 , 上元门 , 柳州东路 ]
-------------end---------------
------------start--------------10.0
仙林中心= [ 金马路 , 大行宫 , 新街口 , 元通 , 油坊桥 ]
柳州东路= [ 泰冯路 , 南京站 , 鸡鸣寺 , 大行宫 , 南京南站 ]
dp_tags= [ 仙林中心 , 金马路 , 大行宫 , 新街口 , 元通 , 油坊桥 , 泰冯路 , 南京站 , 鸡鸣寺 , 大行宫 , 南京南站 , 柳州东路 ]
dp_path= [ 仙林中心 , 仙林中心 , 仙林中心 , 仙林中心 , 仙林中心 , 仙林中心 , 大行宫 , 大行宫 , 金马路 , 大行宫 , 大行宫 , 大行宫 ]
dp_cost= [ 0.0 , 21.0 , 29.0 , 30.0 , 38.0 , 40.0 , 57.0 , 51.0 , 46.0 , 29.0 , 55.0 , 37.0 ]
path= [ 仙林中心 , 学则路 , 仙鹤门 , 金马路 , 马群 , 钟灵街 , 孝陵卫 , 下马坊 , 苜蓿园 , 明故宫 , 西安门 , 大行宫 , 浮桥 , 鸡鸣寺 , 新庄 , 南京站 , 小市 , 五塘广场 , 上元门 , 柳州东路 ]
-------------end---------------
------------start--------------2.0
仙林中心= [ 金马路 , 大行宫 , 新街口 , 元通 , 油坊桥 ]
柳州东路= [ 泰冯路 , 南京站 , 鸡鸣寺 , 大行宫 , 南京南站 ]
dp_tags= [ 仙林中心 , 金马路 , 大行宫 , 新街口 , 元通 , 油坊桥 , 泰冯路 , 南京站 , 鸡鸣寺 , 大行宫 , 南京南站 , 柳州东路 ]
dp_path= [ 仙林中心 , 仙林中心 , 仙林中心 , 仙林中心 , 仙林中心 , 仙林中心 , 金马路 , 金马路 , 金马路 , 大行宫 , 大行宫 , 鸡鸣寺 ]
dp_cost= [ 0.0 , 9.2 , 27.9 , 32.2 , 55.7 , 63.5 , 57.2 , 40.3 , 31.4 , 27.9 , 46.8 , 52.0 ]
path= [ 仙林中心 , 学则路 , 仙鹤门 , 金马路 ,苏宁总部·徐庄 , 聚宝山 , 王家湾 , 蒋王庙 , 岗子村 , 九华山 , 鸡鸣寺 , 新庄 , 南京站 , 小市 , 五塘广场 , 上元门 , 柳州东路 ]
-------------end---------------
这个路段高德地图、百度地图给出的是2->3的换乘路径,实测2->3->4更好(10分钟左右的省时间)。测试的地图软件只有腾讯地图给出了2->4->3的换乘路线,用时约为54min,与我们的52min高度符合。因此我信任了所谓的腾讯地图,把其他两家删掉了!删掉了!!
好了,以上就是这一小节讲解的简单GUI图形化界面制作。感谢友友们一键三连哦!希望这三节的讲解能够帮助到你!