about summary refs log tree commit diff stats
path: root/src/subway_map.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/subway_map.cpp')
-rw-r--r--src/subway_map.cpp700
1 files changed, 700 insertions, 0 deletions
diff --git a/src/subway_map.cpp b/src/subway_map.cpp new file mode 100644 index 0000000..8364714 --- /dev/null +++ b/src/subway_map.cpp
@@ -0,0 +1,700 @@
1#include "subway_map.h"
2
3#include <wx/dcbuffer.h>
4#include <wx/dcgraph.h>
5
6#include <sstream>
7
8#include "ap_state.h"
9#include "game_data.h"
10#include "global.h"
11#include "tracker_state.h"
12
13constexpr int AREA_ACTUAL_SIZE = 21;
14constexpr int OWL_ACTUAL_SIZE = 32;
15
16enum class ItemDrawType { kNone, kBox, kOwl };
17
18SubwayMap::SubwayMap(wxWindow *parent) : wxPanel(parent, wxID_ANY) {
19 SetBackgroundStyle(wxBG_STYLE_PAINT);
20
21 map_image_ =
22 wxImage(GetAbsolutePath("assets/subway.png").c_str(), wxBITMAP_TYPE_PNG);
23 if (!map_image_.IsOk()) {
24 return;
25 }
26
27 owl_image_ =
28 wxImage(GetAbsolutePath("assets/owl.png").c_str(), wxBITMAP_TYPE_PNG);
29 if (!owl_image_.IsOk()) {
30 return;
31 }
32
33 unchecked_eye_ =
34 wxBitmap(wxImage(GetAbsolutePath("assets/unchecked.png").c_str(),
35 wxBITMAP_TYPE_PNG)
36 .Scale(32, 32));
37 checked_eye_ = wxBitmap(
38 wxImage(GetAbsolutePath("assets/checked.png").c_str(), wxBITMAP_TYPE_PNG)
39 .Scale(32, 32));
40
41 tree_ = std::make_unique<quadtree::Quadtree<int, GetItemBox>>(
42 quadtree::Box<float>{0, 0, static_cast<float>(map_image_.GetWidth()),
43 static_cast<float>(map_image_.GetHeight())});
44 for (const SubwayItem &subway_item : GD_GetSubwayItems()) {
45 tree_->add(subway_item.id);
46 }
47
48 Redraw();
49
50 scroll_timer_ = new wxTimer(this);
51
52 Bind(wxEVT_PAINT, &SubwayMap::OnPaint, this);
53 Bind(wxEVT_MOTION, &SubwayMap::OnMouseMove, this);
54 Bind(wxEVT_MOUSEWHEEL, &SubwayMap::OnMouseScroll, this);
55 Bind(wxEVT_LEAVE_WINDOW, &SubwayMap::OnMouseLeave, this);
56 Bind(wxEVT_LEFT_DOWN, &SubwayMap::OnMouseClick, this);
57 Bind(wxEVT_TIMER, &SubwayMap::OnTimer, this);
58
59 zoom_slider_ = new wxSlider(this, wxID_ANY, 0, 0, 8, {15, 15});
60 zoom_slider_->Bind(wxEVT_SLIDER, &SubwayMap::OnZoomSlide, this);
61
62 help_button_ = new wxButton(this, wxID_ANY, "Help");
63 help_button_->Bind(wxEVT_BUTTON, &SubwayMap::OnClickHelp, this);
64 SetUpHelpButton();
65}
66
67void SubwayMap::OnConnect() {
68 networks_.Clear();
69
70 std::map<std::string, std::vector<int>> tagged;
71 for (const SubwayItem &subway_item : GD_GetSubwayItems()) {
72 if (AP_HasEarlyColorHallways() &&
73 (subway_item.special == "starting_room_paintings" ||
74 subway_item.special == "early_color_hallways")) {
75 tagged["early_color_hallways"].push_back(subway_item.id);
76 }
77
78 if (AP_IsPaintingShuffle() && !subway_item.paintings.empty()) {
79 continue;
80 }
81
82 for (const std::string &tag : subway_item.tags) {
83 tagged[tag].push_back(subway_item.id);
84 }
85
86 if (!AP_IsSunwarpShuffle() && subway_item.sunwarp &&
87 subway_item.sunwarp->type != SubwaySunwarpType::kFinal) {
88 std::ostringstream tag;
89 tag << "sunwarp" << subway_item.sunwarp->dots;
90
91 tagged[tag.str()].push_back(subway_item.id);
92 }
93
94 if (!AP_IsPilgrimageEnabled() &&
95 (subway_item.special == "sun_painting" ||
96 subway_item.special == "sun_painting_exit")) {
97 tagged["sun_painting"].push_back(subway_item.id);
98 }
99 }
100
101 for (const auto &[tag, items] : tagged) {
102 // Pairwise connect all items with the same tag.
103 for (auto tag_it1 = items.begin(); std::next(tag_it1) != items.end();
104 tag_it1++) {
105 for (auto tag_it2 = std::next(tag_it1); tag_it2 != items.end();
106 tag_it2++) {
107 networks_.AddLink(*tag_it1, *tag_it2);
108 }
109 }
110 }
111
112 checked_paintings_.clear();
113}
114
115void SubwayMap::UpdateIndicators() {
116 if (AP_IsPaintingShuffle()) {
117 for (const std::string &painting_id : AP_GetCheckedPaintings()) {
118 if (!checked_paintings_.count(painting_id)) {
119 checked_paintings_.insert(painting_id);
120
121 if (AP_GetPaintingMapping().count(painting_id)) {
122 networks_.AddLink(GD_GetSubwayItemForPainting(painting_id),
123 GD_GetSubwayItemForPainting(
124 AP_GetPaintingMapping().at(painting_id)));
125 }
126 }
127 }
128 }
129
130 Redraw();
131}
132
133void SubwayMap::UpdateSunwarp(SubwaySunwarp from_sunwarp,
134 SubwaySunwarp to_sunwarp) {
135 networks_.AddLink(GD_GetSubwayItemForSunwarp(from_sunwarp),
136 GD_GetSubwayItemForSunwarp(to_sunwarp));
137}
138
139void SubwayMap::Zoom(bool in) {
140 wxPoint focus_point;
141
142 if (mouse_position_) {
143 focus_point = *mouse_position_;
144 } else {
145 focus_point = {GetSize().GetWidth() / 2, GetSize().GetHeight() / 2};
146 }
147
148 if (in) {
149 if (zoom_ < 3.0) {
150 SetZoom(zoom_ + 0.25, focus_point);
151 }
152 } else {
153 if (zoom_ > 1.0) {
154 SetZoom(zoom_ - 0.25, focus_point);
155 }
156 }
157}
158
159void SubwayMap::OnPaint(wxPaintEvent &event) {
160 if (GetSize() != rendered_.GetSize()) {
161 wxSize panel_size = GetSize();
162 wxSize image_size = map_image_.GetSize();
163
164 render_x_ = 0;
165 render_y_ = 0;
166 render_width_ = panel_size.GetWidth();
167 render_height_ = panel_size.GetHeight();
168
169 if (image_size.GetWidth() * panel_size.GetHeight() >
170 panel_size.GetWidth() * image_size.GetHeight()) {
171 render_height_ = (panel_size.GetWidth() * image_size.GetHeight()) /
172 image_size.GetWidth();
173 render_y_ = (panel_size.GetHeight() - render_height_) / 2;
174 } else {
175 render_width_ = (image_size.GetWidth() * panel_size.GetHeight()) /
176 image_size.GetHeight();
177 render_x_ = (panel_size.GetWidth() - render_width_) / 2;
178 }
179
180 SetZoomPos({zoom_x_, zoom_y_});
181
182 SetUpHelpButton();
183 }
184
185 wxBufferedPaintDC dc(this);
186 dc.SetBackground(*wxWHITE_BRUSH);
187 dc.Clear();
188
189 {
190 wxMemoryDC rendered_dc;
191 rendered_dc.SelectObject(rendered_);
192
193 int dst_x;
194 int dst_y;
195 int dst_w;
196 int dst_h;
197 int src_x;
198 int src_y;
199 int src_w;
200 int src_h;
201
202 int zoomed_width = render_width_ * zoom_;
203 int zoomed_height = render_height_ * zoom_;
204
205 if (zoomed_width <= GetSize().GetWidth()) {
206 dst_x = (GetSize().GetWidth() - zoomed_width) / 2;
207 dst_w = zoomed_width;
208 src_x = 0;
209 src_w = map_image_.GetWidth();
210 } else {
211 dst_x = 0;
212 dst_w = GetSize().GetWidth();
213 src_x = -zoom_x_ * map_image_.GetWidth() / render_width_ / zoom_;
214 src_w =
215 GetSize().GetWidth() * map_image_.GetWidth() / render_width_ / zoom_;
216 }
217
218 if (zoomed_height <= GetSize().GetHeight()) {
219 dst_y = (GetSize().GetHeight() - zoomed_height) / 2;
220 dst_h = zoomed_height;
221 src_y = 0;
222 src_h = map_image_.GetHeight();
223 } else {
224 dst_y = 0;
225 dst_h = GetSize().GetHeight();
226 src_y = -zoom_y_ * map_image_.GetWidth() / render_width_ / zoom_;
227 src_h =
228 GetSize().GetHeight() * map_image_.GetWidth() / render_width_ / zoom_;
229 }
230
231 wxGCDC gcdc(dc);
232 gcdc.GetGraphicsContext()->SetInterpolationQuality(wxINTERPOLATION_GOOD);
233 gcdc.StretchBlit(dst_x, dst_y, dst_w, dst_h, &rendered_dc, src_x, src_y,
234 src_w, src_h);
235 }
236
237 if (hovered_item_) {
238 // Note that these requirements are duplicated on OnMouseClick so that it
239 // knows when an item has a hover effect.
240 const SubwayItem &subway_item = GD_GetSubwayItem(*hovered_item_);
241 if (subway_item.door && !GetDoorRequirements(*subway_item.door).empty()) {
242 const std::map<std::string, bool> &report =
243 GetDoorRequirements(*subway_item.door);
244
245 int acc_height = 10;
246 int col_width = 0;
247
248 for (const auto &[text, obtained] : report) {
249 wxSize item_extent = dc.GetTextExtent(text);
250 int item_height = std::max(32, item_extent.GetHeight()) + 10;
251 acc_height += item_height;
252
253 if (item_extent.GetWidth() > col_width) {
254 col_width = item_extent.GetWidth();
255 }
256 }
257
258 int item_width = col_width + 10 + 32;
259 int full_width = item_width + 20;
260
261 wxPoint popup_pos =
262 MapPosToRenderPos({subway_item.x + AREA_ACTUAL_SIZE / 2,
263 subway_item.y + AREA_ACTUAL_SIZE / 2});
264
265 if (popup_pos.x + full_width > GetSize().GetWidth()) {
266 popup_pos.x = GetSize().GetWidth() - full_width;
267 }
268 if (popup_pos.y + acc_height > GetSize().GetHeight()) {
269 popup_pos.y = GetSize().GetHeight() - acc_height;
270 }
271
272 dc.SetPen(*wxTRANSPARENT_PEN);
273 dc.SetBrush(*wxBLACK_BRUSH);
274 dc.DrawRectangle(popup_pos, {full_width, acc_height});
275
276 dc.SetFont(GetFont());
277
278 int cur_height = 10;
279
280 for (const auto &[text, obtained] : report) {
281 wxBitmap *eye_ptr = obtained ? &checked_eye_ : &unchecked_eye_;
282
283 dc.DrawBitmap(*eye_ptr, popup_pos + wxPoint{10, cur_height});
284
285 dc.SetTextForeground(obtained ? *wxWHITE : *wxRED);
286 wxSize item_extent = dc.GetTextExtent(text);
287 dc.DrawText(
288 text,
289 popup_pos +
290 wxPoint{10 + 32 + 10,
291 cur_height + (32 - dc.GetFontMetrics().height) / 2});
292
293 cur_height += 10 + 32;
294 }
295 }
296
297 if (networks_.IsItemInNetwork(*hovered_item_)) {
298 dc.SetBrush(*wxTRANSPARENT_BRUSH);
299
300 for (const auto &[item_id1, item_id2] :
301 networks_.GetNetworkGraph(*hovered_item_)) {
302 const SubwayItem &item1 = GD_GetSubwayItem(item_id1);
303 const SubwayItem &item2 = GD_GetSubwayItem(item_id2);
304
305 wxPoint item1_pos = MapPosToRenderPos(
306 {item1.x + AREA_ACTUAL_SIZE / 2, item1.y + AREA_ACTUAL_SIZE / 2});
307 wxPoint item2_pos = MapPosToRenderPos(
308 {item2.x + AREA_ACTUAL_SIZE / 2, item2.y + AREA_ACTUAL_SIZE / 2});
309
310 int left = std::min(item1_pos.x, item2_pos.x);
311 int top = std::min(item1_pos.y, item2_pos.y);
312 int right = std::max(item1_pos.x, item2_pos.x);
313 int bottom = std::max(item1_pos.y, item2_pos.y);
314
315 int halfwidth = right - left;
316 int halfheight = bottom - top;
317
318 if (halfwidth < 4 || halfheight < 4) {
319 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 4));
320 dc.DrawLine(item1_pos, item2_pos);
321 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2));
322 dc.DrawLine(item1_pos, item2_pos);
323 } else {
324 int ellipse_x;
325 int ellipse_y;
326 double start;
327 double end;
328
329 if (item1_pos.x > item2_pos.x) {
330 ellipse_y = top;
331
332 if (item1_pos.y > item2_pos.y) {
333 ellipse_x = left - halfwidth;
334
335 start = 0;
336 end = 90;
337 } else {
338 ellipse_x = left;
339
340 start = 90;
341 end = 180;
342 }
343 } else {
344 ellipse_y = top - halfheight;
345
346 if (item1_pos.y > item2_pos.y) {
347 ellipse_x = left - halfwidth;
348
349 start = 270;
350 end = 360;
351 } else {
352 ellipse_x = left;
353
354 start = 180;
355 end = 270;
356 }
357 }
358
359 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 4));
360 dc.DrawEllipticArc(ellipse_x, ellipse_y, halfwidth * 2,
361 halfheight * 2, start, end);
362 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2));
363 dc.DrawEllipticArc(ellipse_x, ellipse_y, halfwidth * 2,
364 halfheight * 2, start, end);
365 }
366 }
367 }
368 }
369
370 event.Skip();
371}
372
373void SubwayMap::OnMouseMove(wxMouseEvent &event) {
374 wxPoint mouse_pos = RenderPosToMapPos(event.GetPosition());
375
376 std::vector<int> hovered = tree_->query(
377 {static_cast<float>(mouse_pos.x), static_cast<float>(mouse_pos.y), 2, 2});
378 if (!hovered.empty()) {
379 actual_hover_= hovered[0];
380 } else {
381 actual_hover_ = std::nullopt;
382 }
383
384 if (!sticky_hover_ && actual_hover_ != hovered_item_) {
385 hovered_item_ = actual_hover_;
386
387 Refresh();
388 }
389
390 if (scroll_mode_) {
391 EvaluateScroll(event.GetPosition());
392 }
393
394 mouse_position_ = event.GetPosition();
395
396 event.Skip();
397}
398
399void SubwayMap::OnMouseScroll(wxMouseEvent &event) {
400 double new_zoom = zoom_;
401 if (event.GetWheelRotation() > 0) {
402 new_zoom = std::min(3.0, zoom_ + 0.25);
403 } else {
404 new_zoom = std::max(1.0, zoom_ - 0.25);
405 }
406
407 if (zoom_ != new_zoom) {
408 SetZoom(new_zoom, event.GetPosition());
409 }
410
411 event.Skip();
412}
413
414void SubwayMap::OnMouseLeave(wxMouseEvent &event) {
415 SetScrollSpeed(0, 0);
416 mouse_position_ = std::nullopt;
417}
418
419void SubwayMap::OnMouseClick(wxMouseEvent &event) {
420 bool finished = false;
421
422 if (actual_hover_) {
423 const SubwayItem &subway_item = GD_GetSubwayItem(*actual_hover_);
424 if ((subway_item.door && !GetDoorRequirements(*subway_item.door).empty()) ||
425 networks_.IsItemInNetwork(*hovered_item_)) {
426 if (actual_hover_ != hovered_item_) {
427 hovered_item_ = actual_hover_;
428
429 if (!hovered_item_) {
430 sticky_hover_ = false;
431 }
432
433 Refresh();
434 } else {
435 sticky_hover_ = !sticky_hover_;
436 }
437
438 finished = true;
439 }
440 }
441
442 if (!finished) {
443 if (scroll_mode_) {
444 scroll_mode_ = false;
445
446 SetScrollSpeed(0, 0);
447
448 SetCursor(wxCURSOR_ARROW);
449 } else if (event.GetPosition().x < GetSize().GetWidth() / 6 ||
450 event.GetPosition().x > 5 * GetSize().GetWidth() / 6 ||
451 event.GetPosition().y < GetSize().GetHeight() / 6 ||
452 event.GetPosition().y > 5 * GetSize().GetHeight() / 6) {
453 scroll_mode_ = true;
454
455 EvaluateScroll(event.GetPosition());
456
457 SetCursor(wxCURSOR_CROSS);
458 } else {
459 sticky_hover_ = false;
460 }
461 }
462}
463
464void SubwayMap::OnTimer(wxTimerEvent &event) {
465 SetZoomPos({zoom_x_ + scroll_x_, zoom_y_ + scroll_y_});
466 Refresh();
467}
468
469void SubwayMap::OnZoomSlide(wxCommandEvent &event) {
470 double new_zoom = 1.0 + 0.25 * zoom_slider_->GetValue();
471
472 if (new_zoom != zoom_) {
473 SetZoom(new_zoom, {GetSize().GetWidth() / 2, GetSize().GetHeight() / 2});
474 }
475}
476
477void SubwayMap::OnClickHelp(wxCommandEvent &event) {
478 wxMessageBox(
479 "Zoom in/out using the mouse wheel, Ctrl +/-, or the slider in the "
480 "corner.\nClick on a side of the screen to start panning. It will follow "
481 "your mouse. Click again to stop.\nHover over a door to see the "
482 "requirements to open it.\nHover over a warp or active painting to see "
483 "what it is connected to.\nIn painting shuffle, paintings that have not "
484 "yet been checked will not show their connections.\nA green shaded owl "
485 "means that there is a painting entrance there.\nA red shaded owl means "
486 "that there are only painting exits there.\nClick on a door or "
487 "warp to make the popup stick until you click again.",
488 "Subway Map Help");
489}
490
491void SubwayMap::Redraw() {
492 rendered_ = wxBitmap(map_image_);
493
494 wxMemoryDC dc;
495 dc.SelectObject(rendered_);
496
497 wxGCDC gcdc(dc);
498
499 for (const SubwayItem &subway_item : GD_GetSubwayItems()) {
500 ItemDrawType draw_type = ItemDrawType::kNone;
501 const wxBrush *brush_color = wxGREY_BRUSH;
502 std::optional<wxColour> shade_color;
503
504 if (AP_HasEarlyColorHallways() &&
505 (subway_item.special == "starting_room_paintings" ||
506 subway_item.special == "early_color_hallways")) {
507 draw_type = ItemDrawType::kOwl;
508
509 if (subway_item.special == "starting_room_paintings") {
510 shade_color = wxColour(0, 255, 0, 128);
511 } else {
512 shade_color = wxColour(255, 0, 0, 128);
513 }
514 } else if (subway_item.special == "sun_painting") {
515 if (!AP_IsPilgrimageEnabled()) {
516 if (IsDoorOpen(*subway_item.door)) {
517 draw_type = ItemDrawType::kOwl;
518 shade_color = wxColour(0, 255, 0, 128);
519 } else {
520 draw_type = ItemDrawType::kBox;
521 brush_color = wxRED_BRUSH;
522 }
523 }
524 } else if (!subway_item.paintings.empty()) {
525 if (AP_IsPaintingShuffle()) {
526 bool has_checked_painting = false;
527 bool has_unchecked_painting = false;
528 bool has_mapped_painting = false;
529 bool has_codomain_painting = false;
530
531 for (const std::string &painting_id : subway_item.paintings) {
532 if (checked_paintings_.count(painting_id)) {
533 has_checked_painting = true;
534
535 if (AP_GetPaintingMapping().count(painting_id)) {
536 has_mapped_painting = true;
537 } else if (AP_IsPaintingMappedTo(painting_id)) {
538 has_codomain_painting = true;
539 }
540 } else {
541 has_unchecked_painting = true;
542 }
543 }
544
545 if (has_unchecked_painting || has_mapped_painting || has_codomain_painting) {
546 draw_type = ItemDrawType::kOwl;
547
548 if (has_checked_painting) {
549 if (has_mapped_painting) {
550 shade_color = wxColour(0, 255, 0, 128);
551 } else {
552 shade_color = wxColour(255, 0, 0, 128);
553 }
554 }
555 }
556 } else if (!subway_item.tags.empty()) {
557 draw_type = ItemDrawType::kOwl;
558 }
559 } else if (subway_item.door) {
560 draw_type = ItemDrawType::kBox;
561
562 if (IsDoorOpen(*subway_item.door)) {
563 brush_color = wxGREEN_BRUSH;
564 } else {
565 brush_color = wxRED_BRUSH;
566 }
567 }
568
569 wxPoint real_area_pos = {subway_item.x, subway_item.y};
570
571 int real_area_size =
572 (draw_type == ItemDrawType::kOwl ? OWL_ACTUAL_SIZE : AREA_ACTUAL_SIZE);
573
574 if (draw_type == ItemDrawType::kBox) {
575 gcdc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 1));
576 gcdc.SetBrush(*brush_color);
577 gcdc.DrawRectangle(real_area_pos, {real_area_size, real_area_size});
578 } else if (draw_type == ItemDrawType::kOwl) {
579 wxBitmap owl_bitmap = wxBitmap(owl_image_.Scale(
580 real_area_size, real_area_size, wxIMAGE_QUALITY_BILINEAR));
581 gcdc.DrawBitmap(owl_bitmap, real_area_pos);
582
583 if (shade_color) {
584 gcdc.SetBrush(wxBrush(*shade_color));
585 gcdc.DrawRectangle(real_area_pos, {real_area_size, real_area_size});
586 }
587 }
588 }
589}
590
591void SubwayMap::SetUpHelpButton() {
592 help_button_->SetPosition({
593 GetSize().GetWidth() - help_button_->GetSize().GetWidth() - 15,
594 15,
595 });
596}
597
598void SubwayMap::EvaluateScroll(wxPoint pos) {
599 int scroll_x;
600 int scroll_y;
601 if (pos.x < GetSize().GetWidth() / 9) {
602 scroll_x = 20;
603 } else if (pos.x < GetSize().GetWidth() / 6) {
604 scroll_x = 5;
605 } else if (pos.x > 8 * GetSize().GetWidth() / 9) {
606 scroll_x = -20;
607 } else if (pos.x > 5 * GetSize().GetWidth() / 6) {
608 scroll_x = -5;
609 } else {
610 scroll_x = 0;
611 }
612 if (pos.y < GetSize().GetHeight() / 9) {
613 scroll_y = 20;
614 } else if (pos.y < GetSize().GetHeight() / 6) {
615 scroll_y = 5;
616 } else if (pos.y > 8 * GetSize().GetHeight() / 9) {
617 scroll_y = -20;
618 } else if (pos.y > 5 * GetSize().GetHeight() / 6) {
619 scroll_y = -5;
620 } else {
621 scroll_y = 0;
622 }
623
624 SetScrollSpeed(scroll_x, scroll_y);
625}
626
627wxPoint SubwayMap::MapPosToRenderPos(wxPoint pos) const {
628 return {static_cast<int>(pos.x * render_width_ * zoom_ /
629 map_image_.GetSize().GetWidth() +
630 zoom_x_),
631 static_cast<int>(pos.y * render_width_ * zoom_ /
632 map_image_.GetSize().GetWidth() +
633 zoom_y_)};
634}
635
636wxPoint SubwayMap::MapPosToVirtualPos(wxPoint pos) const {
637 return {static_cast<int>(pos.x * render_width_ * zoom_ /
638 map_image_.GetSize().GetWidth()),
639 static_cast<int>(pos.y * render_width_ * zoom_ /
640 map_image_.GetSize().GetWidth())};
641}
642
643wxPoint SubwayMap::RenderPosToMapPos(wxPoint pos) const {
644 return {
645 std::clamp(static_cast<int>((pos.x - zoom_x_) * map_image_.GetWidth() /
646 render_width_ / zoom_),
647 0, map_image_.GetWidth() - 1),
648 std::clamp(static_cast<int>((pos.y - zoom_y_) * map_image_.GetWidth() /
649 render_width_ / zoom_),
650 0, map_image_.GetHeight() - 1)};
651}
652
653void SubwayMap::SetZoomPos(wxPoint pos) {
654 if (render_width_ * zoom_ <= GetSize().GetWidth()) {
655 zoom_x_ = (GetSize().GetWidth() - render_width_ * zoom_) / 2;
656 } else {
657 zoom_x_ = std::clamp(
658 pos.x, GetSize().GetWidth() - static_cast<int>(render_width_ * zoom_),
659 0);
660 }
661 if (render_height_ * zoom_ <= GetSize().GetHeight()) {
662 zoom_y_ = (GetSize().GetHeight() - render_height_ * zoom_) / 2;
663 } else {
664 zoom_y_ = std::clamp(
665 pos.y, GetSize().GetHeight() - static_cast<int>(render_height_ * zoom_),
666 0);
667 }
668}
669
670void SubwayMap::SetScrollSpeed(int scroll_x, int scroll_y) {
671 bool should_timer = (scroll_x != 0 || scroll_y != 0);
672 if (should_timer != scroll_timer_->IsRunning()) {
673 if (should_timer) {
674 scroll_timer_->Start(1000 / 60);
675 } else {
676 scroll_timer_->Stop();
677 }
678 }
679
680 scroll_x_ = scroll_x;
681 scroll_y_ = scroll_y;
682}
683
684void SubwayMap::SetZoom(double zoom, wxPoint static_point) {
685 wxPoint map_pos = RenderPosToMapPos(static_point);
686 zoom_ = zoom;
687
688 wxPoint virtual_pos = MapPosToVirtualPos(map_pos);
689 SetZoomPos(-(virtual_pos - static_point));
690
691 Refresh();
692
693 zoom_slider_->SetValue((zoom - 1.0) / 0.25);
694}
695
696quadtree::Box<float> SubwayMap::GetItemBox::operator()(const int &id) const {
697 const SubwayItem &subway_item = GD_GetSubwayItem(id);
698 return {static_cast<float>(subway_item.x), static_cast<float>(subway_item.y),
699 AREA_ACTUAL_SIZE, AREA_ACTUAL_SIZE};
700}