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
import math
from shapely.geometry import Point, Polygon
from shapely.ops import unary_union
# ----------------------------------------------------------------------
# 1. 配置参数读取
# ----------------------------------------------------------------------
CONFIG_FILE = "simulation_parameter_case.json"
try:
with open(CONFIG_FILE, 'r') as f:
config = json.load(f)
isd = config["channel_parameters"]["isd"]
display_cell_ids = [bs["cell_id"] for bs in config["bs_parameters"]]
except Exception as e:
print(f"Error: {e} - Using default values")
isd = 200
display_cell_ids = []
# ----------------------------------------------------------------------
# 2. 7c3s小区拓扑类定义(添加精确四边形扇区)
# ----------------------------------------------------------------------
class CellTopology:
def __init__(self, isd=200):
self.isd = isd
self.radius = (self.isd / 2.0) / (np.cos(np.deg2rad(30)))
# 基站位置
self.bs_loc_set = np.array([
[0.0, 0.0], # 中心基站
[-math.sqrt(3) * self.isd / 2.0, self.isd / 2.0], # 左上
[0.0, self.isd], # 上
[math.sqrt(3) * self.isd / 2.0, self.isd / 2.0], # 右上
[math.sqrt(3) * self.isd / 2.0, -self.isd / 2.0], # 右下
[0.0, -self.isd], # 下
[-math.sqrt(3) * self.isd / 2.0, -self.isd / 2.0] # 左下
])
# 六边形顶点
self.center_bs_hexgon_vertex = np.array([
[self.radius, 0.0],
[self.radius / 2.0, self.isd / 2.0],
[-self.radius / 2.0, self.isd / 2.0],
[-self.radius, 0.0],
[-self.radius / 2.0, -self.isd / 2.0],
[self.radius / 2.0, -self.isd / 2.0],
[self.radius, 0.0]
])
# 扇区分割线
self.center_sector_split_line = np.array([
[0.0, 0.0], [self.radius, 0.0], # 0°
[0.0, 0.0], [-self.radius / 2.0, self.isd / 2.0], # 120°
[0.0, 0.0], [-self.radius / 2.0, -self.isd / 2.0] # 240°
])
# 存储每个扇区的四边形多边形
self.sector_polygons = {}
current_cell_id = 0
# 扇区角度定义
sector_angles = [0, 120, 240]
for bs_id in range(7):
bs_loc = self.bs_loc_set[bs_id]
hex_vertices = self.center_bs_hexgon_vertex + bs_loc
# 为每个扇区创建四边形多边形
for i, angle in enumerate(sector_angles):
# 扇区起始点和结束点
start_idx = i
end_idx = (i + 1) % 3
# 扇区四边形顶点:基站中心 + 三个六边形顶点
quad_points = np.array([
bs_loc, # 基站中心
hex_vertices[start_idx * 2],
hex_vertices[start_idx * 2 + 1],
hex_vertices[end_idx * 2]
])
self.sector_polygons[current_cell_id] = quad_points
current_cell_id += 1
# 使用配置文件中的ISD值初始化拓扑
topology = CellTopology(isd=isd)
# ----------------------------------------------------------------------
# 3. UE轨迹工具初始设置
# ----------------------------------------------------------------------
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:
default_data = {
"ue_trajectories": {
"trajectory_interval_second": 1,
"interpolation_interval_second": 0.01,
"trajectory_list": [],
"isd": isd, # 添加ISD信息
"display_cell_ids": display_cell_ids # 添加显示的cell_id信息
}
}
with open(JSON_FILE, 'w') as f:
json.dump(default_data, f, indent=4)
else:
try:
with open(JSON_FILE, 'r') as f:
data = json.load(f)
if "ue_trajectories" in data:
# 更新当前ISD和显示的cell_id
data["ue_trajectories"]["isd"] = isd
data["ue_trajectories"]["display_cell_ids"] = display_cell_ids
data["ue_trajectories"]["trajectory_list"] = []
else:
data["ue_trajectories"] = {
"trajectory_interval_second": 1,
"interpolation_interval_second": 0.01,
"trajectory_list": [],
"isd": isd,
"display_cell_ids": display_cell_ids
}
with open(JSON_FILE, 'w') as f:
json.dump(data, f, indent=4)
except Exception as e:
print(f"初始化JSON文件错误: {e}")
default_data = {
"ue_trajectories": {
"trajectory_interval_second": 1,
"interpolation_interval_second": 0.01,
"trajectory_list": [],
"isd": isd,
"display_cell_ids": display_cell_ids
}
}
with open(JSON_FILE, 'w') as f:
json.dump(default_data, f, indent=4)
# 初始化JSON文件
initialize_json_file()
# ----------------------------------------------------------------------
# 4. 背景点生成函数(只生成指定扇区内的点)
# ----------------------------------------------------------------------
def generate_background_points(display_cell_ids, density=5):
"""只为显示的扇区生成背景点"""
display_polygons = []
# 获取所有要显示扇区的多边形
for cell_id in display_cell_ids:
if cell_id in topology.sector_polygons:
quad_points = topology.sector_polygons[cell_id]
display_polygons.append(Polygon(quad_points))
if not display_polygons:
return [], []
# 合并所有多边形
combined_poly = unary_union(display_polygons)
# 计算边界
min_x, min_y, max_x, max_y = combined_poly.bounds
min_x -= 10
max_x += 10
min_y -= 10
max_y += 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 combined_poly.contains(p):
points_x.append(x)
points_y.append(y)
return points_x, points_y
# 生成指定扇区的背景点
BG_POINTS_X, BG_POINTS_Y = generate_background_points(display_cell_ids, density=2)
# ----------------------------------------------------------------------
# 5. 小区拓扑绘图函数(只绘制指定四边形扇区)
# ----------------------------------------------------------------------
def create_initial_figure(all_trajectories_data=[]):
fig = go.Figure()
# 收集所有要显示的扇区顶点坐标
all_x = []
all_y = []
# 1. 只绘制指定的四边形扇区
for cell_id in display_cell_ids:
if cell_id in topology.sector_polygons:
quad_points = topology.sector_polygons[cell_id]
all_x.extend(quad_points[:, 0])
all_y.extend(quad_points[:, 1])
# 闭合多边形(第一个点添加到末尾)
x_quad = list(quad_points[:, 0]) + [quad_points[0, 0]]
y_quad = list(quad_points[:, 1]) + [quad_points[0, 1]]
# 添加四边形扇区边界
fig.add_trace(go.Scatter(
x=x_quad, y=y_quad,
mode='lines',
line=dict(color='green', width=2),
name=f'Cell {cell_id}',
hoverinfo='skip',
showlegend=False
))
# 2. 添加扇区ID标签
labels_x = []
labels_y = []
labels_text = []
for cell_id in display_cell_ids:
if cell_id in topology.sector_polygons:
quad_points = topology.sector_polygons[cell_id]
# 标签位置:四边形中心点
center_x = np.mean(quad_points[:, 0])
center_y = np.mean(quad_points[:, 1])
labels_x.append(center_x)
labels_y.append(center_y)
labels_text.append(f"Cell {cell_id}")
# 添加中心点到坐标集合
all_x.append(center_x)
all_y.append(center_y)
fig.add_trace(go.Scatter(
x=labels_x,
y=labels_y,
mode='text',
text=labels_text,
textfont=dict(size=12, color='black'),
textposition='middle center',
hoverinfo='skip',
showlegend=False
))
# 3. 添加透明背景点
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),
name='Click Area',
hoverinfo='none',
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
))
# 添加背景点到坐标集合
all_x.extend(BG_POINTS_X)
all_y.extend(BG_POINTS_Y)
# 4. 改进的自适应范围计算(解决左右太窄问题)
if all_x and all_y: # 确保有数据点
min_x = min(all_x)
max_x = max(all_x)
min_y = min(all_y)
max_y = max(all_y)
# 计算实际范围
x_range = max_x - min_x
y_range = max_y - min_y
# 计算屏幕宽高比(假设16:9)
screen_ratio = 16 / 9
# 计算数据宽高比
data_ratio = x_range / y_range if y_range != 0 else 1
# 调整范围以适应屏幕比例
if data_ratio > screen_ratio:
# 数据比屏幕宽,需要增加高度
required_height = x_range / screen_ratio
height_difference = max(0, (required_height - y_range) / 2)
min_y -= height_difference
max_y += height_difference
else:
# 数据比屏幕窄,需要增加宽度
required_width = y_range * screen_ratio
width_difference = max(0, (required_width - x_range) / 2)
min_x -= width_difference
max_x += width_difference
# 添加最小边距
margin = min(x_range, y_range) * 0.05
x_range = [min_x - margin, max_x + margin]
y_range = [min_y - margin, max_y + margin]
else:
# 默认范围(宽屏比例)
x_range = [-200, 200]
y_range = [-112.5, 112.5]
# 5. 最大化显示布局设置
fig.update_layout(
title_text=f'Selected Cells: {display_cell_ids} - UE #{len(all_trajectories_data) + 1}',
xaxis_title='X Coordinate (m)',
yaxis_title='Y Coordinate (m)',
xaxis=dict(
range=x_range,
scaleanchor='y',
scaleratio=1,
constrain='domain',
automargin=True # 自动调整边距
),
yaxis=dict(
range=y_range,
constrain='domain',
automargin=True # 自动调整边距
),
dragmode='select',
template='plotly_white',
clickmode='event+select',
autosize=True,
height=None,
# 最小化边距,特别是左右边距
margin=dict(l=0, r=0, t=40, b=0),
# 布局优化参数
paper_bgcolor='rgba(0,0,0,0)',
plot_bgcolor='rgba(0,0,0,0)',
separators='.,'
)
return fig
# ----------------------------------------------------------------------
# 6. Dash应用布局
# ----------------------------------------------------------------------
app = dash.Dash(__name__)
app.layout = html.Div([
html.Div([
html.H1(f"Cell Visualization Tool (ISD={isd}m)",
style={'margin': '5px 0', 'padding': '0'}),
html.P(f"Displayed Cells: {display_cell_ids}",
style={'font-weight': 'bold', 'margin': '0', 'padding': '0 0 5px 0'})
], style={'flex-shrink': 0}), # 防止标题区域压缩
# 图形容器 - 占据所有剩余空间
html.Div([
dcc.Graph(
id='topology-graph',
figure=create_initial_figure(),
config={'displayModeBar': True, 'displaylogo': False},
style={
'height': '100%',
'width': '100%',
'min-height': '400px' # 确保最小高度
}
)
], style={
'flex-grow': 1, # 关键:占据所有可用空间
'position': 'relative',
'overflow': 'hidden'
}),
html.Div(id='selected-data-output',
style={'margin': '5px 0', 'flex-shrink': 0}),
html.Div([
html.Button('Save Current UE Trajectory', id='save-clear-button', n_clicks=0,
style={'margin-right': '10px', 'padding': '8px', 'background-color': '#4CAF50', 'color': 'white'}),
html.Button('Export to JSON', id='export-json-button', n_clicks=0,
style={'padding': '8px', 'background-color': '#008CBA', 'color': 'white'}),
], style={'margin': '10px 0', 'flex-shrink': 0}),
dcc.Store(id='current-ue-store', data=[]),
dcc.Store(id='all-trajectories-store', data=[]),
dcc.Store(id='last-point-coords', data={'x': None, 'y': None})
], style={
'height': '100vh',
'display': 'flex',
'flex-direction': 'column',
'padding': '10px',
'box-sizing': 'border-box', # 确保内边距不影响整体尺寸
'overflow': 'hidden'
})
# ----------------------------------------------------------------------
# 7. Dash回调函数
# ----------------------------------------------------------------------
@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'),
State('topology-graph', 'figure')], # 添加对图形状态的依赖
prevent_initial_call=True
)
def handle_graph_select(selectedData, current_ue_data, figure):
if selectedData is None or not selectedData['points']:
return dash.no_update, dash.no_update
last_point = selectedData['points'][-1]
# 动态计算背景点轨迹的曲线编号
background_curve_number = len(display_cell_ids) + 1 # 四边形扇区 + 标签轨迹
# 确认选择来自"点击区域"轨迹
if last_point.get('curveNumber') != background_curve_number:
return dash.no_update, dash.no_update
x = last_point['x']
y = last_point['y']
z = 1.5
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)}
@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:
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
))
if last_point_coords['x'] is not None:
display_text = f"Last Point: X={last_point_coords['x']}, Y={last_point_coords['y']}, Height=1.5m"
else:
display_text = "Drag/click on the chart area to draw UE trajectory..."
return fig, display_text
@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):
if n_clicks > 0:
if not current_ue_data:
return [], all_trajectories_data
updated_all_trajectories = all_trajectories_data + [current_ue_data]
new_current_ue_data = []
return new_current_ue_data, updated_all_trajectories
return dash.no_update, dash.no_update
@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):
if 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"Exported {num_trajectories} trajectories to {JSON_FILE}"
except Exception:
return "Export failed!"
return 'Export All Trajectories to JSON File'
# ----------------------------------------------------------------------
# 7. 运行应用
# ----------------------------------------------------------------------
if __name__ == '__main__':
print(f"Please visit http://127.0.0.1:8050/")
app.run(debug=True)
在上面代码中,在生成的页面里最大化绘制的小区拓扑图,最好让它占满整个html页面
最新发布