图算法雷达扫描可视化实现
一、Edge类(边模型)
用于存储图中"顶点-目标顶点"的关联关系,包含目标顶点名称和两点间距离
java
import java.util.*;
/**
* 边模型类:存储图中边的目标顶点名称与两点间距离
* 无向图中,一条边会对应两个顶点的双向关联(如A到B的边,同时存在于A的边列表和B的边列表)
*/
class Edge {
// 目标顶点的名称(如"X""A""B")
String name;
// 当前顶点到目标顶点的距离(整数型,简化距离计算结果)
int dis;
/**
* 构造方法:初始化边的目标顶点和距离
* @param name 目标顶点名称
* @param dis 两点间距离
*/
public Edge(String name, int dis) {
this.dis = dis; // 赋值距离
this.name = name; // 赋值目标顶点名称
}
}
关键注意点:
- Edge类是图的"边"数据载体,仅存储单方向的关联关系(如从A指向B),无向图的双向性需在Graph类中通过逻辑实现;
- 距离用int类型而非double,是为了简化后续排序和展示,避免小数带来的冗余计算。
二、MPoint类(顶点坐标模型)
记录图中顶点的名称与屏幕坐标,为距离计算(勾股定理)提供基础坐标数据
java
/**
* 顶点坐标模型类:关联顶点名称与屏幕像素坐标
* 用于存储所有顶点的位置信息,支持鼠标交互时计算新顶点与历史顶点的距离
*/
class MPoint {
// 顶点的唯一标识名称(如初始顶点"X",后续添加的"A""B"等)
String name;
// 顶点在Swing窗口中的X轴像素坐标
int x;
// 顶点在Swing窗口中的Y轴像素坐标
int y;
/**
* 构造方法:初始化顶点名称与坐标
* @param name 顶点名称
* @param x X轴坐标
* @param y Y轴坐标
*/
public MPoint(String name, int x, int y) {
this.name = name; // 赋值顶点名称
this.x = x; // 赋值X轴坐标
this.y = y; // 赋值Y轴坐标
}
}
关键注意点:
- MPoint的核心作用是"坐标存档",每次鼠标点击添加新目标时,会生成一个MPoint对象存入列表,后续计算距离时直接从列表中获取历史顶点坐标;
- 坐标与Swing窗口的像素对应(如(200,700)是初始顶点"X"的固定位置),确保距离计算结果与可视化位置一致。
三、Graph类(图核心逻辑类)
实现无向图的构建、边添加、距离排序与信息展示,是整个图算法的核心
java
/**
* 图核心逻辑类:管理顶点与边的关系,实现无向图的构建、排序与信息展示
* 核心数据结构为HashMap,键是顶点名称,值是该顶点关联的所有边列表
*/
public class Graph {
// 无向图的存储结构:key=顶点名称,value=该顶点的所有边(Edge列表)
Map<String, List<Edge>> graph = new HashMap<>();
/**
* 向图中添加顶点(若顶点已存在则不重复添加)
* @param name 要添加的顶点名称
*/
public void add(String name) {
// 1. 先判断顶点是否已存在(避免重复创建空边列表)
if (!graph.containsKey(name)) {
// 2. 为新顶点初始化空的边列表(后续通过scan方法添加边)
List<Edge> edgeList = new ArrayList<>();
// 3. 将顶点与边列表存入HashMap
graph.put(name, edgeList);
}
}
/**
* 构建无向边:为两个顶点添加双向关联的边(体现无向图特性)
* @param name 起点顶点名称
* @param to 终点顶点名称
* @param dis 两点间距离
*/
public void scan(String name, String to, int dis) {
// 1. 确保起点和终点都已存在于图中(若不存在则调用add方法创建)
if (!graph.containsKey(name)) {
add(name);
}
if (!graph.containsKey(to)) {
add(to);
}
// 2. 为起点添加"指向终点"的边
// 2.1 获取起点的边列表
List<Edge> edgeList = graph.get(name);
// 2.2 创建起点到终点的边对象
Edge edge = new Edge(to, dis);
// 2.3 将边添加到起点的边列表
edgeList.add(edge);
// 3. 为终点添加"指向起点"的边(无向图的双向性关键步骤)
// 3.1 获取终点的边列表
List<Edge> edgeList1 = graph.get(to);
// 3.2 创建终点到起点的边对象(距离与起点到终点一致)
Edge edge1 = new Edge(name, dis);
// 3.3 将边添加到终点的边列表
edgeList1.add(edge1);
}
/**
* 展示图中所有顶点的关联关系:按距离升序排序后打印
* 便于直观查看每个顶点到其他目标的距离,支持"优先攻击最近目标"的业务需求
*/
public void showInfo() {
// 1. 获取所有顶点的名称集合(通过HashMap的keySet方法)
Set<String> keySet = graph.keySet();
// 2. 打印标题,区分每次扫描结果
System.out.println("=== 当前雷达扫描图 ===");
// 3. 遍历每个顶点,处理并展示其边列表
for (String name : keySet) {
// 3.1 获取当前顶点的边列表
List<Edge> edgeList = graph.get(name);
// 3.2 打印当前顶点名称
System.out.println("顶点:" + name);
// 3.3 冒泡排序:将边列表按距离升序排序(最近目标在前)
// 外层循环:控制排序轮次(每轮确定一个最大距离的边到末尾)
for (int i = 0; i < edgeList.size(); i++) {
// 内层循环:比较相邻边的距离,交换顺序(大距离后移)
for (int j = 0; j < edgeList.size() - 1; j++) {
Edge e1 = edgeList.get(j); // 当前边
Edge e2 = edgeList.get(j + 1); // 下一条边
// 若当前边距离 > 下一条边距离,交换两者位置
if (e1.dis > e2.dis) {
edgeList.set(j, e2);
edgeList.set(j + 1, e1);
}
}
}
// 3.4 打印排序后的目标与距离
for (int i = 0; i < edgeList.size(); i++) {
Edge edge = edgeList.get(i);
System.out.println("目标:" + edge.name + "- 距离:" + edge.dis);
}
// 3.5 打印分隔线,区分不同顶点的信息
System.out.println("-----");
}
System.out.println("------");
}
}
关键注意点:
add方法的防御性判断:避免重复添加同一顶点导致的边列表覆盖,确保每个顶点只对应一个边列表;scan方法的双向性:每次调用会同时为两个顶点添加边,是无向图"双向可达"的核心逻辑,若遗漏则会变成有向图;- 冒泡排序的作用:将每个顶点的边列表按距离升序排列,直接为"优先攻击最近Alien"提供数据支持,无需额外处理排序逻辑;
- 信息展示格式:通过"顶点→目标-距离"的结构打印,清晰区分每个顶点的关联关系,便于调试和业务使用。
四、RobotListener类(鼠标交互与绘制逻辑)
处理鼠标点击事件,实现新目标的绘制、距离计算与图数据更新,是"可视化"与"图算法"的桥梁
java
import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.ArrayList;
/**
* 鼠标监听器类:处理鼠标交互(点击添加目标)、图形绘制与图数据同步
* 关联Graph类和MPoint类,实现"点击→绘制→计算距离→更新图"的完整流程
*/
public class RobotListener implements MouseListener {
// 画笔对象:用于在Swing窗口中绘制目标点(圆形)和初始顶点(矩形)
Graphics g;
// 初始顶点"X"的固定坐标(Swing窗口中的位置)
final int sx = 200, sy = 700;
// 新添加目标的名称标识(从"A"开始自动递增,如"A""B""C")
char name = 'A';
// 关联的图实例:用于存储新目标与历史顶点的距离关系
Graph scanGraph = new Graph();
// 顶点坐标列表:存储所有顶点的MPoint对象,用于批量计算新目标与历史顶点的距离
ArrayList<MPoint> points = new ArrayList<>();
/**
* 构造方法:初始化初始顶点"X"
* 程序启动时自动创建"X"顶点,作为雷达扫描的起点
*/
public RobotListener() {
scanGraph.add("X"); // 向图中添加初始顶点"X"
// 向顶点列表添加"X"的坐标(与sx、sy一致)
points.add(new MPoint("X", 200, 700));
}
// 鼠标点击事件(本项目未使用,留空)
@Override
public void mouseClicked(MouseEvent e) {}
/**
* 鼠标按下事件:核心交互逻辑
* 鼠标按下时触发:绘制新目标、计算距离、更新图数据、展示扫描结果
*/
@Override
public void mousePressed(MouseEvent e) {
System.out.println("按下"); // 调试用:打印鼠标按下状态
// 1. 获取鼠标按下的坐标(即新目标的坐标)
int x = e.getX(); // 鼠标按下位置的X轴坐标
int y = e.getY(); // 鼠标按下位置的Y轴坐标
// 2. 绘制新目标:红色圆形(直径50像素,视觉上区分初始顶点)
g.setColor(Color.RED); // 设置画笔颜色为红色
// 绘制圆形:参数依次为(X轴坐标、Y轴坐标、宽度、高度),宽高一致即为圆形
g.fillOval(x, y, 50, 50);
// 3. 计算新目标与所有历史顶点的距离,并更新图数据
// 遍历顶点列表(包含"X"及已添加的目标)
for (int i = 0; i < points.size(); i++) {
MPoint p = points.get(i); // 获取历史顶点的MPoint对象
// 3.1 勾股定理计算距离:sqrt((新X-历史X)² + (新Y-历史Y)²),转为int简化
int dis = (int) Math.sqrt((x - p.x) * (x - p.x) + (y - p.y) * (y - p.y));
// 3.2 调用scan方法:为新目标与历史顶点添加双向边
scanGraph.scan(name + "", p.name, dis);
}
// 4. 将新目标添加到顶点列表(供后续新目标计算距离使用)
MPoint mPoint = new MPoint(name + "", x, y); // 创建新目标的MPoint对象
points.add(mPoint); // 加入列表
// 5. 更新目标名称标识(下一次点击将使用下一个字母,如"A"→"B")
name++;
// 6. 展示当前图的所有扫描信息(按距离排序后的结果)
scanGraph.showInfo();
}
// 鼠标释放事件(本项目未使用,留空)
@Override
public void mouseReleased(MouseEvent e) {}
// 鼠标进入窗口事件(本项目未使用,留空)
@Override
public void mouseEntered(MouseEvent e) {}
// 鼠标离开窗口事件(本项目未使用,留空)
@Override
public void mouseExited(MouseEvent e) {}
}
关键注意点:
- 初始顶点"X"的初始化:在构造方法中完成,确保程序启动时就有扫描起点,避免后续计算空指针;
- 鼠标坐标与绘制的关系:
e.getX()和e.getY()获取的是鼠标在Swing窗口中的相对坐标,直接用于绘制圆形,确保视觉位置与坐标计算一致; - 批量距离计算:通过遍历
points列表,一次性计算新目标与所有历史顶点的距离,避免遗漏关联关系; - 目标名称自动递增:
char name='A'通过name++实现自动命名,确保每个新目标的名称唯一,避免顶点名称冲突。
五、GraphRobot类(窗口容器类)
创建Swing窗口,初始化界面布局与画笔,绑定鼠标监听器,是可视化的载体
java
import javax.swing.*;
import java.awt.*;
/**
* 窗口容器类:创建Swing窗口,初始化界面配置,关联鼠标监听器
* 是整个程序的入口,负责将图算法与可视化界面结合
*/
public class GraphRobot {
/**
* 初始化并展示Swing窗口
* 配置窗口标题、大小、关闭策略、布局,绑定监听器并绘制初始顶点
*/
public void showUI() {
// 1. 创建Swing窗口对象(JFrame)
JFrame jf = new JFrame();
// 2. 设置窗口标题(显示在窗口顶部)
jf.setTitle("图算法应用");
// 3. 设置窗口大小(宽度800像素,高度1000像素)
jf.setSize(800, 1000);
// 4. 设置窗口关闭策略:关闭窗口时退出整个程序
jf.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
// 5. 设置窗口居中显示(打开时位于屏幕正中央)
jf.setLocationRelativeTo(null);
// 6. 设置窗口布局:流式布局(组件按添加顺序排列)
FlowLayout flow = new FlowLayout();
jf.setLayout(flow);
// 7. 显示窗口(必须在配置完成后调用,否则界面不显示)
jf.setVisible(true);
// 8. 获取窗口的画笔对象(用于后续绘制图形)
Graphics g = jf.getGraphics();
// 9. 创建鼠标监听器对象,并绑定画笔
RobotListener rl = new RobotListener();
rl.g = g; // 将窗口画笔传递给监听器,确保监听器能绘制图形
// 10. 为窗口绑定鼠标监听器(监听鼠标按下等事件)
jf.addMouseListener(rl);
// 11. 延迟500毫秒后绘制初始顶点"X"(矩形)
try {
Thread.sleep(500); // 延迟500ms:确保窗口完全加载后再绘制,避免图形不显示
} catch (InterruptedException e) {
// 捕获中断异常,转为运行时异常抛出(简化异常处理)
throw new RuntimeException(e);
}
// 绘制初始顶点"X":黑色矩形(坐标200,700,宽高100像素)
g.setColor(Color.BLACK); // 设置画笔颜色为黑色
g.fillRect(200, 700, 100, 100); // 绘制矩形:(X,Y,宽,高)
}
/**
* 程序入口(main方法)
* 启动程序,调用showUI方法展示窗口
*/
public static void main(String[] args) {
// 创建GraphRobot对象
GraphRobot ger = new GraphRobot();
// 调用showUI方法初始化并显示窗口
ger.showUI();
}
}
关键注意点:
- 窗口显示顺序:
jf.setVisible(true)必须在窗口配置(大小、标题等)之后调用,否则配置不生效; - 画笔获取时机:
jf.getGraphics()必须在jf.setVisible(true)之后调用,否则无法获取有效画笔,导致绘制失败; - 延迟绘制初始顶点:
Thread.sleep(500)是为了等待窗口完全加载,避免因窗口未初始化完成导致初始矩形不显示; - 关闭策略设置:
WindowConstants.EXIT_ON_CLOSE确保关闭窗口时程序完全退出,避免后台进程残留; - 布局选择:流式布局(FlowLayout)简化了界面管理,适合本项目的简单可视化需求,若需复杂布局可替换为其他布局(如BorderLayout)。