Моделирование полёта дронов
作者
In [ ]:
using LinearAlgebra
using Plots
plotlyjs() # Используем plotlyjs для статических графиков
gr() # Используем gr для анимации
# --- 1. Генерация ландшафта с ориентирами ---
"""
find_landmarks(x, y, z, n_min::Int=3, n_max::Int=3)
Находит `n_min` локальных минимумов и `n_max` локальных максимумов высоты на ландшафте.
"""
function find_landmarks(x, y, z, n_min::Int=3, n_max::Int=3)
landmarks = Vector{Vector{Float64}}()
nx, ny = size(z)
# Проверим все внутренние точки ландшафта на локальные минимумы/максимумы
for i in 2:nx-1
for j in 2:ny-1
is_local_min = true
is_local_max = true
current_z = z[i, j]
# Проверяем 8 соседей
for di in -1:1
for dj in -1:1
if di == 0 && dj == 0
continue
end
neighbor_z = z[i + di, j + dj]
if neighbor_z <= current_z
is_local_min = false
end
if neighbor_z >= current_z
is_local_max = false
end
if !is_local_min && !is_local_max
break
end
end
if !is_local_min && !is_local_max
break
end
end
if is_local_min || is_local_max
push!(landmarks, [x[i], y[j], z[i, j]])
end
end
end
if length(landmarks) < (n_min + n_max)
# Если ориентиров мало, добавим старт и финиш
# Предполагаем, что start и finish определены в области видимости или переданы
# Для простоты здесь просто возвращаем найденные
return landmarks
end
sort!(landmarks, by = p -> p[3])
min_landmarks = landmarks[1:min(n_min, length(landmarks))]
max_landmarks = landmarks[max(1, end - n_max + 1):end]
all_landmarks = unique(vcat(min_landmarks, max_landmarks))
return all_landmarks
end
"""
generate_landscape(width_m, length_m)
Генерирует 3D-ландшафт и находит на нем визуальные ориентиры.
"""
function generate_landscape(width_m, length_m)
x = 0:1:width_m
y = 0:1:length_m
z = [10(sin(xi / 15) + cos(yi / 15)) + 5rand() + 10 for xi in x, yi in y]
z = reverse(z, dims=(1, 2))
random_yi_start = rand(1:length(y))
random_yi_finish = rand(1:length(y))
xi_start = 10
xi_finish = length(x)-10
start = [x[xi_start], y[random_yi_start], z[xi_start, random_yi_start]]
finish = [x[xi_finish], y[random_yi_finish], z[xi_finish, random_yi_finish]]
landmarks = find_landmarks(x, y, z, 4, 4)
plt = surface(x, y, z', c=:terrain, xlabel="X (м)", ylabel="Y (м)", zlabel="Z (м)", title="3D ландшафт", legend=false)
scatter!(plt, [start[1]], [start[2]], [start[3]+3], marker=(:circle), color=:green, label="Старт")
scatter!(plt, [finish[1]], [finish[2]], [finish[3]+3], marker=(:circle), color=:red, label="Финиш")
if !isempty(landmarks)
lm_x = [lm[1] for lm in landmarks]
lm_y = [lm[2] for lm in landmarks]
lm_z = [lm[3] .+ 1.0 for lm in landmarks]
scatter!(plt, lm_x, lm_y, lm_z, marker=(:hexagon), color=:yellow, markersize=6, label="Ориентиры")
end
display(plt)
return x, y, z, start, finish, landmarks
end
# --- 2. Реалистичная симуляция полета первого дрона с ориентирами ---
"""
simulate_drone_realistic(landscape_data, min_height, max_height, base_speed;
max_acceleration=0.2, vz_max=0.5, max_turn_rate_deg=10.0,
fov_angle=45.0, view_distance=10.0)
Симулирует полет первого дрона с более реалистичной динамикой и учетом ориентиров.
"""
function simulate_drone_realistic(landscape_data, min_height, max_height, base_speed;
max_acceleration=0.2, vz_max=0.5, max_turn_rate_deg=10.0,
fov_angle=45.0, view_distance=10.0)
x, y, z, start, finish, landmarks = landscape_data
drone_path = [copy(start)]
drone_heights = [start[3]]
obstacles = [[]]
seen_landmarks_history = [Set{Int}()]
current_pos = copy(start)
velocity = zeros(3)
max_speed = base_speed
turn_rate_rad = deg2rad(max_turn_rate_deg)
landing_phase = false
landing_steps = 10
landing_step_count = 0
landing_target_z = 0.0
for step in 1:2000
if !landing_phase
if norm(current_pos[1:2] .- finish[1:2]) < base_speed * 1.5
landing_phase = true
landing_step_count = 0
xi_end = argmin(abs.(x .- finish[1]))
yi_end = argmin(abs.(y .- finish[2]))
landing_target_z = z[xi_end, yi_end]
println("Начало посадки на шаге $step")
end
end
if landing_phase
landing_step_count += 1
if landing_step_count > landing_steps
break
end
target_pos_xy = finish[1:2]
target_z = current_pos[3] - (current_pos[3] - landing_target_z) / (landing_steps - landing_step_count + 1)
target_pos = [target_pos_xy[1], target_pos_xy[2], target_z]
else
target_pos = finish
end
direction_to_target_xy = normalize(target_pos[1:2] .- current_pos[1:2])
desired_velocity_xy = direction_to_target_xy * max_speed
current_speed_xy = norm(velocity[1:2])
if current_speed_xy > 1e-6
current_direction_xy = velocity[1:2] / current_speed_xy
dot_product = clamp(dot(current_direction_xy, direction_to_target_xy), -1.0, 1.0)
angle_to_target = acos(dot_product)
if angle_to_target > turn_rate_rad
cross_product_z = current_direction_xy[1] * direction_to_target_xy[2] - current_direction_xy[2] * direction_to_target_xy[1]
sign = cross_product_z > 0 ? 1 : -1
cos_turn = cos(turn_rate_rad)
sin_turn = sin(turn_rate_rad) * sign
new_vx = current_direction_xy[1] * cos_turn - current_direction_xy[2] * sin_turn
new_vy = current_direction_xy[1] * sin_turn + current_direction_xy[2] * cos_turn
desired_velocity_xy = [new_vx, new_vy] * current_speed_xy
end
else
desired_velocity_xy = direction_to_target_xy * min(max_acceleration, max_speed)
end
delta_v_xy = desired_velocity_xy .- velocity[1:2]
acceleration_magnitude = norm(delta_v_xy)
if acceleration_magnitude > 1e-10
unit_accel = delta_v_xy / acceleration_magnitude
applied_accel = unit_accel * min(acceleration_magnitude, max_acceleration)
velocity[1:2] .+= applied_accel
end
speed_xy = norm(velocity[1:2])
if speed_xy > max_speed
velocity[1:2] .*= (max_speed / speed_xy)
end
xi_next = argmin(abs.(x .- (current_pos[1] + velocity[1])))
yi_next = argmin(abs.(y .- (current_pos[2] + velocity[2])))
xi_next = clamp(xi_next, 1, length(x))
yi_next = clamp(yi_next, 1, length(y))
terrain_height_next = z[xi_next, yi_next]
desired_altitude = max(min_height, 0.0)
desired_z = terrain_height_next + desired_altitude
desired_vz = desired_z - current_pos[3]
desired_vz = clamp(desired_vz, -vz_max, vz_max)
velocity[3] = desired_vz
next_pos_xy = current_pos[1:2] .+ velocity[1:2]
next_pos_xy[1] = clamp(next_pos_xy[1], x[1], x[end])
next_pos_xy[2] = clamp(next_pos_xy[2], y[1], y[end])
new_z = current_pos[3] + velocity[3]
visible_obstacles = Tuple{Float64, Float64, Float64}[]
visible_landmarks_indices = Set{Int}()
current_direction_xy = velocity[1:2]
current_speed_xy = norm(current_direction_xy)
if current_speed_xy > 1e-6
current_direction_xy ./= current_speed_xy
else
current_direction_xy = direction_to_target_xy
end
for angle in range(-fov_angle/2, fov_angle/2, length=12)
rad = deg2rad(angle)
dir_x = current_direction_xy[1]*cos(rad) - current_direction_xy[2]*sin(rad)
dir_y = current_direction_xy[1]*sin(rad) + current_direction_xy[2]*cos(rad)
check_pos = next_pos_xy .+ [dir_x, dir_y] * view_distance
xi_check = argmin(abs.(x .- check_pos[1]))
yi_check = argmin(abs.(y .- check_pos[2]))
if 1 ≤ xi_check ≤ size(z, 1) && 1 ≤ yi_check ≤ size(z, 2)
obstacle_height = z[xi_check, yi_check]
if obstacle_height + min_height > new_z - 0.1
push!(visible_obstacles, (x[xi_check], y[yi_check], obstacle_height))
end
end
for (idx, lm) in enumerate(landmarks)
lm_vector = lm[1:2] .- current_pos[1:2]
distance_to_lm = norm(lm_vector)
if distance_to_lm > 0 && distance_to_lm <= view_distance
dir_to_lm = lm_vector / distance_to_lm
dot_product_lm = dot(current_direction_xy, dir_to_lm)
if dot_product_lm >= cos(deg2rad(fov_angle / 2))
push!(visible_landmarks_indices, idx)
end
end
end
end
if !isempty(visible_obstacles) && !landing_phase
max_obstacle = maximum(obs[3] for obs in visible_obstacles)
if max_obstacle + min_height > new_z - 0.1
if rand() < 0.5
side = rand([-1.0, 1.0])
avoidance_dir_x = -current_direction_xy[2] * side
avoidance_dir_y = current_direction_xy[1] * side
avoidance_velocity_xy = [avoidance_dir_x, avoidance_dir_y] * (max_speed * 0.7)
desired_velocity_xy = (desired_velocity_xy + avoidance_velocity_xy) / 2
delta_v_xy = desired_velocity_xy .- velocity[1:2]
acceleration_magnitude = norm(delta_v_xy)
if acceleration_magnitude > 1e-10
unit_accel = delta_v_xy / acceleration_magnitude
applied_accel = unit_accel * min(acceleration_magnitude, max_acceleration)
velocity[1:2] .+= applied_accel
end
speed_xy = norm(velocity[1:2])
if speed_xy > max_speed
velocity[1:2] .*= (max_speed / speed_xy)
end
end
end
end
current_pos = [next_pos_xy[1], next_pos_xy[2], new_z]
push!(drone_path, copy(current_pos))
push!(drone_heights, new_z)
push!(obstacles, visible_obstacles)
push!(seen_landmarks_history, visible_landmarks_indices)
end
println("Время полёта первого дрона: $(length(drone_path)) шагов")
return drone_path, drone_heights, obstacles, seen_landmarks_history, start, finish, landmarks
end
# --- 3. Симуляция второго дрона и моделирование связи ---
"""
simulate_second_drone(drone1_path, landscape_data, follow_distance=10.0, follow_height_offset=2.0)
Симулирует полет второго дрона, следуя за первым, и вычисляет качество связи.
"""
function simulate_second_drone(drone1_path, landscape_data, follow_distance=10.0, follow_height_offset=2.0)
x, y, z, _, _, _ = landscape_data
drone2_path = Vector{Vector{Float64}}()
connection_quality_history = Vector{Float64}()
# Начальная позиция второго дрона позади первого
d1_start = drone1_path[1]
# --- Логика определения начального направления ---
# Если есть хотя бы 2 точки у первого дрона, используем их для направления
if length(drone1_path) > 1
start_dir = normalize(drone1_path[2][1:2] - d1_start[1:2])
else
# Если только одна точка, используем фиктивное направление
start_dir = normalize([1.0, 0.0]) # Пример направления
end
# --- Конец логики ---
perpendicular_dir = [-start_dir[2], start_dir[1]] # Перпендикулярно
# Начинаем справа и немного сзади
start_offset = -perpendicular_dir * (follow_distance / 2) - start_dir * follow_distance
d2_start_xy = d1_start[1:2] + start_offset
d2_start_z = d1_start[3] + follow_height_offset
push!(drone2_path, [d2_start_xy[1], d2_start_xy[2], d2_start_z])
# --- Исправление: Добавляем начальное значение качества связи ---
# Можно рассчитать, но для простоты возьмем 1.0 или рассчитаем между стартовыми позициями
# Рассчитаем между начальными позициями:
dx_start = d1_start[1] - d2_start_xy[1]
dy_start = d1_start[2] - d2_start_xy[2]
dz_start = d1_start[3] - d2_start_z
distance_start = sqrt(dx_start^2 + dy_start^2 + dz_start^2)
initial_quality = 1.0
max_comm_range = 30.0
if distance_start > max_comm_range
initial_quality = 0.0
else
initial_quality = 1.0 - (distance_start / max_comm_range)
end
# Проверка на препятствия для начальной линии связи
if initial_quality > 0.1
num_checks = 20
for j in 1:num_checks
t = j / num_checks
check_x = d2_start_xy[1] + (d1_start[1] - d2_start_xy[1]) * t
check_y = d2_start_xy[2] + (d1_start[2] - d2_start_xy[2]) * t
xi_check = argmin(abs.(x .- check_x))
yi_check = argmin(abs.(y .- check_y))
if 1 ≤ xi_check ≤ size(z, 1) && 1 ≤ yi_check ≤ size(z, 2)
terrain_z = z[xi_check, yi_check]
line_z = d2_start_z + (d1_start[3] - d2_start_z) * t
if terrain_z > line_z - 0.5
initial_quality *= 0.5
if initial_quality < 0.1
initial_quality = 0.0
break
end
end
end
end
end
initial_quality = max(0.0, initial_quality)
push!(connection_quality_history, initial_quality)
# --- Конец исправления ---
# Параметры движения второго дрона (проще, чем у первого)
d2_velocity = zeros(3)
d2_max_speed = 1.0 # Максимальная скорость второго дрона
d2_max_acceleration = 0.3
d2_vz_max = 0.6
for i in 2:length(drone1_path)
d1_pos = drone1_path[i]
d2_pos = drone2_path[end]
# Цель второго дрона: точка на заданном расстоянии позади первого
# --- Улучшенная логика определения направления ---
if i > 1
d1_prev_pos = drone1_path[i-1]
# Проверяем, чтобы избежать деления на ноль
if d1_pos[1:2] != d1_prev_pos[1:2]
d1_direction = normalize(d1_pos[1:2] - d1_prev_pos[1:2])
else
# Если позиции совпадают, используем предыдущее направление или фиктивное
if length(drone2_path) > 1
# Примерный предыдущий вектор скорости второго дрона
prev_vel = drone2_path[end][1:2] - drone2_path[end-1][1:2]
if norm(prev_vel) > 1e-6
d1_direction = normalize(prev_vel)
else
d1_direction = [1.0, 0.0]
end
else
d1_direction = [1.0, 0.0]
end
end
else
# Этот случай уже обработан выше, но оставим для полноты
d1_direction = [1.0, 0.0]
end
# --- Конец улучшения ---
# Точка, за которой следует второй дрон
follow_target_xy = d1_pos[1:2] - d1_direction * follow_distance
follow_target_z = d1_pos[3] + follow_height_offset
follow_target = [follow_target_xy[1], follow_target_xy[2], follow_target_z]
# Желаемая скорость второго дрона
desired_vel_xy = normalize(follow_target[1:2] - d2_pos[1:2]) * d2_max_speed
# Простое пропорциональное управление по Z
desired_vz = (follow_target[3] - d2_pos[3]) * 0.5
desired_vz = clamp(desired_vz, -d2_vz_max, d2_vz_max)
# Применяем ускорение к XY
delta_v_xy = desired_vel_xy .- d2_velocity[1:2]
accel_mag = norm(delta_v_xy)
if accel_mag > 1e-10
unit_accel = delta_v_xy / accel_mag
applied_accel = unit_accel * min(accel_mag, d2_max_acceleration)
d2_velocity[1:2] .+= applied_accel
end
speed_xy = norm(d2_velocity[1:2])
if speed_xy > d2_max_speed
d2_velocity[1:2] .*= (d2_max_speed / speed_xy)
end
d2_velocity[3] = desired_vz # Упрощенное управление Z
# Обновляем позицию второго дрона
new_d2_pos = d2_pos .+ d2_velocity
new_d2_pos[1] = clamp(new_d2_pos[1], x[1], x[end])
new_d2_pos[2] = clamp(new_d2_pos[2], y[1], y[end])
push!(drone2_path, new_d2_pos)
# --- Моделирование качества связи ---
quality = 1.0
dx = d1_pos[1] - new_d2_pos[1]
dy = d1_pos[2] - new_d2_pos[2]
dz = d1_pos[3] - new_d2_pos[3]
distance = sqrt(dx^2 + dy^2 + dz^2)
# 1. Затухание сигнала с расстоянием (упрощенно)
max_comm_range = 30.0
if distance > max_comm_range
quality = 0.0
else
# Линейное затухание от 1 до 0
quality = 1.0 - (distance / max_comm_range)
end
# 2. Проверка на препятствия (простая модель)
if quality > 0.1 # Проверяем только если сигнал ещё не нулевой
num_checks = 20
for j in 1:num_checks
t = j / num_checks
check_x = d2_pos[1] + (d1_pos[1] - d2_pos[1]) * t
check_y = d2_pos[2] + (d1_pos[2] - d2_pos[2]) * t
xi_check = argmin(abs.(x .- check_x))
yi_check = argmin(abs.(y .- check_y))
if 1 ≤ xi_check ≤ size(z, 1) && 1 ≤ yi_check ≤ size(z, 2)
terrain_z = z[xi_check, yi_check]
# Линейная интерполяция высоты линии связи
line_z = d2_pos[3] + (d1_pos[3] - d2_pos[3]) * t
if terrain_z > line_z - 0.5 # Если рельеф выше линии связи (с запасом)
quality *= 0.5 # Ухудшаем качество
if quality < 0.1
quality = 0.0
break
end
end
end
end
end
quality = max(0.0, quality) # Убедимся, что качество не отрицательное
push!(connection_quality_history, quality)
end
println("Время полёта второго дрона: $(length(drone2_path)) шагов")
# Добавим проверку длины для отладки
println("Длина истории качества связи: $(length(connection_quality_history)) шагов")
return drone2_path, connection_quality_history
end
# --- 4. Визуализация ---
"""
visualize_flight_with_two_drones(landscape_data,
drone1_path, drone1_obstacles, drone1_seen_landmarks,
drone2_path, connection_quality,
speed, start, finish, landmarks; fps=5)
"""
function visualize_flight_with_two_drones(landscape_data,
drone1_path, drone1_obstacles, drone1_seen_landmarks,
drone2_path, connection_quality,
speed, start, finish, landmarks; fps=5)
x, y, z, _, _, _ = landscape_data
x_unique = unique(vcat([p[1] for p in drone1_path], [p[1] for p in drone2_path]))
y_unique = unique(vcat([p[2] for p in drone1_path], [p[2] for p in drone2_path]))
xidx = Dict(px => argmin(abs.(x .- px)) for px in x_unique)
yidx = Dict(py => argmin(abs.(y .- py)) for py in y_unique)
base_surface = surface(x, y, z', c=:terrain, legend=false, aspect_ratio=:auto, axis=false)
base_2d = plot(aspect_ratio=1, legend=false, axis=false)
contour!(base_2d, x, y, z', c=:terrain, alpha=0.3)
scatter!(base_surface, [start[1]], [start[2]], [start[3]], marker=:circle, markercolor=:green, label="Старт", markersize=6)
scatter!(base_surface, [finish[1]], [finish[2]], [finish[3]], marker=:circle, markercolor=:red, label="Финиш", markersize=6)
scatter!(base_2d, [start[1]], [start[2]], marker=:circle, markercolor=:green, label="Старт")
scatter!(base_2d, [finish[1]], [finish[2]], marker=:circle, markercolor=:red, label="Финиш")
if !isempty(landmarks)
lm_x_all = [lm[1] for lm in landmarks]
lm_y_all = [lm[2] for lm in landmarks]
lm_z_all = [lm[3] .+ 1.0 for lm in landmarks]
scatter!(base_surface, lm_x_all, lm_y_all, lm_z_all, marker=:hexagon, markercolor=:yellow, markersize=5, label="Ориентиры")
scatter!(base_2d, lm_x_all, lm_y_all, marker=:hexagon, markercolor=:yellow, markersize=4, label="Ориентиры")
end
# Определяем длину анимации по кратчайшей траектории
min_len = min(length(drone1_path), length(drone2_path))
anim = @animate for i in 1:min_len
plt1 = deepcopy(base_surface)
plt2 = deepcopy(base_2d)
# Позиции дронов
px1, py1, pz1 = drone1_path[i]
px2, py2, pz2 = drone2_path[i]
# Цвет связи в зависимости от качества
conn_quality = connection_quality[i]
conn_color = :red
if conn_quality > 0.7
conn_color = :green
elseif conn_quality > 0.3
conn_color = :orange
end
conn_alpha = clamp(conn_quality, 0.1, 1.0)
# 3D график
scatter!(plt1, [px1], [py1], [pz1], markersize=6, markercolor=:blue, marker=:circle, label="Дрон 1")
scatter!(plt1, [px2], [py2], [pz2], markersize=6, markercolor=:purple, marker=:circle, label="Дрон 2")
plot!(plt1, [px1, px2], [py1, py2], [pz1, pz2],
linecolor=conn_color, linewidth=2, label="Связь", alpha=conn_alpha)
# Фокусировка на первом дроне
xlims!(plt1, px1 - 15, px1 + 15)
ylims!(plt1, py1 - 15, py1 + 15)
xi1, yi1 = xidx[px1], yidx[py1]
height_above1 = pz1 - z[xi1, yi1]
arrow1 = i == 1 ? "→" : (height_above1 > (drone1_path[i - 1][3] - z[xidx[drone1_path[i - 1][1]], yidx[drone1_path[i - 1][2]]]) ? "↑" : "↓")
title!(plt1, "Дрон 1: $(round(height_above1, digits=1)) м $arrow1 | Связь: $(round(conn_quality, digits=2))")
# 2D график
if i > 1
path1 = drone1_path[1:i]
path2 = drone2_path[1:i]
plot!(plt2, [p[1] for p in path1], [p[2] for p in path1], linewidth=2, linecolor=:blue, label="Тр. Дрон 1")
plot!(plt2, [p[1] for p in path2], [p[2] for p in path2], linewidth=2, linecolor=:purple, label="Тр. Дрон 2")
plot!(plt2, [px1, px2], [py1, py2],
linecolor=conn_color, linewidth=2, label="Связь", alpha=conn_alpha)
end
scatter!(plt2, [px1], [py1], markersize=6, markercolor=:blue, marker=:circle, label=false)
scatter!(plt2, [px2], [py2], markersize=6, markercolor=:purple, marker=:circle, label=false)
# Препятствия для первого дрона
if !isempty(drone1_obstacles[i])
obs_x = [o[1] for o in drone1_obstacles[i]]
obs_y = [o[2] for o in drone1_obstacles[i]]
obs_z = [o[3] .+ 1.0 for o in drone1_obstacles[i]]
scatter!(plt1, obs_x, obs_y, obs_z, markersize=4, markercolor=:orange, marker=:xcross, label="Препятствия")
scatter!(plt2, obs_x, obs_y, markersize=3, markercolor=:orange, marker=:xcross, label="Препятствия")
end
# Видимые ориентиры для первого дрона
if !isempty(landmarks) && !isempty(drone1_seen_landmarks[i])
visible_lm_indices = collect(drone1_seen_landmarks[i])
visible_lm_coords = landmarks[visible_lm_indices]
v_lm_x = [lm[1] for lm in visible_lm_coords]
v_lm_y = [lm[2] for lm in visible_lm_coords]
v_lm_z = [lm[3] .+ 2.0 for lm in visible_lm_coords]
scatter!(plt1, v_lm_x, v_lm_y, v_lm_z, markersize=7, markercolor=:cyan, marker=:pentagon, label="Видимые ориентиры")
scatter!(plt2, v_lm_x, v_lm_y, markersize=6, markercolor=:cyan, marker=:pentagon, label="Видимые ориентиры")
for (j, lm) in enumerate(visible_lm_coords)
plot!(plt1, [px1, lm[1]], [py1, lm[2]], [pz1, lm[3] + 2.0],
linecolor=:cyan, linestyle=:dash, linewidth=1, label=false)
end
end
plot(plt1, plt2, layout = @layout([a{0.7w} b]), size=(1000, 400))
end every max(1, round(Int, 1))
gif_file = gif(anim, "flight_with_two_drones_and_comm.gif", fps=fps)
println("Анимация сохранена в файл: flight_with_two_drones_and_comm.gif")
return gif_file
end
# --- Основной блок выполнения ---
println("=== Генерация ландшафта ===")
landscape_data = generate_landscape(100, 100);
println("\n=== Симуляция полета первого дрона ===")
base_speed = 1.0
drone1_path, drone1_heights, drone1_obstacles, drone1_seen_lm, start, finish, landmarks =
simulate_drone_realistic(landscape_data, -5, 5, base_speed);
println("\n=== Симуляция полета второго дрона ===")
drone2_path, connection_quality = simulate_second_drone(drone1_path, landscape_data, 12.0, 1.0);
println("\n=== Создание визуализации ===")
visualize_flight_with_two_drones(landscape_data,
drone1_path, drone1_obstacles, drone1_seen_lm,
drone2_path, connection_quality,
base_speed, start, finish, landmarks, fps=5);
println("\n=== Проект завершен ===")