diff --git a/Base/res/graphics/maps/marker-gray.png b/Base/res/graphics/maps/marker-gray.png new file mode 100644 index 00000000000..5aee1bc8cba Binary files /dev/null and b/Base/res/graphics/maps/marker-gray.png differ diff --git a/Userland/Applications/Maps/CMakeLists.txt b/Userland/Applications/Maps/CMakeLists.txt index a302797ee00..5e3633bc581 100644 --- a/Userland/Applications/Maps/CMakeLists.txt +++ b/Userland/Applications/Maps/CMakeLists.txt @@ -7,6 +7,7 @@ serenity_component( set(SOURCES main.cpp MapWidget.cpp + UsersMapWidget.cpp ) serenity_app(Maps ICON app-maps) diff --git a/Userland/Applications/Maps/UsersMapWidget.cpp b/Userland/Applications/Maps/UsersMapWidget.cpp new file mode 100644 index 00000000000..6363e1e1df6 --- /dev/null +++ b/Userland/Applications/Maps/UsersMapWidget.cpp @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2023, Bastiaan van der Plaat + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "UsersMapWidget.h" +#include +#include + +UsersMapWidget::UsersMapWidget(Options const& options) + : MapWidget::MapWidget(options) +{ + m_marker_gray_image = Gfx::Bitmap::load_from_file("/res/graphics/maps/marker-gray.png"sv).release_value_but_fixme_should_propagate_errors(); +} + +void UsersMapWidget::get_users() +{ + // Start HTTP GET request to load people.json + HashMap headers; + headers.set("User-Agent", "SerenityOS Maps"); + headers.set("Accept", "application/json"); + URL url("https://usermap.serenityos.org/people.json"); + auto request = request_client()->start_request("GET", url, headers, {}); + VERIFY(!request.is_null()); + m_request = request; + request->on_buffered_request_finish = [this, request, url](bool success, auto, auto&, auto, ReadonlyBytes payload) { + m_request.clear(); + if (!success) { + dbgln("Maps: Can't load: {}", url); + return; + } + + // Parse JSON data + JsonParser parser(payload); + auto result = parser.parse(); + if (result.is_error()) { + dbgln("Maps: Can't parse JSON: {}", url); + return; + } + + // Parse each user + // FIXME: Handle JSON parsing errors + m_users = Vector(); + auto json_users = result.release_value().as_array(); + for (size_t i = 0; i < json_users.size(); i++) { + auto const& json_user = json_users.at(i).as_object(); + User user { + MUST(String::from_deprecated_string(json_user.get_deprecated_string("nick"sv).release_value())), + { json_user.get_array("coordinates"sv).release_value().at(0).to_double(), + json_user.get_array("coordinates"sv).release_value().at(1).to_double() }, + json_user.has_bool("contributor"sv), + }; + m_users.value().append(user); + } + add_users_to_map(); + }; + request->set_should_buffer_all_input(true); + request->on_certificate_requested = []() -> Protocol::Request::CertificateAndKey { return {}; }; +} + +void UsersMapWidget::add_users_to_map() +{ + if (!m_users.has_value()) + return; + + for (auto const& user : m_users.value()) { + MapWidget::Marker marker = { user.coordinates, user.nick }; + if (!user.contributor) + marker.image = m_marker_gray_image; + add_marker(marker); + } + + add_panel({ MUST(String::formatted("{} users are already registered", m_users.value().size())), + Panel::Position::TopRight, + { { "https://github.com/SerenityOS/user-map" } } }); +} diff --git a/Userland/Applications/Maps/UsersMapWidget.h b/Userland/Applications/Maps/UsersMapWidget.h new file mode 100644 index 00000000000..876361a6165 --- /dev/null +++ b/Userland/Applications/Maps/UsersMapWidget.h @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2023, Bastiaan van der Plaat + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include "MapWidget.h" + +class UsersMapWidget final : public MapWidget { + C_OBJECT(UsersMapWidget); + +public: + bool show_users() const { return m_show_users; } + void set_show_users(bool show_users) + { + m_show_users = show_users; + if (m_show_users) { + if (!m_users.has_value()) { + get_users(); + } else { + add_users_to_map(); + } + } else { + clear_markers(); + clear_panels(); + } + } + +private: + UsersMapWidget(Options const&); + + void get_users(); + + void add_users_to_map(); + + RefPtr m_marker_gray_image; + RefPtr m_request; + bool m_show_users { false }; + + struct User { + String nick; + LatLng coordinates; + bool contributor; + }; + Optional> m_users; +}; diff --git a/Userland/Applications/Maps/main.cpp b/Userland/Applications/Maps/main.cpp index 564f6741423..1161f1cbfe4 100644 --- a/Userland/Applications/Maps/main.cpp +++ b/Userland/Applications/Maps/main.cpp @@ -4,7 +4,7 @@ * SPDX-License-Identifier: BSD-2-Clause */ -#include "MapWidget.h" +#include "UsersMapWidget.h" #include #include #include @@ -45,18 +45,22 @@ ErrorOr serenity_main(Main::Arguments arguments) auto toolbar = TRY(toolbar_container->try_add()); // Map widget - MapWidget::Options options {}; + UsersMapWidget::Options options {}; options.center.latitude = Config::read_string("Maps"sv, "MapView"sv, "CenterLatitude"sv, "30"sv).to_double().value_or(30.0); options.center.longitude = Config::read_string("Maps"sv, "MapView"sv, "CenterLongitude"sv, "0"sv).to_double().value_or(0.0); options.zoom = Config::read_i32("Maps"sv, "MapView"sv, "Zoom"sv, MAP_ZOOM_DEFAULT); - auto maps = TRY(root_widget->try_add(options)); + auto maps = TRY(root_widget->try_add(options)); maps->set_frame_style(Gfx::FrameStyle::SunkenContainer); + maps->set_show_users(Config::read_bool("Maps"sv, "MapView"sv, "ShowUsers"sv, false)); // Main menu actions auto file_menu = window->add_menu("&File"_string); file_menu->add_action(GUI::CommonActions::make_quit_action([](auto&) { GUI::Application::the()->quit(); })); auto view_menu = window->add_menu("&View"_string); + auto show_users_action = GUI::Action::create_checkable( + "Show SerenityOS users", TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/ladyball.png"sv)), [maps](auto& action) { maps->set_show_users(action.is_checked()); }, window); + show_users_action->set_checked(maps->show_users()); auto zoom_in_action = GUI::CommonActions::make_zoom_in_action([maps](auto&) { maps->set_zoom(maps->zoom() + 1); }, window); auto zoom_out_action = GUI::CommonActions::make_zoom_out_action([maps](auto&) { maps->set_zoom(maps->zoom() - 1); }, window); auto reset_zoom_action = GUI::CommonActions::make_reset_zoom_action([maps](auto&) { maps->set_zoom(MAP_ZOOM_DEFAULT); }, window); @@ -66,6 +70,8 @@ ErrorOr serenity_main(Main::Arguments arguments) maps->set_frame_style(window->is_fullscreen() ? Gfx::FrameStyle::NoFrame : Gfx::FrameStyle::SunkenContainer); }, window); + view_menu->add_action(show_users_action); + view_menu->add_separator(); view_menu->add_action(zoom_in_action); view_menu->add_action(zoom_out_action); view_menu->add_action(reset_zoom_action); @@ -77,6 +83,8 @@ ErrorOr serenity_main(Main::Arguments arguments) help_menu->add_action(GUI::CommonActions::make_about_action("Maps", app_icon, window)); // Main toolbar actions + toolbar->add_action(show_users_action); + toolbar->add_separator(); toolbar->add_action(zoom_in_action); toolbar->add_action(zoom_out_action); toolbar->add_action(reset_zoom_action); @@ -88,5 +96,6 @@ ErrorOr serenity_main(Main::Arguments arguments) Config::write_string("Maps"sv, "MapView"sv, "CenterLatitude"sv, TRY(String::number(maps->center().latitude))); Config::write_string("Maps"sv, "MapView"sv, "CenterLongitude"sv, TRY(String::number(maps->center().longitude))); Config::write_i32("Maps"sv, "MapView"sv, "Zoom"sv, maps->zoom()); + Config::write_bool("Maps"sv, "MapView"sv, "ShowUsers"sv, maps->show_users()); return exec; }