qHexWalker 0.0.1
Hexagonal Grid Pathfinding & Maze Visualization on Interactive Maps
Loading...
Searching...
No Matches
h3MazeAdapter.cpp
Go to the documentation of this file.
1#include "h3MazeAdapter.h"
2#include "h3MazeGenerator.h"
3
4#include <QtConcurrent/qtconcurrentrun.h>
5#include <ranges>
6
7H3MazeAdapter::H3MazeAdapter(QObject *parent) : QObject(parent) { mazeGenerator_ = new H3MazeGenerator(this); }
8
10 // Ждем завершения всех асинхронных задач перед уничтожением
11 for (auto &future : pendingFutures_) {
12 if (future.isRunning()) {
13 future.waitForFinished();
14 }
15 }
16 pendingFutures_.clear();
17}
18
19void H3MazeAdapter::generateMazeAsync(const double lat, const double lon, const int kRingRadius) {
20 // Используем QPointer для безопасного доступа к this из другого потока
21 QPointer self(this);
22
23 const auto future = QtConcurrent::run([self, lat, lon, kRingRadius] {
24 // Проверяем, что объект все еще существует
25 if (!self) {
26 spdlog::warn("H3MazeAdapter was deleted before maze generation completed");
27 return;
28 }
29
30 try {
31 self->generateMaze(lat, lon, kRingRadius);
32 } catch (const std::exception &e) {
33 spdlog::critical("Maze generation failed: {}", e.what());
34 }
35 });
36
37 // Сохраняем future для отслеживания и корректного завершения
38 pendingFutures_.append(future);
39
40 // Очищаем завершенные futures для экономии памяти
41 pendingFutures_.erase(
42 std::ranges::remove_if(pendingFutures_, [](const QFuture<void> &f) { return f.isFinished(); }).begin(),
43 pendingFutures_.end());
44}
45
46void H3MazeAdapter::generateMaze(const double lat, const double lon, const int kRingRadius) {
47 std::unordered_set<H3Index> walls;
48 // Конвертируем координату в H3
49 const LatLng ll{.lat = lat, .lng = lon};
50
51 H3Index centerCell = H3_NULL;
52 H3Error err = E_SUCCESS;
53 err = latLngToCell(&ll, 3, &centerCell);
54 if (err != E_SUCCESS) {
55 spdlog::warn("{} {}", "Failed to convert center coordinates to H3", describeH3Error(err));
56 }
57 int radius = kRingRadius;
58 spdlog::info("Generating maze at center cell with radius {}", radius);
59
60 // Генерируем клеточный лабиринт (возвращает клетки-стены)
61 try {
62 walls = mazeGenerator_->generateMaze(centerCell, radius);
63 } catch (const std::exception &e) {
64 spdlog::error("{}", e.what());
65 }
66
67 spdlog::info("Cell maze generated: {} wall cells", walls.size());
68
69 // кольцо вокруг лабиринта с радиусом на 1 больше
70 int64_t ringSize = 0;
71 radius++;
72 err = maxGridDiskSize(radius, &ringSize);
73 if (err != E_SUCCESS) {
74 spdlog::error("maxGridDiskSize failed: {}", describeH3Error(err));
75 return; // Прерываем выполнение при критической ошибке
76 }
77
78 std::vector<H3Index> ring1st(ringSize);
79 err = gridRing(centerCell, radius, ring1st.data());
80 if (err != E_SUCCESS) {
81 spdlog::error("gridRing failed: {}", describeH3Error(err));
82 return; // Прерываем выполнение при критической ошибке
83 }
84
85 // Фильтруем невалидные ячейки из ring
86 std::erase_if(ring1st, [](const H3Index cell) { return cell == H3_NULL || !isValidCell(cell); });
87
88 if (ring1st.empty()) {
89 spdlog::error("No valid cells in ring after filtering");
90 return;
91 }
92
93 const H3Index zeroCell = ring1st.front();
94 const H3Index middleCell = getMiddleOfRing(ring1st, zeroCell);
95
96 if (middleCell == H3_NULL) {
97 spdlog::error("Failed to find middle cell in ring");
98 return;
99 }
100
101 deleteStartEndEntities(zeroCell, middleCell, walls);
102
103 // The first cell is the entrance, skip it.
104 for (auto const &cellId : ring1st | std::views::drop(1)) {
105 if (cellId == middleCell) {
106 continue;
107 }
108 if (!isValidCell(cellId)) {
109 continue;
110 }
111 if (walls.contains(cellId)) {
112 continue;
113 }
114 walls.insert(cellId);
115 }
116
117 // Вычисляем максимальный радиус лабиринта
118 LatLng centerLatLng;
119 cellToLatLng(centerCell, &centerLatLng);
120 double maxDistance = 0.0;
121
122 for (const auto &wallCell : walls) {
123 if (!isValidCell(wallCell)) {
124 continue;
125 }
126 LatLng wallLatLng;
127 if (const auto err2 = cellToLatLng(wallCell, &wallLatLng); err2 != E_SUCCESS) {
128 continue;
129 }
130 if (const double distance = greatCircleDistanceM(&centerLatLng, &wallLatLng); distance > maxDistance) {
131 maxDistance = distance;
132 }
133 }
134
135 // Добавляем буфер 150'000 м, т.к. Лабиринт неидеальный круг из-за h3 rings
136 const double radiusWithBuffer = maxDistance + 150000;
137 const QGeoCoordinate center(lat, lon);
138
139 spdlog::info("Maze radius calculated: max distance = {} m, with buffer = {} m", maxDistance, radiusWithBuffer);
140
141 // Marshal results back to GUI thread
142 QMetaObject::invokeMethod(
143 this,
144 [this, walls, center, radiusWithBuffer] {
145 // Визуализация: объединяем все стены в полигоны и отправляем
146 if (const auto mergedPolygons = cellsToMergedPolygons(walls); !mergedPolygons.empty()) {
147 emit mazePolygonsComputed(mergedPolygons);
148 spdlog::info("Maze polygons computed and emitted");
149 }
150
151 // Передаем стены для A* алгоритма
152 emit mazeWallsGenerated(walls);
153
154 // Передаем центр и радиус лабиринта
155 emit mazeRadiusComputed(center, radiusWithBuffer);
156
157 spdlog::info("Maze generation complete: {} wall cells", walls.size());
158 },
159 Qt::QueuedConnection);
160}
161
162std::vector<QVariantList> H3MazeAdapter::cellsToMergedPolygons(const std::unordered_set<H3Index> &cells) {
163 if (cells.empty()) {
164 return {};
165 }
166
167 // Filter valid cells - remove H3_NULL, pentagons, and ensure same resolution
168 std::vector<H3Index> cellsVec;
169 cellsVec.reserve(cells.size());
170
171 int targetRes = -1;
172 for (const auto &cell : cells) {
173 if (cell == H3_NULL || !isValidCell(cell)) {
174 continue;
175 }
176 if (isPentagon(cell)) {
177 continue; // Skip pentagons - they cause issues with cellsToLinkedMultiPolygon
178 }
179
180 const int res = getResolution(cell);
181 if (targetRes == -1) {
182 targetRes = res;
183 } else if (res != targetRes) {
184 continue; // Skip cells with different resolution
185 }
186
187 cellsVec.emplace_back(cell);
188 }
189
190 if (cellsVec.empty()) {
191 spdlog::warn("No valid cells to convert to polygons");
192 return {};
193 }
194
195 spdlog::info("Converting {} valid cells (res={}) to polygons", cellsVec.size(), targetRes);
196
197 LinkedGeoPolygon polygon{};
198
199 // RAII: Автоматически освобождаем память при любом выходе из функции
200 auto cleanup = qScopeGuard([&polygon] { destroyLinkedMultiPolygon(&polygon); });
201
202 if (const H3Error err = cellsToLinkedMultiPolygon(cellsVec.data(), static_cast<int>(cellsVec.size()), &polygon);
203 err != E_SUCCESS) {
204 spdlog::warn("cellsToLinkedMultiPolygon failed: {} (cells count: {})", describeH3Error(err), cellsVec.size());
205 return {}; // cleanup вызовется автоматически
206 }
207
208 // Process all polygons in the linked list
209 std::vector<QVariantList> result;
210 const LinkedGeoPolygon *currentPoly = &polygon;
211 while (currentPoly != nullptr) {
212 // Process outer loop (first loop is the outer boundary)
213 if (currentPoly->first != nullptr) {
214 QVariantList polyPath;
215
216 const LinkedLatLng *currentVertex = currentPoly->first->first;
217 double prevLng = 0.0;
218 bool isFirst = true;
219
220 while (currentVertex != nullptr) {
221 const double lat = radsToDegs(currentVertex->vertex.lat);
222 double lng = radsToDegs(currentVertex->vertex.lng);
223
224 // Обработка антимеридиана - корректируем долготу если скачок > 180°
225 if (!isFirst) {
226 if (const double delta = lng - prevLng; delta > 180.0) {
227 lng -= 360.0;
228 } else if (delta < -180.0) {
229 lng += 360.0;
230 }
231 }
232
233 polyPath.emplace_back(QVariant::fromValue(QGeoCoordinate{lat, lng, 0}));
234 prevLng = lng;
235 isFirst = false;
236 currentVertex = currentVertex->next;
237 }
238
239 // Close the polygon by adding the first point at the end
240 if (!polyPath.isEmpty() && currentPoly->first->first != nullptr) {
241 const double lat = radsToDegs(currentPoly->first->first->vertex.lat);
242 double lng = radsToDegs(currentPoly->first->first->vertex.lng);
243
244 // Корректируем замыкающую точку относительно последней
245 if (const double delta = lng - prevLng; delta > 180.0) {
246 lng -= 360.0;
247 } else if (delta < -180.0) {
248 lng += 360.0;
249 }
250
251 polyPath.emplace_back(QVariant::fromValue(QGeoCoordinate{lat, lng, 0}));
252 }
253
254 if (!polyPath.isEmpty()) {
255 result.emplace_back(std::move(polyPath));
256 }
257 }
258
259 currentPoly = currentPoly->next;
260 }
261
262 // cleanup (qScopeGuard) автоматически вызовет destroyLinkedMultiPolygon
263 // при выходе из функции
264
265 spdlog::info("Converted {} cells to {} polygons", cells.size(), result.size());
266 return result;
267}
268void H3MazeAdapter::deleteStartEndEntities(const H3Index start, const H3Index end, std::unordered_set<H3Index> &walls) {
269 // start
270 int64_t maxSize = 0;
271 constexpr int kRingSize = 3;
272 H3Error err = maxGridDiskSize(kRingSize, &maxSize);
273 if (err != E_SUCCESS) {
274 spdlog::warn(describeH3Error(err));
275 }
276
277 std::vector<H3Index> disk(maxSize);
278 err = gridDisk(start, kRingSize, disk.data());
279 if (err != E_SUCCESS) {
280 spdlog::warn(describeH3Error(err));
281 }
282 for (auto d : disk) {
283 if (walls.contains(d)) {
284 walls.erase(d);
285 }
286 }
287
288 // end
289 std::vector<H3Index> disk2(maxSize);
290 err = gridDisk(end, kRingSize, disk2.data());
291 if (err != E_SUCCESS) {
292 spdlog::warn(describeH3Error(err));
293 }
294 for (auto d : disk2) {
295 if (walls.contains(d)) {
296 walls.erase(d);
297 }
298 }
299}
300H3Index H3MazeAdapter::getMiddleOfRing(const std::vector<H3Index> &distances, const H3Index zeroCell) {
301 LatLng zeroLatLng;
302 cellToLatLng(zeroCell, &zeroLatLng);
303 double dist = 0;
304 LatLng ll;
305 H3Index middleCell = H3_NULL;
306 H3Error err = E_SUCCESS;
307 for (auto const &cellId : distances | std::views::drop(1)) {
308 if (!isValidCell(cellId)) {
309 continue;
310 }
311 err = cellToLatLng(cellId, &ll);
312 if (err != E_SUCCESS) {
313 spdlog::warn(describeH3Error(err));
314 }
315 if (const auto distTemp = greatCircleDistanceM(&zeroLatLng, &ll); distTemp > dist) {
316 dist = distTemp;
317 middleCell = cellId;
318 }
319 }
320 return middleCell;
321}
~H3MazeAdapter() override
QList< QFuture< void > > pendingFutures_
static std::vector< QVariantList > cellsToMergedPolygons(const std::unordered_set< H3Index > &cells)
void mazeWallsGenerated(const std::unordered_set< H3Index > &walls)
void generateMazeAsync(double lat, double lon, int kRingRadius)
void generateMaze(double lat, double lon, int kRingRadius)
static void deleteStartEndEntities(H3Index start, H3Index end, std::unordered_set< H3Index > &walls)
void mazeRadiusComputed(const QGeoCoordinate &center, double radiusMeters)
static H3Index getMiddleOfRing(const std::vector< H3Index > &distances, H3Index zeroCell)
H3MazeAdapter(QObject *parent=nullptr)
H3MazeGenerator * mazeGenerator_
Generates perfect mazes on H3 hexagonal grids.
std::unordered_set< H3Index > generateMaze(H3Index centerCell, int radius)
Generates a maze centered at the given cell.
Procedural maze generation on H3 hexagonal grids using Prim's algorithm.