#include "subway_map.h"
#include <fmt/core.h>
#include <wx/dcbuffer.h>
#include <wx/dcgraph.h>
#include <sstream>
#include "ap_state.h"
#include "game_data.h"
#include "global.h"
#include "tracker_state.h"
constexpr int AREA_ACTUAL_SIZE = 21;
constexpr int OWL_ACTUAL_SIZE = 32;
enum class ItemDrawType { kNone, kBox, kOwl };
namespace {
std::optional<int> GetRealSubwayDoor(const SubwayItem subway_item) {
std::optional<int> subway_door = subway_item.door;
if (AP_IsSunwarpShuffle() && subway_item.sunwarp &&
subway_item.sunwarp->type != SubwaySunwarpType::kFinal) {
int sunwarp_index = subway_item.sunwarp->dots - 1;
if (subway_item.sunwarp->type == SubwaySunwarpType::kExit) {
sunwarp_index += 6;
}
for (const auto &[start_index, mapping] : AP_GetSunwarpMapping()) {
if (start_index == sunwarp_index || mapping.exit_index == sunwarp_index) {
subway_door = GD_GetSunwarpDoors().at(mapping.dots - 1);
}
}
return subway_door;
}
}
} // namespace
SubwayMap::SubwayMap(wxWindow *parent) : wxPanel(parent, wxID_ANY) {
SetBackgroundStyle(wxBG_STYLE_PAINT);
map_image_ =
wxImage(GetAbsolutePath("assets/subway.png").c_str(), wxBITMAP_TYPE_PNG);
if (!map_image_.IsOk()) {
return;
}
owl_image_ =
wxImage(GetAbsolutePath("assets/owl.png").c_str(), wxBITMAP_TYPE_PNG);
if (!owl_image_.IsOk()) {
return;
}
unchecked_eye_ =
wxBitmap(wxImage(GetAbsolutePath("assets/unchecked.png").c_str(),
wxBITMAP_TYPE_PNG)
.Scale(32, 32));
checked_eye_ = wxBitmap(
wxImage(GetAbsolutePath("assets/checked.png").c_str(), wxBITMAP_TYPE_PNG)
.Scale(32, 32));
tree_ = std::make_unique<quadtree::Quadtree<int, GetItemBox>>(
quadtree::Box<float>{0, 0, static_cast<float>(map_image_.GetWidth()),
static_cast<float>(map_image_.GetHeight())});
for (const SubwayItem &subway_item : GD_GetSubwayItems()) {
tree_->add(subway_item.id);
}
Redraw();
scroll_timer_ = new wxTimer(this);
Bind(wxEVT_PAINT, &SubwayMap::OnPaint, this);
Bind(wxEVT_MOTION, &SubwayMap::OnMouseMove, this);
Bind(wxEVT_MOUSEWHEEL, &SubwayMap::OnMouseScroll, this);
Bind(wxEVT_LEAVE_WINDOW, &SubwayMap::OnMouseLeave, this);
Bind(wxEVT_LEFT_DOWN, &SubwayMap::OnMouseClick, this);
Bind(wxEVT_TIMER, &SubwayMap::OnTimer, this);
zoom_slider_ = new wxSlider(this, wxID_ANY, 0, 0, 8, {15, 15});
zoom_slider_->Bind(wxEVT_SLIDER, &SubwayMap::OnZoomSlide, this);
help_button_ = new wxButton(this, wxID_ANY, "Help");
help_button_->Bind(wxEVT_BUTTON, &SubwayMap::OnClickHelp, this);
SetUpHelpButton();
}
void SubwayMap::OnConnect() {
networks_.Clear();
std::map<std::string, std::vector<int>> tagged;
for (const SubwayItem &subway_item : GD_GetSubwayItems()) {
if (AP_HasEarlyColorHallways() &&
subway_item.special == "starting_room_paintings") {
tagged["early_ch"].push_back(subway_item.id);
}
if (AP_IsPaintingShuffle() && !subway_item.paintings.empty()) {
continue;
}
for (const std::string &tag : subway_item.tags) {
tagged[tag].push_back(subway_item.id);
}
if (!AP_IsSunwarpShuffle() && subway_item.sunwarp &&
subway_item.sunwarp->type != SubwaySunwarpType::kFinal) {
std::string tag = fmt::format("subway{}", subway_item.sunwarp->dots);
tagged[tag].push_back(subway_item.id);
}
if (!AP_IsPilgrimageEnabled() &&
(subway_item.special == "sun_painting" ||
subway_item.special == "sun_painting_exit")) {
tagged["sun_painting"].push_back(subway_item.id);
}
}
if (AP_IsSunwarpShuffle()) {
for (const auto &[index, mapping] : AP_GetSunwarpMapping()) {
std::string tag = fmt::format("sunwarp{}", mapping.dots);
SubwaySunwarp fromWarp;
if (index < 6) {
fromWarp.dots = index + 1;
fromWarp.type = SubwaySunwarpType::kEnter;
} else {
fromWarp.dots = index - 5;
fromWarp.type = SubwaySunwarpType::kExit;
}
SubwaySunwarp toWarp;
if (mapping.exit_index < 6) {
toWarp.dots = mapping.exit_index + 1;
toWarp.type = SubwaySunwarpType::kEnter;
} else {
toWarp.dots = mapping.exit_index - 5;
toWarp.type = SubwaySunwarpType::kExit;
}
tagged[tag].push_back(GD_GetSubwayItemForSunwarp(fromWarp));
tagged[tag].push_back(GD_GetSubwayItemForSunwarp(toWarp));
}
}
for (const auto &[tag, items] : tagged) {
// Pairwise connect all items with the same tag.
for (auto tag_it1 = items.begin(); std::next(tag_it1) != items.end();
tag_it1++) {
for (auto tag_it2 = std::next(tag_it1); tag_it2 != items.end();
tag_it2++) {
networks_.AddLink(*tag_it1, *tag_it2);
}
}
}
checked_paintings_.clear();
}
void SubwayMap::UpdateIndicators() {
if (AP_IsPaintingShuffle()) {
for (const std::string &painting_id : AP_GetCheckedPaintings()) {
if (!checked_paintings_.count(painting_id)) {
checked_paintings_.insert(painting_id);
if (AP_GetPaintingMapping().count(painting_id)) {
std::optional<int> from_id = GD_GetSubwayItemForPainting(painting_id);
std::optional<int> to_id = GD_GetSubwayItemForPainting(
AP_GetPaintingMapping().at(painting_id));
if (from_id && to_id) {
networks_.AddLink(*from_id, *to_id);
}
}
}
}
}
Redraw();
}
void SubwayMap::UpdateSunwarp(SubwaySunwarp from_sunwarp,
SubwaySunwarp to_sunwarp) {
networks_.AddLink(GD_GetSubwayItemForSunwarp(from_sunwarp),
GD_GetSubwayItemForSunwarp(to_sunwarp));
}
void SubwayMap::Zoom(bool in) {
wxPoint focus_point;
if (mouse_position_) {
focus_point = *mouse_position_;
} else {
focus_point = {GetSize().GetWidth() / 2, GetSize().GetHeight() / 2};
}
if (in) {
if (zoom_ < 3.0) {
SetZoom(zoom_ + 0.25, focus_point);
}
} else {
if (zoom_ > 1.0) {
SetZoom(zoom_ - 0.25, focus_point);
}
}
}
void SubwayMap::OnPaint(wxPaintEvent &event) {
if (GetSize() != rendered_.GetSize()) {
wxSize panel_size = GetSize();
wxSize image_size = map_image_.GetSize();
render_x_ = 0;
render_y_ = 0;
render_width_ = panel_size.GetWidth();
render_height_ = panel_size.GetHeight();
if (image_size.GetWidth() * panel_size.GetHeight() >
panel_size.GetWidth() * image_size.GetHeight()) {
render_height_ = (panel_size.GetWidth() * image_size.GetHeight()) /
image_size.GetWidth();
render_y_ = (panel_size.GetHeight() - render_height_) / 2;
} else {
render_width_ = (image_size.GetWidth() * panel_size.GetHeight()) /
image_size.GetHeight();
render_x_ = (panel_size.GetWidth() - render_width_) / 2;
}
SetZoomPos({zoom_x_, zoom_y_});
SetUpHelpButton();
}
wxBufferedPaintDC dc(this);
dc.SetBackground(*wxWHITE_BRUSH);
dc.Clear();
{
wxMemoryDC rendered_dc;
rendered_dc.SelectObject(rendered_);
int dst_x;
int dst_y;
int dst_w;
int dst_h;
int src_x;
int src_y;
int src_w;
int src_h;
int zoomed_width = render_width_ * zoom_;
int zoomed_height = render_height_ * zoom_;
if (zoomed_width <= GetSize().GetWidth()) {
dst_x = (GetSize().GetWidth() - zoomed_width) / 2;
dst_w = zoomed_width;
src_x = 0;
src_w = map_image_.GetWidth();
} else {
dst_x = 0;
dst_w = GetSize().GetWidth();
src_x = -zoom_x_ * map_image_.GetWidth() / render_width_ / zoom_;
src_w =
GetSize().GetWidth() * map_image_.GetWidth() / render_width_ / zoom_;
}
if (zoomed_height <= GetSize().GetHeight()) {
dst_y = (GetSize().GetHeight() - zoomed_height) / 2;
dst_h = zoomed_height;
src_y = 0;
src_h = map_image_.GetHeight();
} else {
dst_y = 0;
dst_h = GetSize().GetHeight();
src_y = -zoom_y_ * map_image_.GetWidth() / render_width_ / zoom_;
src_h =
GetSize().GetHeight() * map_image_.GetWidth() / render_width_ / zoom_;
}
wxGCDC gcdc(dc);
gcdc.GetGraphicsContext()->SetInterpolationQuality(wxINTERPOLATION_GOOD);
gcdc.StretchBlit(dst_x, dst_y, dst_w, dst_h, &rendered_dc, src_x, src_y,
src_w, src_h);
}
if (hovered_item_) {
// Note that these requirements are duplicated on OnMouseClick so that it
// knows when an item has a hover effect.
const SubwayItem &subway_item = GD_GetSubwayItem(*hovered_item_);
std::optional<int> subway_door = GetRealSubwayDoor(subway_item);
if (subway_door && !GetDoorRequirements(*subway_door).empty()) {
const std::map<std::string, bool> &report =
GetDoorRequirements(*subway_door);
int acc_height = 10;
int col_width = 0;
for (const auto &[text, obtained] : report) {
wxSize item_extent = dc.GetTextExtent(text);
int item_height = std::max(32, item_extent.GetHeight()) + 10;
acc_height += item_height;
if (item_extent.GetWidth() > col_width) {
col_width = item_extent.GetWidth();
}
}
int item_width = col_width + 10 + 32;
int full_width = item_width + 20;
wxPoint popup_pos =
MapPosToRenderPos({subway_item.x + AREA_ACTUAL_SIZE / 2,
subway_item.y + AREA_ACTUAL_SIZE / 2});
if (popup_pos.x + full_width > GetSize().GetWidth()) {
popup_pos.x = GetSize().GetWidth() - full_width;
}
if (popup_pos.y + acc_height > GetSize().GetHeight()) {
popup_pos.y = GetSize().GetHeight() - acc_height;
}
dc.SetPen(*wxTRANSPARENT_PEN);
dc.SetBrush(*wxBLACK_BRUSH);
dc.DrawRectangle(popup_pos, {full_width, acc_height});
dc.SetFont(GetFont());
int cur_height = 10;
for (const auto &[text, obtained] : report) {
wxBitmap *eye_ptr = obtained ? &checked_eye_ : &unchecked_eye_;
dc.DrawBitmap(*eye_ptr, popup_pos + wxPoint{10, cur_height});
dc.SetTextForeground(obtained ? *wxWHITE : *wxRED);
wxSize item_extent = dc.GetTextExtent(text);
dc.DrawText(
text,
popup_pos +
wxPoint{10 + 32 + 10,
cur_height + (32 - dc.GetFontMetrics().height) / 2});
cur_height += 10 + 32;
}
}
if (networks_.IsItemInNetwork(*hovered_item_)) {
dc.SetBrush(*wxTRANSPARENT_BRUSH);
for (const auto &[item_id1, item_id2] :
networks_.GetNetworkGraph(*hovered_item_)) {
const SubwayItem &item1 = GD_GetSubwayItem(item_id1);
const SubwayItem &item2 = GD_GetSubwayItem(item_id2);
wxPoint item1_pos = MapPosToRenderPos(
{item1.x + AREA_ACTUAL_SIZE / 2, item1.y + AREA_ACTUAL_SIZE / 2});
wxPoint item2_pos = MapPosToRenderPos(
{item2.x + AREA_ACTUAL_SIZE / 2, item2.y + AREA_ACTUAL_SIZE / 2});
int left = std::min(item1_pos.x, item2_pos.x);
int top = std::min(item1_pos.y, item2_pos.y);
int right = std::max(item1_pos.x, item2_pos.x);
int bottom = std::max(item1_pos.y, item2_pos.y);
int halfwidth = right - left;
int halfheight = bottom - top;
if (halfwidth < 4 || halfheight < 4) {
dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 4));
dc.DrawLine(item1_pos, item2_pos);
dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2));
dc.DrawLine(item1_pos, item2_pos);
} else {
int ellipse_x;
int ellipse_y;
double start;
double end;
if (item1_pos.x > item2_pos.x) {
ellipse_y = top;
if (item1_pos.y > item2_pos.y) {
ellipse_x = left - halfwidth;
start = 0;
end = 90;
} else {
ellipse_x = left;
start = 90;
end = 180;
}
} else {
ellipse_y = top - halfheight;
if (item1_pos.y > item2_pos.y) {
ellipse_x = left - halfwidth;
start = 270;
end = 360;
} else {
ellipse_x = left;
start = 180;
end = 270;
}
}
dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 4));
dc.DrawEllipticArc(ellipse_x, ellipse_y, halfwidth * 2,
halfheight * 2, start, end);
dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2));
dc.DrawEllipticArc(ellipse_x, ellipse_y, halfwidth * 2,
halfheight * 2, start, end);
}
}
}
}
event.Skip();
}
void SubwayMap::OnMouseMove(wxMouseEvent &event) {
wxPoint mouse_pos = RenderPosToMapPos(event.GetPosition());
std::vector<int> hovered = tree_->query(
{static_cast<float>(mouse_pos.x), static_cast<float>(mouse_pos.y), 2, 2});
if (!hovered.empty()) {
actual_hover_ = hovered[0];
} else {
actual_hover_ = std::nullopt;
}
if (!sticky_hover_ && actual_hover_ != hovered_item_) {
hovered_item_ = actual_hover_;
Refresh();
}
if (scroll_mode_) {
EvaluateScroll(event.GetPosition());
}
mouse_position_ = event.GetPosition();
event.Skip();
}
void SubwayMap::OnMouseScroll(wxMouseEvent &event) {
double new_zoom = zoom_;
if (event.GetWheelRotation() > 0) {
new_zoom = std::min(3.0, zoom_ + 0.25);
} else {
new_zoom = std::max(1.0, zoom_ - 0.25);
}
if (zoom_ != new_zoom) {
SetZoom(new_zoom, event.GetPosition());
}
event.Skip();
}
void SubwayMap::OnMouseLeave(wxMouseEvent &event) {
SetScrollSpeed(0, 0);
mouse_position_ = std::nullopt;
}
void SubwayMap::OnMouseClick(wxMouseEvent &event) {
bool finished = false;
if (actual_hover_) {
const SubwayItem &subway_item = GD_GetSubwayItem(*actual_hover_);
std::optional<int> subway_door = GetRealSubwayDoor(subway_item);
if ((subway_door && !GetDoorRequirements(*subway_door).empty()) ||
networks_.IsItemInNetwork(*hovered_item_)) {
if (actual_hover_ != hovered_item_) {
hovered_item_ = actual_hover_;
if (!hovered_item_) {
sticky_hover_ = false;
}
Refresh();
} else {
sticky_hover_ = !sticky_hover_;
}
finished = true;
}
}
if (!finished) {
if (scroll_mode_) {
scroll_mode_ = false;
SetScrollSpeed(0, 0);
SetCursor(wxCURSOR_ARROW);
} else if (event.GetPosition().x < GetSize().GetWidth() / 6 ||
event.GetPosition().x > 5 * GetSize().GetWidth() / 6 ||
event.GetPosition().y < GetSize().GetHeight() / 6 ||
event.GetPosition().y > 5 * GetSize().GetHeight() / 6) {
scroll_mode_ = true;
EvaluateScroll(event.GetPosition());
SetCursor(wxCURSOR_CROSS);
} else {
sticky_hover_ = false;
}
}
}
void SubwayMap::OnTimer(wxTimerEvent &event) {
SetZoomPos({zoom_x_ + scroll_x_, zoom_y_ + scroll_y_});
Refresh();
}
void SubwayMap::OnZoomSlide(wxCommandEvent &event) {
double new_zoom = 1.0 + 0.25 * zoom_slider_->GetValue();
if (new_zoom != zoom_) {
SetZoom(new_zoom, {GetSize().GetWidth() / 2, GetSize().GetHeight() / 2});
}
}
void SubwayMap::OnClickHelp(wxCommandEvent &event) {
wxMessageBox(
"Zoom in/out using the mouse wheel, Ctrl +/-, or the slider in the "
"corner.\nClick on a side of the screen to start panning. It will follow "
"your mouse. Click again to stop.\nHover over a door to see the "
"requirements to open it.\nHover over a warp or active painting to see "
"what it is connected to.\nIn painting shuffle, paintings that have not "
"yet been checked will not show their connections.\nA green shaded owl "
"means that there is a painting entrance there.\nA red shaded owl means "
"that there are only painting exits there.\nClick on a door or "
"warp to make the popup stick until you click again.",
"Subway Map Help");
}
void SubwayMap::Redraw() {
rendered_ = wxBitmap(map_image_);
wxMemoryDC dc;
dc.SelectObject(rendered_);
wxGCDC gcdc(dc);
for (const SubwayItem &subway_item : GD_GetSubwayItems()) {
ItemDrawType draw_type = ItemDrawType::kNone;
const wxBrush *brush_color = wxGREY_BRUSH;
std::optional<wxColour> shade_color;
if (AP_HasEarlyColorHallways() &&
subway_item.special == "starting_room_paintings") {
draw_type = ItemDrawType::kOwl;
shade_color = wxColour(0, 255, 0, 128);
} else if (subway_item.special == "sun_painting") {
if (!AP_IsPilgrimageEnabled()) {
if (IsDoorOpen(*subway_item.door)) {
draw_type = ItemDrawType::kOwl;
shade_color = wxColour(0, 255, 0, 128);
} else {
draw_type = ItemDrawType::kBox;
brush_color = wxRED_BRUSH;
}
}
} else if (!subway_item.paintings.empty()) {
if (AP_IsPaintingShuffle()) {
bool has_checked_painting = false;
bool has_unchecked_painting = false;
bool has_mapped_painting = false;
bool has_codomain_painting = false;
for (const std::string &painting_id : subway_item.paintings) {
if (checked_paintings_.count(painting_id)) {
has_checked_painting = true;
if (AP_GetPaintingMapping().count(painting_id)) {
has_mapped_painting = true;
} else if (AP_IsPaintingMappedTo(painting_id)) {
has_codomain_painting = true;
}
} else {
has_unchecked_painting = true;
}
}
if (has_unchecked_painting || has_mapped_painting ||
has_codomain_painting) {
draw_type = ItemDrawType::kOwl;
if (has_checked_painting) {
if (has_mapped_painting) {
shade_color = wxColour(0, 255, 0, 128);
} else {
shade_color = wxColour(255, 0, 0, 128);
}
}
}
} else if (!subway_item.tags.empty()) {
draw_type = ItemDrawType::kOwl;
}
} else if (subway_item.door) {
draw_type = ItemDrawType::kBox;
if (IsDoorOpen(*subway_item.door)) {
brush_color = wxGREEN_BRUSH;
} else {
brush_color = wxRED_BRUSH;
}
}
wxPoint real_area_pos = {subway_item.x, subway_item.y};
int real_area_size =
(draw_type == ItemDrawType::kOwl ? OWL_ACTUAL_SIZE : AREA_ACTUAL_SIZE);
if (draw_type == ItemDrawType::kBox) {
gcdc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 1));
gcdc.SetBrush(*brush_color);
gcdc.DrawRectangle(real_area_pos, {real_area_size, real_area_size});
} else if (draw_type == ItemDrawType::kOwl) {
wxBitmap owl_bitmap = wxBitmap(owl_image_.Scale(
real_area_size, real_area_size, wxIMAGE_QUALITY_BILINEAR));
gcdc.DrawBitmap(owl_bitmap, real_area_pos);
if (shade_color) {
gcdc.SetBrush(wxBrush(*shade_color));
gcdc.DrawRectangle(real_area_pos, {real_area_size, real_area_size});
}
}
}
}
void SubwayMap::SetUpHelpButton() {
help_button_->SetPosition({
GetSize().GetWidth() - help_button_->GetSize().GetWidth() - 15,
15,
});
}
void SubwayMap::EvaluateScroll(wxPoint pos) {
int scroll_x;
int scroll_y;
if (pos.x < GetSize().GetWidth() / 9) {
scroll_x = 20;
} else if (pos.x < GetSize().GetWidth() / 6) {
scroll_x = 5;
} else if (pos.x > 8 * GetSize().GetWidth() / 9) {
scroll_x = -20;
} else if (pos.x > 5 * GetSize().GetWidth() / 6) {
scroll_x = -5;
} else {
scroll_x = 0;
}
if (pos.y < GetSize().GetHeight() / 9) {
scroll_y = 20;
} else if (pos.y < GetSize().GetHeight() / 6) {
scroll_y = 5;
} else if (pos.y > 8 * GetSize().GetHeight() / 9) {
scroll_y = -20;
} else if (pos.y > 5 * GetSize().GetHeight() / 6) {
scroll_y = -5;
} else {
scroll_y = 0;
}
SetScrollSpeed(scroll_x, scroll_y);
}
wxPoint SubwayMap::MapPosToRenderPos(wxPoint pos) const {
return {static_cast<int>(pos.x * render_width_ * zoom_ /
map_image_.GetSize().GetWidth() +
zoom_x_),
static_cast<int>(pos.y * render_width_ * zoom_ /
map_image_.GetSize().GetWidth() +
zoom_y_)};
}
wxPoint SubwayMap::MapPosToVirtualPos(wxPoint pos) const {
return {static_cast<int>(pos.x * render_width_ * zoom_ /
map_image_.GetSize().GetWidth()),
static_cast<int>(pos.y * render_width_ * zoom_ /
map_image_.GetSize().GetWidth())};
}
wxPoint SubwayMap::RenderPosToMapPos(wxPoint pos) const {
return {
std::clamp(static_cast<int>((pos.x - zoom_x_) * map_image_.GetWidth() /
render_width_ / zoom_),
0, map_image_.GetWidth() - 1),
std::clamp(static_cast<int>((pos.y - zoom_y_) * map_image_.GetWidth() /
render_width_ / zoom_),
0, map_image_.GetHeight() - 1)};
}
void SubwayMap::SetZoomPos(wxPoint pos) {
if (render_width_ * zoom_ <= GetSize().GetWidth()) {
zoom_x_ = (GetSize().GetWidth() - render_width_ * zoom_) / 2;
} else {
zoom_x_ = std::clamp(
pos.x, GetSize().GetWidth() - static_cast<int>(render_width_ * zoom_),
0);
}
if (render_height_ * zoom_ <= GetSize().GetHeight()) {
zoom_y_ = (GetSize().GetHeight() - render_height_ * zoom_) / 2;
} else {
zoom_y_ = std::clamp(
pos.y, GetSize().GetHeight() - static_cast<int>(render_height_ * zoom_),
0);
}
}
void SubwayMap::SetScrollSpeed(int scroll_x, int scroll_y) {
bool should_timer = (scroll_x != 0 || scroll_y != 0);
if (should_timer != scroll_timer_->IsRunning()) {
if (should_timer) {
scroll_timer_->Start(1000 / 60);
} else {
scroll_timer_->Stop();
}
}
scroll_x_ = scroll_x;
scroll_y_ = scroll_y;
}
void SubwayMap::SetZoom(double zoom, wxPoint static_point) {
wxPoint map_pos = RenderPosToMapPos(static_point);
zoom_ = zoom;
wxPoint virtual_pos = MapPosToVirtualPos(map_pos);
SetZoomPos(-(virtual_pos - static_point));
Refresh();
zoom_slider_->SetValue((zoom - 1.0) / 0.25);
}
quadtree::Box<float> SubwayMap::GetItemBox::operator()(const int &id) const {
const SubwayItem &subway_item = GD_GetSubwayItem(id);
return {static_cast<float>(subway_item.x), static_cast<float>(subway_item.y),
AREA_ACTUAL_SIZE, AREA_ACTUAL_SIZE};
}