import dash
from dash import dcc, html
from dash.dependencies import Input, Output, State
import plotly.graph_objects as go
import json
import pandas as pd
import os
import numpy as np
from shapely.geometry import Point, Polygon # 用于点是否在多边形内的判断
# ----------------------------------------------------------------------
# 1. 初始设置和数据结构
# ----------------------------------------------------------------------
# 小区拓扑数据 (四个顶点坐标)
# 注意:坐标按顺序排列,用于绘制平行四边形
cell_coords = {
"cell1": [(0, 200), (115.4701, 200), (57.73503, 100), (-57.73503, 100)],
"cell2": [(-57.73503, 100), (57.73503, 100), (115.4701, 0), (0, 0)],
}
# 定义shapely多边形用于点过滤
CELL1_POLYGON = Polygon(cell_coords["cell1"])
CELL2_POLYGON = Polygon(cell_coords["cell2"])
# JSON 文件路径
JSON_FILE = "user_specific_parameter.json"
# 初始化JSON文件结构(如果不存在或为空)
def initialize_json_file():
if not os.path.exists(JSON_FILE) or os.stat(JSON_FILE).st_size == 0:
initial_data = {"ue_trajectories": {"trajectory_list": []}}
with open(JSON_FILE, 'w') as f:
json.dump(initial_data, f, indent=4)
print(f"Initialized {JSON_FILE}")
# 初始化JSON文件
initialize_json_file()
# ----------------------------------------------------------------------
# 2. 背景点生成函数 (用于捕获点击)
# ----------------------------------------------------------------------
def generate_background_points(cell_polygons, density=5):
"""在指定多边形区域内生成均匀分布的背景点."""
# 确定边界范围
min_x = min(min(c[0] for c in p.exterior.coords) for p in cell_polygons) - 10
max_x = max(max(c[0] for c in p.exterior.coords) for p in cell_polygons) + 10
min_y = min(min(c[1] for c in p.exterior.coords) for p in cell_polygons) - 10
max_y = max(max(c[1] for c in p.exterior.coords) for p in cell_polygons) + 10
# 创建网格点
x_coords = np.linspace(min_x, max_x, int((max_x - min_x) * density))
y_coords = np.linspace(min_y, max_y, int((max_y - min_y) * density))
points_x = []
points_y = []
# 过滤点,只保留在任意小区内的点
for x in x_coords:
for y in y_coords:
p = Point(x, y)
if any(polygon.contains(p) or polygon.boundary.contains(p) for polygon in cell_polygons):
points_x.append(x)
points_y.append(y)
return points_x, points_y
# 生成背景点数据
BG_POINTS_X, BG_POINTS_Y = generate_background_points([CELL1_POLYGON, CELL2_POLYGON], density=2) # 降低密度以提高性能
# ----------------------------------------------------------------------
# 3. 小区拓扑图绘制函数
# ----------------------------------------------------------------------
def create_initial_figure(all_trajectories_data=[]):
"""绘制初始的小区拓扑图(平行四边形)."""
fig = go.Figure()
# --- 绘制小区1 和 小区2 (拓扑) ---
for i, (name, coords) in enumerate(cell_coords.items()):
x, y = zip(*coords)
fig.add_trace(go.Scatter(
x=list(x) + [x[0]],
y=list(y) + [y[0]],
mode='lines',
line=dict(color='green', width=2),
name=f'{name} Topology',
hoverinfo='skip',
showlegend=True
))
# --- 绘制透明背景点 (用于点击捕获) ---
fig.add_trace(go.Scatter(
x=BG_POINTS_X,
y=BG_POINTS_Y,
mode='markers',
marker=dict(
size=5,
color='rgba(0,0,0,0)', # 完全透明
opacity=0,
line=dict(width=0)
),
name='Click Area',
hoverinfo='none',
# crucial: enable selection on this invisible trace
customdata=[[x, y] for x, y in zip(BG_POINTS_X, BG_POINTS_Y)],
unselected=dict(marker={'opacity': 0}), # 未选择时透明
selected=dict(marker={'color': 'rgba(255, 0, 0, 0.5)', 'opacity': 0.5, 'size': 8}), # 选择时半透明红色
showlegend=False
))
# 设置图表布局
fig.update_layout(
title=f'小区拓扑图与UE轨迹绘制工具 - 正在绘制第 {len(all_trajectories_data) + 1} 个UE轨迹',
xaxis_title='X 坐标',
yaxis_title='Y 坐标',
# 移除比例约束,允许图表拉伸
xaxis=dict(range=[-80, 140]),
yaxis=dict(range=[-20, 220]),
# 关键: 设置拖拽模式为 lasso 或 rectangular 选择
dragmode='select',
selectdirection='any',
template='plotly_white',
clickmode='event+select',
# 确保图形占满整个容器
autosize=True,
)
return fig
# ----------------------------------------------------------------------
# 4. Dash 应用和布局
# ----------------------------------------------------------------------
app = dash.Dash(__name__)
app.layout = html.Div([
html.H1("UE 轨迹绘制工具"),
# 图表区域: 设置 style 确保图表高度较大,占满页面
dcc.Graph(
id='topology-graph',
figure=create_initial_figure(),
config={'displayModeBar': True, 'editable': True},
style={'height': '80vh', 'width': '100%'} # 确保图形占满大部分页面高度
),
# 坐标显示区域
html.Div(id='selected-data-output',
style={'margin-top': '10px', 'font-size': '16px', 'font-weight': 'bold'}),
# UE 轨迹操作按钮
html.Div([
html.Button('保存当前UE轨迹并开始下一个', id='save-clear-button', n_clicks=0,
style={'margin-right': '10px', 'padding': '10px', 'background-color': '#4CAF50', 'color': 'white',
'border': 'none', 'border-radius': '5px'}),
html.Button('导出所有轨迹到JSON文件', id='export-json-button', n_clicks=0,
style={'padding': '10px', 'background-color': '#008CBA', 'color': 'white', 'border': 'none',
'border-radius': '5px'}),
], style={'margin-top': '20px'}),
# 隐藏的存储组件
dcc.Store(id='current-ue-store', data=[]),
dcc.Store(id='all-trajectories-store', data=json.load(open(JSON_FILE))["ue_trajectories"]["trajectory_list"]),
dcc.Store(id='last-point-coords', data={'x': None, 'y': None})
])
# ----------------------------------------------------------------------
# 5. Dash 回调函数
# ----------------------------------------------------------------------
# 回调1: 处理鼠标选择事件,更新当前UE轨迹
@app.callback(
[Output('current-ue-store', 'data', allow_duplicate=True),
Output('last-point-coords', 'data', allow_duplicate=True)],
[Input('topology-graph', 'selectedData')],
[State('current-ue-store', 'data')],
prevent_initial_call=True
)
def handle_graph_select(selectedData, current_ue_data):
"""选择背景点时,记录新的轨迹点."""
if selectedData is None or 'points' not in selectedData or not selectedData['points']:
# 没有有效的选择数据
return dash.no_update, dash.no_update
# 获取最后一个被选择的点
# 注意: Plotly selection 可以选择多个点。我们只取最后一个点作为轨迹点。
last_point = selectedData['points'][-1]
# 确认选择的是我们用于捕获的 'Click Area' 轨迹
if last_point.get('curveNumber') != 2: # 假设 'Click Area' 是第三个轨迹 (索引2)
return dash.no_update, dash.no_update
x = last_point['x']
y = last_point['y']
z = 1.5 # 固定z坐标
# 构造新的轨迹点: [x, y, z]
new_point = [round(x, 4), round(y, 4), z]
# 检查是否重复添加了同一个点 (防止多次点击同一区域导致的重复)
if current_ue_data and new_point == current_ue_data[-1]:
return dash.no_update, dash.no_update
# 更新当前轨迹列表
updated_trajectory = current_ue_data + [new_point]
# 返回更新后的数据和最新的点击坐标
return updated_trajectory, {'x': round(x, 4), 'y': round(y, 4)}
# 回调2: 绘制当前UE轨迹和更新坐标显示
@app.callback(
[Output('topology-graph', 'figure', allow_duplicate=True),
Output('selected-data-output', 'children')],
[Input('current-ue-store', 'data'),
Input('last-point-coords', 'data')],
[State('all-trajectories-store', 'data')],
prevent_initial_call=True
)
def update_graph_and_display(current_ue_data, last_point_coords, all_trajectories_data):
"""根据当前轨迹数据,在图上绘制轨迹点和连线,并更新坐标显示."""
# 重新创建初始的小区图
fig = create_initial_figure(all_trajectories_data)
# 如果当前有轨迹点,则进行绘制
if current_ue_data:
# 提取 x 和 y 坐标
df = pd.DataFrame(current_ue_data, columns=['x', 'y', 'z'])
# 绘制连线和点
fig.add_trace(go.Scatter(
x=df['x'],
y=df['y'],
mode='lines+markers',
marker=dict(size=8, color='red', symbol='circle'), # 使用圆点更清晰
line=dict(color='red', width=2),
name='UE Trajectory',
hoverinfo='text',
text=[f'({x}, {y})' for x, y in zip(df['x'], df['y'])],
showlegend=True
))
# 强制更新标题 (在 create_initial_figure 中已更新)
# 更新坐标显示
if last_point_coords['x'] is not None:
display_text = f"上一次轨迹点坐标: X={last_point_coords['x']}, Y={last_point_coords['y']}, Z=1.5"
else:
display_text = "请使用鼠标拖拽/点击图表上的区域来绘制UE轨迹..."
return fig, display_text
# 回调3: 保存当前UE轨迹并开始下一个
@app.callback(
[Output('current-ue-store', 'data', allow_duplicate=True),
Output('all-trajectories-store', 'data', allow_duplicate=True)],
[Input('save-clear-button', 'n_clicks')],
[State('current-ue-store', 'data'),
State('all-trajectories-store', 'data')],
prevent_initial_call=True
)
def save_and_clear_trajectory(n_clicks, current_ue_data, all_trajectories_data):
"""点击按钮时,保存当前UE轨迹,并清空当前轨迹列表."""
changed_id = [p['prop_id'] for p in dash.callback_context.triggered][0]
if 'save-clear-button' in changed_id and n_clicks > 0:
if not current_ue_data:
print("当前UE轨迹为空,未保存任何数据。")
return [], all_trajectories_data
# 1. 保存当前轨迹
updated_all_trajectories = all_trajectories_data + [current_ue_data]
print(f"UE轨迹已保存。当前总计 {len(updated_all_trajectories)} 条轨迹。")
# 2. 清空当前轨迹数据,准备绘制下一个
new_current_ue_data = []
# 返回更新后的数据
return new_current_ue_data, updated_all_trajectories
return dash.no_update, dash.no_update
# 回调4: 导出所有轨迹到JSON文件
# ... (与前一版本保持一致,功能无变化)
@app.callback(
Output('export-json-button', 'children'),
[Input('export-json-button', 'n_clicks')],
[State('all-trajectories-store', 'data')],
prevent_initial_call=True
)
def export_to_json(n_clicks, all_trajectories_data):
"""将所有已保存的轨迹数据导出到JSON文件."""
changed_id = [p['prop_id'] for p in dash.callback_context.triggered][0]
if 'export-json-button' in changed_id and n_clicks > 0:
try:
with open(JSON_FILE, 'r') as f:
data = json.load(f)
data["ue_trajectories"]["trajectory_list"] = all_trajectories_data
with open(JSON_FILE, 'w') as f:
json.dump(data, f, indent=4)
num_trajectories = len(all_trajectories_data)
return f"成功导出 ({num_trajectories} 条)!"
except Exception as e:
return "导出失败!"
return '导出所有轨迹到JSON文件'
if __name__ == '__main__':
# 运行应用
print(f"请安装 shapely (pip install shapely) 并打开浏览器访问 http://127.0.0.1:8050/")
app.run(debug=True)
将上面代码中所有中文部分均改为英文,同时添加一个功能,运行前先检查是否存在user_specific_parameter.json文件,若不存在进行创建,user_specific_parameter.json文件格式如下
{
"ue_trajectories": {
"trajectory_interval_second": 1,
"interpolation_interval_second": 0.01,
"trajectory_list": []
最新发布