MapWidget.cpp 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  1. /*
  2. * Copyright (c) 2023, Bastiaan van der Plaat <bastiaan.v.d.plaat@gmail.com>
  3. * Copyright (c) 2023, Jelle Raaijmakers <jelle@gmta.nl>
  4. *
  5. * SPDX-License-Identifier: BSD-2-Clause
  6. */
  7. #include "MapWidget.h"
  8. #include <AK/URL.h>
  9. #include <Applications/MapsSettings/Defaults.h>
  10. #include <LibConfig/Client.h>
  11. #include <LibDesktop/Launcher.h>
  12. #include <LibGUI/Action.h>
  13. #include <LibGUI/Application.h>
  14. #include <LibGUI/Clipboard.h>
  15. #include <LibGfx/ImageFormats/ImageDecoder.h>
  16. #include <LibProtocol/Request.h>
  17. namespace Maps {
  18. // Math helpers
  19. // https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Pseudo-code
  20. static double longitude_to_tile_x(double longitude, int zoom)
  21. {
  22. return pow(2, zoom) * ((longitude + 180.0) / 360.0);
  23. }
  24. static double latitude_to_tile_y(double latitude, int zoom)
  25. {
  26. return pow(2, zoom) * (1.0 - (log(tan(AK::to_radians(latitude)) + (1.0 / cos(AK::to_radians(latitude)))) / M_PI)) / 2.0;
  27. }
  28. static double tile_x_to_longitude(double x, int zoom)
  29. {
  30. return x / pow(2, zoom) * 360.0 - 180.0;
  31. }
  32. static double tile_y_to_latitude(double y, int zoom)
  33. {
  34. return AK::to_degrees(atan(sinh(M_PI * (1.0 - 2.0 * y / pow(2, zoom)))));
  35. }
  36. static double nice_round_number(double number)
  37. {
  38. double pow10 = pow(10, floor(log10(floor(number))));
  39. double d = number / pow10;
  40. return pow10 * (d >= 10 ? 10 : (d >= 5 ? 5 : (d >= 3 ? 3 : (d >= 2 ? 2 : 1))));
  41. }
  42. double MapWidget::LatLng::distance_to(LatLng const& other) const
  43. {
  44. return EARTH_RADIUS * 2.0 * asin(sqrt(pow(sin((AK::to_radians(other.latitude) - AK::to_radians(latitude)) / 2.0), 2.0) + cos(AK::to_radians(latitude)) * cos(AK::to_radians(other.latitude)) * pow(sin((AK::to_radians(other.longitude) - AK::to_radians(longitude)) / 2.0), 2.0)));
  45. }
  46. int MapWidget::LatLngBounds::get_zoom() const
  47. {
  48. double distance_meters = north_west.distance_to(south_east);
  49. int zoom = ZOOM_MIN;
  50. while (distance_meters < EARTH_RADIUS / pow(2, zoom - 1) && zoom != ZOOM_MAX)
  51. ++zoom;
  52. return min(zoom + 1, ZOOM_MAX);
  53. }
  54. // MapWidget class
  55. MapWidget::MapWidget(Options const& options)
  56. : m_tile_provider(options.tile_provider)
  57. , m_center(options.center)
  58. , m_zoom(options.zoom)
  59. , m_context_menu_enabled(options.context_menu_enabled)
  60. , m_scale_enabled(options.scale_enabled)
  61. , m_scale_max_width(options.scale_max_width)
  62. , m_attribution_enabled(options.attribution_enabled)
  63. {
  64. m_request_client = Protocol::RequestClient::try_create().release_value_but_fixme_should_propagate_errors();
  65. if (options.attribution_enabled) {
  66. auto attribution_text = options.attribution_text.value_or(MUST(String::from_deprecated_string(Config::read_string("Maps"sv, "MapWidget"sv, "TileProviderAttributionText"sv, Maps::default_tile_provider_attribution_text))));
  67. URL attribution_url = options.attribution_url.value_or(URL(Config::read_string("Maps"sv, "MapWidget"sv, "TileProviderAttributionUrl"sv, Maps::default_tile_provider_attribution_url)));
  68. add_panel({ attribution_text, Panel::Position::BottomRight, attribution_url, "attribution"_string });
  69. }
  70. m_marker_image = Gfx::Bitmap::load_from_file("/res/graphics/maps/marker-blue.png"sv).release_value_but_fixme_should_propagate_errors();
  71. m_default_tile_provider = MUST(String::from_deprecated_string(Config::read_string("Maps"sv, "MapWidget"sv, "TileProviderUrlFormat"sv, Maps::default_tile_provider_url_format)));
  72. }
  73. void MapWidget::set_zoom(int zoom)
  74. {
  75. m_zoom = min(max(zoom, ZOOM_MIN), ZOOM_MAX);
  76. clear_tile_queue();
  77. update();
  78. }
  79. void MapWidget::config_string_did_change(StringView domain, StringView group, StringView key, StringView value)
  80. {
  81. if (domain != "Maps" || group != "MapWidget")
  82. return;
  83. if (key == "TileProviderUrlFormat") {
  84. // When config tile provider changes clear all active requests and loaded tiles
  85. m_default_tile_provider = MUST(String::from_utf8(value));
  86. m_first_image_loaded = false;
  87. m_active_requests.clear();
  88. m_tiles.clear();
  89. update();
  90. }
  91. if (key == "TileProviderAttributionText") {
  92. // Update attribution panel text when it exists
  93. for (auto& panel : m_panels) {
  94. if (panel.name == "attribution") {
  95. panel.text = MUST(String::from_utf8(value));
  96. return;
  97. }
  98. }
  99. update();
  100. }
  101. if (key == "TileProviderAttributionUrl") {
  102. // Update attribution panel url when it exists
  103. for (auto& panel : m_panels) {
  104. if (panel.name == "attribution") {
  105. panel.url = URL(value);
  106. return;
  107. }
  108. }
  109. }
  110. }
  111. void MapWidget::doubleclick_event(GUI::MouseEvent& event)
  112. {
  113. int new_zoom = event.shift() ? m_zoom - 1 : m_zoom + 1;
  114. set_zoom_for_mouse_event(new_zoom, event);
  115. }
  116. void MapWidget::mousedown_event(GUI::MouseEvent& event)
  117. {
  118. if (m_connection_failed)
  119. return;
  120. if (event.button() == GUI::MouseButton::Primary) {
  121. // Ignore panels click
  122. for (auto& panel : m_panels)
  123. if (panel.rect.contains(event.x(), event.y()))
  124. return;
  125. // Start map tiles dragging
  126. m_dragging = true;
  127. m_last_mouse_x = event.x();
  128. m_last_mouse_y = event.y();
  129. set_override_cursor(Gfx::StandardCursor::Drag);
  130. }
  131. }
  132. void MapWidget::mousemove_event(GUI::MouseEvent& event)
  133. {
  134. if (m_connection_failed)
  135. return;
  136. if (m_dragging) {
  137. // Adjust map center by mouse delta
  138. double delta_x = event.x() - m_last_mouse_x;
  139. double delta_y = event.y() - m_last_mouse_y;
  140. set_center({ tile_y_to_latitude(latitude_to_tile_y(m_center.latitude, m_zoom) - delta_y / TILE_SIZE, m_zoom),
  141. tile_x_to_longitude(longitude_to_tile_x(m_center.longitude, m_zoom) - delta_x / TILE_SIZE, m_zoom) });
  142. m_last_mouse_x = event.x();
  143. m_last_mouse_y = event.y();
  144. return;
  145. }
  146. // Handle panels hover
  147. for (auto& panel : m_panels)
  148. if (panel.url.has_value() && panel.rect.contains(event.x(), event.y()))
  149. return set_override_cursor(Gfx::StandardCursor::Hand);
  150. set_override_cursor(Gfx::StandardCursor::Arrow);
  151. // Handle marker tooltip hover
  152. int center_tile_x = longitude_to_tile_x(m_center.longitude, m_zoom);
  153. int center_tile_y = latitude_to_tile_y(m_center.latitude, m_zoom);
  154. double offset_x = (longitude_to_tile_x(m_center.longitude, m_zoom) - center_tile_x) * TILE_SIZE;
  155. double offset_y = (latitude_to_tile_y(m_center.latitude, m_zoom) - center_tile_y) * TILE_SIZE;
  156. for (auto const& marker : m_markers) {
  157. if (!marker.tooltip.has_value())
  158. continue;
  159. RefPtr<Gfx::Bitmap> marker_image = marker.image ? marker.image : m_marker_image;
  160. Gfx::IntRect marker_rect = {
  161. static_cast<int>(width() / 2 + (longitude_to_tile_x(marker.latlng.longitude, m_zoom) - center_tile_x) * TILE_SIZE - offset_x) - marker_image->width() / 2,
  162. static_cast<int>(height() / 2 + (latitude_to_tile_y(marker.latlng.latitude, m_zoom) - center_tile_y) * TILE_SIZE - offset_y) - marker_image->height(),
  163. marker_image->width(),
  164. marker_image->height()
  165. };
  166. if (marker_rect.contains(event.x(), event.y())) {
  167. GUI::Application::the()->show_tooltip(marker.tooltip.value().to_deprecated_string(), this);
  168. return;
  169. }
  170. }
  171. GUI::Application::the()->hide_tooltip();
  172. }
  173. void MapWidget::mouseup_event(GUI::MouseEvent& event)
  174. {
  175. if (m_connection_failed)
  176. return;
  177. // Stop map tiles dragging
  178. if (m_dragging) {
  179. m_dragging = false;
  180. set_override_cursor(Gfx::StandardCursor::Arrow);
  181. return;
  182. }
  183. if (event.button() == GUI::MouseButton::Primary) {
  184. // Handle panels click
  185. for (auto& panel : m_panels) {
  186. if (panel.url.has_value() && panel.rect.contains(event.x(), event.y())) {
  187. Desktop::Launcher::open(panel.url.value());
  188. return;
  189. }
  190. }
  191. }
  192. }
  193. void MapWidget::mousewheel_event(GUI::MouseEvent& event)
  194. {
  195. if (m_connection_failed)
  196. return;
  197. int new_zoom = event.wheel_delta_y() > 0 ? m_zoom - 1 : m_zoom + 1;
  198. set_zoom_for_mouse_event(new_zoom, event);
  199. }
  200. void MapWidget::context_menu_event(GUI::ContextMenuEvent& event)
  201. {
  202. if (!m_context_menu_enabled)
  203. return;
  204. LatLng latlng = { tile_y_to_latitude(latitude_to_tile_y(m_center.latitude, m_zoom) + static_cast<double>(event.position().y() - height() / 2) / TILE_SIZE, m_zoom),
  205. tile_x_to_longitude(longitude_to_tile_x(m_center.longitude, m_zoom) + static_cast<double>(event.position().x() - width() / 2) / TILE_SIZE, m_zoom) };
  206. m_context_menu = GUI::Menu::construct();
  207. m_context_menu->add_action(GUI::Action::create(
  208. "&Copy Coordinates to Clipboard", MUST(Gfx::Bitmap::load_from_file("/res/icons/16x16/edit-copy.png"sv)), [latlng](auto&) {
  209. GUI::Clipboard::the().set_plain_text(MUST(String::formatted("{}, {}", latlng.latitude, latlng.longitude)).bytes_as_string_view());
  210. }));
  211. m_context_menu->add_separator();
  212. auto link_icon = MUST(Gfx::Bitmap::load_from_file("/res/icons/16x16/filetype-symlink.png"sv));
  213. m_context_menu->add_action(GUI::Action::create(
  214. "Open in &OpenStreetMap", link_icon, [this, latlng](auto&) {
  215. Desktop::Launcher::open(URL(MUST(String::formatted("https://www.openstreetmap.org/#map={}/{}/{}", m_zoom, latlng.latitude, latlng.longitude))));
  216. }));
  217. m_context_menu->add_action(GUI::Action::create(
  218. "Open in &Google Maps", link_icon, [this, latlng](auto&) {
  219. Desktop::Launcher::open(URL(MUST(String::formatted("https://www.google.com/maps/@{},{},{}z", latlng.latitude, latlng.longitude, m_zoom))));
  220. }));
  221. m_context_menu->add_action(GUI::Action::create(
  222. "Open in &Bing Maps", link_icon, [this, latlng](auto&) {
  223. Desktop::Launcher::open(URL(MUST(String::formatted("https://www.bing.com/maps/?cp={}~{}&lvl={}", latlng.latitude, latlng.longitude, m_zoom))));
  224. }));
  225. m_context_menu->add_action(GUI::Action::create(
  226. "Open in &DuckDuckGo Maps", link_icon, [latlng](auto&) {
  227. Desktop::Launcher::open(URL(MUST(String::formatted("https://duckduckgo.com/?q={},+{}&ia=web&iaxm=maps", latlng.latitude, latlng.longitude))));
  228. }));
  229. m_context_menu->add_separator();
  230. m_context_menu->add_action(GUI::Action::create(
  231. "Center &map here", MUST(Gfx::Bitmap::load_from_file("/res/icons/16x16/scale.png"sv)), [this, latlng](auto&) { set_center(latlng); }));
  232. m_context_menu->popup(event.screen_position());
  233. }
  234. void MapWidget::set_zoom_for_mouse_event(int zoom, GUI::MouseEvent& event)
  235. {
  236. if (zoom == m_zoom || zoom < ZOOM_MIN || zoom > ZOOM_MAX)
  237. return;
  238. if (zoom < m_zoom) {
  239. set_center({ tile_y_to_latitude(latitude_to_tile_y(m_center.latitude, m_zoom) - static_cast<double>(event.y() - height() / 2) / TILE_SIZE, m_zoom),
  240. tile_x_to_longitude(longitude_to_tile_x(m_center.longitude, m_zoom) - static_cast<double>(event.x() - width() / 2) / TILE_SIZE, m_zoom) });
  241. } else {
  242. set_center({ tile_y_to_latitude(latitude_to_tile_y(m_center.latitude, zoom) + static_cast<double>(event.y() - height() / 2) / TILE_SIZE, zoom),
  243. tile_x_to_longitude(longitude_to_tile_x(m_center.longitude, zoom) + static_cast<double>(event.x() - width() / 2) / TILE_SIZE, zoom) });
  244. }
  245. set_zoom(zoom);
  246. }
  247. Optional<RefPtr<Gfx::Bitmap>> MapWidget::get_tile_image(int x, int y, int zoom, TileDownloadBehavior download_behavior)
  248. {
  249. // Get the tile from tiles cache
  250. TileKey const key = { x, y, zoom };
  251. if (auto it = m_tiles.find(key); it != m_tiles.end()) {
  252. if (it->value)
  253. return it->value;
  254. return {};
  255. }
  256. if (download_behavior == TileDownloadBehavior::DoNotDownload)
  257. return {};
  258. // Register an empty tile so we don't send requests multiple times
  259. if (m_tiles.size() >= TILES_CACHE_MAX)
  260. m_tiles.remove(m_tiles.begin());
  261. m_tiles.set(key, nullptr);
  262. // Schedule the tile download
  263. m_tile_queue.enqueue(key);
  264. process_tile_queue();
  265. return {};
  266. }
  267. void MapWidget::process_tile_queue()
  268. {
  269. if (m_active_requests.size() >= TILES_DOWNLOAD_PARALLEL_MAX)
  270. return;
  271. if (m_tile_queue.is_empty())
  272. return;
  273. auto tile_key = m_tile_queue.dequeue();
  274. // Start HTTP GET request to load image
  275. HashMap<DeprecatedString, DeprecatedString> headers;
  276. headers.set("User-Agent", "SerenityOS Maps");
  277. headers.set("Accept", "image/png");
  278. URL url(MUST(String::formatted(m_tile_provider.value_or(m_default_tile_provider), tile_key.zoom, tile_key.x, tile_key.y)));
  279. auto request = m_request_client->start_request("GET", url, headers, {});
  280. VERIFY(!request.is_null());
  281. m_active_requests.append(request);
  282. request->on_buffered_request_finish = [this, request, url, tile_key](bool success, auto, auto&, auto, ReadonlyBytes payload) {
  283. auto was_active = m_active_requests.remove_first_matching([request](auto const& other_request) { return other_request->id() == request->id(); });
  284. if (!was_active)
  285. return;
  286. deferred_invoke([this]() { this->process_tile_queue(); });
  287. // When first image load fails set connection failed
  288. if (!success) {
  289. if (!m_first_image_loaded) {
  290. m_first_image_loaded = true;
  291. m_connection_failed = true;
  292. }
  293. dbgln("Maps: Can't load image: {}", url);
  294. return;
  295. }
  296. m_first_image_loaded = true;
  297. // Decode loaded PNG image data
  298. auto decoder = Gfx::ImageDecoder::try_create_for_raw_bytes(payload, "image/png");
  299. if (!decoder || (decoder->frame_count() == 0)) {
  300. dbgln("Maps: Can't decode image: {}", url);
  301. return;
  302. }
  303. m_tiles.set(tile_key, decoder->frame(0).release_value_but_fixme_should_propagate_errors().image);
  304. // FIXME: only update the part of the screen that this tile covers
  305. update();
  306. };
  307. request->set_should_buffer_all_input(true);
  308. request->on_certificate_requested = []() -> Protocol::Request::CertificateAndKey { return {}; };
  309. }
  310. void MapWidget::clear_tile_queue()
  311. {
  312. m_tile_queue.clear();
  313. // FIXME: ideally we would like to abort all active requests here, but invoking `->stop()`
  314. // often causes hangs for me for some reason.
  315. m_active_requests.clear_with_capacity();
  316. m_tiles.remove_all_matching([](auto, auto const& value) -> bool { return !value; });
  317. }
  318. void MapWidget::paint_map(GUI::Painter& painter)
  319. {
  320. int center_tile_x = longitude_to_tile_x(m_center.longitude, m_zoom);
  321. int center_tile_y = latitude_to_tile_y(m_center.latitude, m_zoom);
  322. double offset_x = (longitude_to_tile_x(m_center.longitude, m_zoom) - center_tile_x) * TILE_SIZE;
  323. double offset_y = (latitude_to_tile_y(m_center.latitude, m_zoom) - center_tile_y) * TILE_SIZE;
  324. // Draw grid around center tile
  325. int grid_width = (width() + TILE_SIZE - 1) / TILE_SIZE;
  326. int grid_height = (height() + TILE_SIZE - 1) / TILE_SIZE;
  327. for (int dy = -(grid_height / 2) - 1; dy < ((grid_height + 2 - 1) / 2) + 1; ++dy) {
  328. for (int dx = -(grid_width / 2) - 1; dx < ((grid_width + 2 - 1) / 2) + 1; ++dx) {
  329. int tile_x = center_tile_x + dx;
  330. int tile_y = center_tile_y + dy;
  331. // Only draw tiles that exist
  332. if (tile_x < 0 || tile_y < 0 || tile_x > pow(2, m_zoom) - 1 || tile_y > pow(2, m_zoom) - 1)
  333. continue;
  334. auto tile_rect = Gfx::IntRect {
  335. static_cast<int>(width() / 2 + dx * TILE_SIZE - offset_x),
  336. static_cast<int>(height() / 2 + dy * TILE_SIZE - offset_y),
  337. TILE_SIZE,
  338. TILE_SIZE,
  339. };
  340. if (!tile_rect.intersects(frame_inner_rect()))
  341. continue;
  342. // Get tile, when it has a loaded image draw it at the right position
  343. auto tile_image = get_tile_image(tile_x, tile_y, m_zoom, TileDownloadBehavior::Download);
  344. auto const tile_source = Gfx::IntRect { 0, 0, TILE_SIZE, TILE_SIZE };
  345. if (tile_image.has_value()) {
  346. painter.blit(tile_rect.location(), *tile_image.release_value(), tile_source, 1);
  347. continue;
  348. }
  349. // Fallback: try to compose the tile from already cached tiles from a higher zoom level
  350. auto cached_tiles_used = 0;
  351. if (m_zoom < ZOOM_MAX) {
  352. auto const child_top_left_tile_x = tile_x * 2;
  353. auto const child_top_left_tile_y = tile_y * 2;
  354. for (auto child_tile_x = child_top_left_tile_x; child_tile_x <= child_top_left_tile_x + 1; ++child_tile_x) {
  355. for (auto child_tile_y = child_top_left_tile_y; child_tile_y <= child_top_left_tile_y + 1; ++child_tile_y) {
  356. auto child_tile = get_tile_image(child_tile_x, child_tile_y, m_zoom + 1, TileDownloadBehavior::DoNotDownload);
  357. if (!child_tile.has_value())
  358. continue;
  359. auto target_rect = tile_rect;
  360. target_rect.set_size(TILE_SIZE / 2, TILE_SIZE / 2);
  361. if ((child_tile_x & 1) > 0)
  362. target_rect.translate_by(TILE_SIZE / 2, 0);
  363. if ((child_tile_y & 1) > 0)
  364. target_rect.translate_by(0, TILE_SIZE / 2);
  365. painter.draw_scaled_bitmap(target_rect, *child_tile.release_value(), tile_source, 1.f, Gfx::Painter::ScalingMode::BoxSampling);
  366. ++cached_tiles_used;
  367. }
  368. }
  369. }
  370. // Fallback: try to use an already cached tile from a lower zoom level
  371. // Note: we only want to try this if we did not find exactly 4 cached child tiles in the previous fallback (i.e. there are gaps)
  372. if (m_zoom > ZOOM_MIN && cached_tiles_used < 4) {
  373. auto const parent_tile_x = tile_x / 2;
  374. auto const parent_tile_y = tile_y / 2;
  375. auto larger_tile = get_tile_image(parent_tile_x, parent_tile_y, m_zoom - 1, TileDownloadBehavior::DoNotDownload);
  376. if (larger_tile.has_value()) {
  377. auto source_rect = Gfx::IntRect { 0, 0, TILE_SIZE / 2, TILE_SIZE / 2 };
  378. if ((tile_x & 1) > 0)
  379. source_rect.translate_by(TILE_SIZE / 2, 0);
  380. if ((tile_y & 1) > 0)
  381. source_rect.translate_by(0, TILE_SIZE / 2);
  382. painter.draw_scaled_bitmap(tile_rect, *larger_tile.release_value(), source_rect, 1.f, Gfx::Painter::ScalingMode::BilinearBlend);
  383. }
  384. }
  385. }
  386. }
  387. // Draw markers
  388. for (auto const& marker : m_markers) {
  389. RefPtr<Gfx::Bitmap> marker_image = marker.image ? marker.image : m_marker_image;
  390. Gfx::IntRect marker_rect = {
  391. static_cast<int>(width() / 2 + (longitude_to_tile_x(marker.latlng.longitude, m_zoom) - center_tile_x) * TILE_SIZE - offset_x) - marker_image->width() / 2,
  392. static_cast<int>(height() / 2 + (latitude_to_tile_y(marker.latlng.latitude, m_zoom) - center_tile_y) * TILE_SIZE - offset_y) - marker_image->height(),
  393. marker_image->width(),
  394. marker_image->height()
  395. };
  396. if (marker_rect.intersects(frame_inner_rect()))
  397. painter.blit(marker_rect.location(), *marker_image, { 0, 0, marker_image->width(), marker_image->height() }, 1);
  398. }
  399. }
  400. void MapWidget::paint_scale_line(GUI::Painter& painter, String label, Gfx::IntRect rect)
  401. {
  402. painter.fill_rect(rect, panel_background_color);
  403. painter.fill_rect({ rect.x(), rect.y(), 1, rect.height() }, panel_foreground_color);
  404. painter.fill_rect({ rect.x() + rect.width() - 1, rect.y(), 1, rect.height() }, panel_foreground_color);
  405. Gfx::FloatRect label_rect { rect.x() + PANEL_PADDING_X, rect.y() + PANEL_PADDING_Y, rect.width() - PANEL_PADDING_X * 2, rect.height() - PANEL_PADDING_Y * 2 };
  406. painter.draw_text(label_rect, label, Gfx::TextAlignment::TopLeft, panel_foreground_color);
  407. }
  408. void MapWidget::paint_scale(GUI::Painter& painter)
  409. {
  410. double max_meters = m_center.distance_to({ m_center.latitude, tile_x_to_longitude(longitude_to_tile_x(m_center.longitude, m_zoom) + static_cast<double>(m_scale_max_width) / TILE_SIZE, m_zoom) });
  411. float margin_x = 8;
  412. float margin_y = 8;
  413. float line_height = PANEL_PADDING_Y + painter.font().pixel_size() + PANEL_PADDING_Y;
  414. // Metric line
  415. double meters = nice_round_number(max_meters);
  416. float metric_width = m_scale_max_width * (meters / max_meters);
  417. Gfx::IntRect metric_rect = { frame_inner_rect().x() + margin_x, frame_inner_rect().bottom() - margin_y - line_height * 2, metric_width, line_height };
  418. if (meters < 1000) {
  419. paint_scale_line(painter, MUST(String::formatted("{} m", meters)), metric_rect);
  420. } else {
  421. paint_scale_line(painter, MUST(String::formatted("{} km", meters / 1000)), metric_rect);
  422. }
  423. // Imperial line
  424. double max_feet = max_meters * 3.28084;
  425. double feet = nice_round_number(max_feet);
  426. double max_miles = max_feet / 5280;
  427. double miles = nice_round_number(max_miles);
  428. float imperial_width = m_scale_max_width * (feet < 5280 ? feet / max_feet : miles / max_miles);
  429. Gfx::IntRect imperial_rect = { frame_inner_rect().x() + margin_x, frame_inner_rect().bottom() - margin_y - line_height, imperial_width, line_height };
  430. if (feet < 5280) {
  431. paint_scale_line(painter, MUST(String::formatted("{} ft", feet)), imperial_rect);
  432. } else {
  433. paint_scale_line(painter, MUST(String::formatted("{} mi", miles)), imperial_rect);
  434. }
  435. // Border between
  436. painter.fill_rect({ frame_inner_rect().x() + margin_x, frame_inner_rect().bottom() - margin_y - line_height, max(metric_width, imperial_width), 1.0f }, panel_foreground_color);
  437. }
  438. void MapWidget::paint_panels(GUI::Painter& painter)
  439. {
  440. for (auto& panel : m_panels) {
  441. int panel_width = PANEL_PADDING_X + painter.font().width(panel.text) + PANEL_PADDING_X;
  442. int panel_height = PANEL_PADDING_Y + painter.font().pixel_size() + PANEL_PADDING_Y;
  443. if (panel.position == Panel::Position::TopLeft)
  444. panel.rect = { frame_inner_rect().x(), frame_inner_rect().y(), panel_width, panel_height };
  445. if (panel.position == Panel::Position::TopRight)
  446. panel.rect = { frame_inner_rect().right() - panel_width, frame_inner_rect().y(), panel_width, panel_height };
  447. if (panel.position == Panel::Position::BottomLeft)
  448. panel.rect = { frame_inner_rect().x(), frame_inner_rect().bottom() - panel_height, panel_width, panel_height };
  449. if (panel.position == Panel::Position::BottomRight)
  450. panel.rect = { frame_inner_rect().right() - panel_width, frame_inner_rect().bottom() - panel_height, panel_width, panel_height };
  451. painter.fill_rect(panel.rect, panel_background_color);
  452. Gfx::FloatRect text_rect = { panel.rect.x() + PANEL_PADDING_X, panel.rect.y() + PANEL_PADDING_Y, panel.rect.width(), panel.rect.height() };
  453. painter.draw_text(text_rect, panel.text, Gfx::TextAlignment::TopLeft, panel_foreground_color);
  454. }
  455. }
  456. void MapWidget::paint_event(GUI::PaintEvent& event)
  457. {
  458. Frame::paint_event(event);
  459. GUI::Painter painter(*this);
  460. painter.add_clip_rect(event.rect());
  461. painter.add_clip_rect(frame_inner_rect());
  462. painter.fill_rect(frame_inner_rect(), map_background_color);
  463. if (m_connection_failed)
  464. return painter.draw_text(frame_inner_rect(), "Failed to fetch map tiles :^("sv, Gfx::TextAlignment::Center, panel_foreground_color);
  465. paint_map(painter);
  466. if (m_scale_enabled)
  467. paint_scale(painter);
  468. paint_panels(painter);
  469. }
  470. }