【Java实现】南京地铁导航系统的简单实现(三)—— 图形化界面的设计

         关于如何存储信息、 最短路径算法的实现已经在之前章节说过,这里就不重复了,简单回顾一下实现内容。麻烦有需求的看官Tp到前一节

【Java实现】南京地铁导航系统的简单实现(一)—— 存储站点信息_kksp993的博客-CSDN博客

【Java实现】南京地铁导航系统的简单实现(二)—— 最短路径算法的实现_kksp993的博客-CSDN博客


 实现内容

        以南京地铁运营示意图为模板,实现任意两个站点之间最优路径导航的规划与动态展示效果。具体模板图片以及要求如下:

        1. 存储南京地铁线路站点信息。

        2. 给定起点站和终点站,假设相邻站点路径长度相等,求路径最短的地铁乘坐方案;

        3. 给定起点站和终点站,假设相邻站点路径长度相等,求换乘次数最少的地铁乘坐方案,若存在多条换乘次数相同的乘坐方案,则给出换乘次数最少且路径长度最短的乘坐方案。

        4. 在实际应用中,相邻站点的距离并不相等,假设中转站地铁停留时间为T1,非中转站地铁停留时间为T2,地铁换乘一次的时间消耗为T3(不考虑等待地铁的时间),地铁平均速度为v,相邻站点的路径长度已知,试求:在给定起点站和终点站的情况下,求乘坐时间最短的地铁乘坐方案。

        5. 设计可视化的查询界面,对以上内容进行动态化展示。


(注明:此例使用Swing包写java图形化界面)

窗体JFrame的设计

        很多初学者(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方法就不说了。


MapPanel设计

        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<Integer, Color> colors = new HashMap<>();
    private static boolean isShowing = false;
    private static boolean isViewPort = false;
    private static ArrayList<Station> 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<Station> 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<Station> 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<Station> 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<Station> line, Point lastPoint) {
        drawProcessEdge(g, line, lastPoint, 1);
    }

    /**
     * 绘制一条渐进的线
     * @param g 画笔
     * @param line 绘制数组
     * @param lastPoint 上一路由节点
     * @param process 进度条
     */
    private void drawProcessEdge(Graphics g, ArrayList<Station> 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)一一映射到具体的参考位置上:

                        V_{physic}=V_{logic}\cdot blockwidth+B_p

                        V_{logic}=floor(\frac{V_{physic}-B_p }{blockwidth}+0.5)

                其中V_{logic}=(m,n)表示棋盘上的逻辑点位置,比如迈皋桥是(14,3),对应初始状态下的物理坐标为(280,60)如果B_p(0,0)的话;同样,点击时,只要x,y都在该点左右1/2间距以内都算点到这个格点的,因此四舍五入需要加个0.5。这样可以一一对应了,后面就不需要再为转化而烦恼了。

        (2)对于用户平移类操作可以直接在B_p上修改,绘制时所有点自动向B_p对齐:

        (3)对于用户缩放类操作,需要基于鼠标位置和滚轮进度对B_p和blockwidth进行修正

        当用户以鼠标某一位置缩放地图时,用户实际想要看清楚鼠标所指之处——以鼠标为中心进行缩放。这样会造成基准点的移动,需要以中心点为参考点更改基准点的坐标

        对于鼠标在图像C点,基准点在B_p,由于缩放(以blockwidth自增为例),CB_P

长度增加了原来的1/blockwidth倍(例如:一开始是blockwidth是20,那么当所有间距扩大一格时,CB_P变为原来的1.05倍),但相对方向不变,因此可以以此更新基准点坐标。

        具体而言,新的基准点B_p满足如下公式:

        其中,C(x,y)表示鼠标位置,B_p表示基准点位置,B_P'表示缩放完成后的B_p。(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

 其他的程序设计就是你自己想画什么画什么了/^.^/


ControlPanel设计

        这部分就比较简单了,没什么特别的,完全根据自己的想法一个个放置就好了。

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<Station> 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);
    ArrayList<Station> 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();
}

        这个部分抽出来写,否则很多代码是一样的。首先进行一些简单的提升健壮性的判断。是否正在导航?(这里直接给它摁了,你没结束我也强制结束)是否起点终点都有且不相同?都符合,我们进行导航——调用章节(二)提供的接口(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图形化界面制作。感谢友友们一键三连!希望这三节的讲解能够帮助到你!

  • 7
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值