From 4ed6464d22badcff66d1e19f0b369183aea8f1b2 Mon Sep 17 00:00:00 2001 From: Zoriot Date: Sun, 24 May 2026 16:41:13 +0200 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=8C=8D=20feat(navigation):=20add=20lo?= =?UTF-8?q?cal=20reverse-geocode=20database=20with=20auto-download=20and?= =?UTF-8?q?=20thread-safety=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add optional offline country/region lookup using alpslib-geo RgcHandler - Automatically download and cache RGC database on first run from configured URL - Fall back to online Photon/Komoot API if local database disabled or unavailable - Mark rgcHandler field as volatile to guarantee cross-thread visibility - Mark Module.enabled as volatile to ensure reliable isEnabled() checks across threads - Schedule RgcHandler creation and all method calls on main thread to prevent concurrent access - Create parent directories before FileOutputStream to handle first-run download - Use try-with-resources for download streams to prevent resource leaks - Schedule RGC lookups on main thread when called from worker threads in OpenStreetMapAPI - Add getCCA2FromCountryName() fallback helper for country code lookup - Add RGC configuration options: enabled, url, path under reverse-geocode.local-database - Handle download failures gracefully with logging and fallback to API Dependencies: - Add com.alpsbte.alpslib:alpslib-geo:1.0.0 Files: - build.gradle.kts: add alpslib-geo dependency - gradle/libs.versions.toml: define alpslib-geo version - settings.gradle.kts: enable mavenLocal() (TEMPORARY - disable before merge) - NavigationModule.java: add RGC initialization with auto-download - Module.java: mark enabled field volatile - OpenStreetMapAPI.java: schedule RGC lookups on main thread - NavUtils.java: add getCCA2FromCountryName() helper - ConfigPaths.java: add RGC_LOCAL_DB_* constants - WarpsComponent.java, WarpEditMenu.java: use fallback country code lookup - config.yml: document RGC local-database settings --- build.gradle.kts | 1 + gradle/libs.versions.toml | 2 + settings.gradle.kts | 2 +- .../modules/navigation/NavUtils.java | 16 ++++++ .../modules/navigation/NavigationModule.java | 51 +++++++++++++++++-- .../components/warps/WarpsComponent.java | 2 + .../components/warps/menu/WarpEditMenu.java | 4 ++ .../modules/network/api/OpenStreetMapAPI.java | 36 +++++++++++++ .../buildteamtools/utils/io/ConfigPaths.java | 6 +++ .../buildteamtools/utils/io/ConfigUtil.java | 2 +- .../resources/modules/navigation/config.yml | 13 ++++- 11 files changed, 129 insertions(+), 6 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 760775df..ee90e563 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -11,6 +11,7 @@ dependencies { implementation(libs.alpslib.utils) { exclude(group = "com.github.cryptomorin", module = "XSeries") } + implementation(libs.alpslib.geo) implementation(libs.com.alpsbte.canvas) implementation(libs.com.github.cryptomorin.xseries) implementation(libs.net.wesjd.anvilgui) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0ccedcb9..e730b6cb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,6 +22,7 @@ fawe-bom = "1.56" # Ref: https://github.com/IntellectualSites/bom bstats = "3.+" # https://central.sonatype.com/artifact/org.bstats/bstats-bukkit bluemap-api = "2.+" # Ref: https://github.com/BlueMap-Minecraft/BlueMapAPI net-buildtheearth-projection = "1.+" # Ref: https://github.com/BuildTheEarth/projection +alpslib-geo = "1.0.0" # https://mvn.alps-bte.com/service/rest/repository/browse/alps-bte/com/alpsbte/alpslib/alpslib-geo/ [libraries] com-alpsbte-alpslib-alpslib-libpsterra = { module = "com.alpsbte.alpslib:alpslib-libpsterra", version.ref = "com-alpsbte-alpslib-alpslib-libpsterra" } @@ -41,6 +42,7 @@ fawe-bom = { module = "com.intellectualsites.bom:bom-newest", version.ref = "faw bstats-bukkit = { module = "org.bstats:bstats-bukkit", version.ref = "bstats" } bluemap-api = { module = "de.bluecolored:bluemap-api", version.ref = "bluemap-api" } net-buildtheearth-projection = { module = "net.buildtheearth:projection", version.ref = "net-buildtheearth-projection"} +alpslib-geo = { module = "com.alpsbte.alpslib:alpslib-geo", version.ref = "alpslib-geo" } [plugins] lombok = { id = "io.freefair.lombok", version.ref = "io-freefair-lombok" } diff --git a/settings.gradle.kts b/settings.gradle.kts index c6b80f74..ee0ae27f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -4,7 +4,7 @@ dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { mavenCentral() - //mavenLocal() // NEVER use in Production/Commits! + // mavenLocal() // NEVER use in Production/Commits! maven { url = uri("https://repo.papermc.io/repository/maven-public/") } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/NavUtils.java b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/NavUtils.java index 04030a1d..2a0c9053 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/NavUtils.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/NavUtils.java @@ -24,6 +24,8 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.Objects; + @UtilityClass public class NavUtils { public static void sendPlayerToConnectedServer(Player player, String server) { @@ -173,4 +175,18 @@ public static Location getLocationFromCoordinatesYawPitch(GeographicalCoordinate public static Location getLocationFromCoordinates(GeographicalCoordinate coordinate) { return getLocationFromCoordinatesYawPitch(coordinate, 0, 0); } + + /** + * Returns the CCA2 code of the country of the given country name. + */ + public static String getCCA2FromCountryName(String countryName, Player clickPlayer) { + var region = Objects.requireNonNull(NetworkModule.getInstance().getBuildTeam()).getRegions().stream().filter(regionF -> regionF.getName().equals(countryName)).findFirst(); + if (region.isPresent()) { + return region.get().getCountryCodeCca2(); + } else { + clickPlayer.sendMessage(ChatHelper.getErrorString("Could not find the country of the location! Please report that")); + return ""; + } + + } } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/NavigationModule.java b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/NavigationModule.java index 00bd28a1..72e78ed4 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/NavigationModule.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/NavigationModule.java @@ -1,5 +1,7 @@ package net.buildtheearth.buildteamtools.modules.navigation; +import com.alpsbte.alpslib.geo.rgc.RgcHandler; +import com.alpsbte.alpslib.utils.ChatHelper; import lombok.Getter; import net.buildtheearth.buildteamtools.BuildTeamTools; import net.buildtheearth.buildteamtools.modules.Module; @@ -21,6 +23,15 @@ import net.buildtheearth.buildteamtools.utils.io.ConfigPaths; import net.buildtheearth.buildteamtools.utils.io.ConfigUtil; import org.bukkit.Bukkit; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.FileOutputStream; +import java.net.URI; +import java.net.URL; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; +import java.nio.channels.ReadableByteChannel; /** * Manages all things related to universal tpll @@ -36,7 +47,9 @@ public class NavigationModule extends Module { private TpllComponent tpllComponent; @Getter private BluemapComponent bluemapComponent; - + @Getter + @Nullable + private RgcHandler rgcHandler = null; private static NavigationModule instance = null; @@ -60,9 +73,41 @@ public void enable() { navigatorComponent = new NavigatorComponent(); tpllComponent = new TpllComponent(); + var navConfig = BuildTeamTools.getInstance().getConfig(ConfigUtil.NAVIGATION); + + if (navConfig.getBoolean(ConfigPaths.Navigation.RGC_LOCAL_DB_ENABLED, false)) { + File rgcFile = BuildTeamTools.getInstance().getDataPath().resolve("modules/navigation").resolve(navConfig.getString(ConfigPaths.Navigation.RGC_LOCAL_DB_PATH, "bs.file")).toFile(); + ChatHelper.logDebug("Reverse Geocode local database support is enabled. Checking for local database file at: %s", rgcFile.getAbsolutePath()); + if (rgcFile.exists()) { + rgcHandler = new RgcHandler(rgcFile, BuildTeamTools.getInstance().getSLF4JLogger(), false); + } else { + BuildTeamTools.getInstance().getComponentLogger().info("Reverse Geocode local database is enabled but the file does not exist at the specified path, installing it from the configured url."); + Bukkit.getScheduler().runTaskAsynchronously(BuildTeamTools.getInstance(), () -> { + try { + if (!rgcFile.getParentFile().mkdirs()) { + BuildTeamTools.getInstance().getComponentLogger().warn("Failed to create parent directories for Reverse Geocode local database file. Make sure the plugin has the necessary permissions to create directories and files in the plugin data folder."); + } + URL url = URI.create(navConfig.getString(ConfigPaths.Navigation.RGC_LOCAL_DB_UPDATE_URL, "")).toURL(); + try (ReadableByteChannel readableByteChannel = Channels.newChannel(url.openStream())) { + try (FileOutputStream fileOutputStream = new FileOutputStream(rgcFile)) { + FileChannel fileChannel = fileOutputStream.getChannel(); + fileChannel.transferFrom(readableByteChannel, 0, Long.MAX_VALUE); + } + } + Bukkit.getScheduler().runTask(BuildTeamTools.getInstance(), () -> { + rgcHandler = new RgcHandler(rgcFile, BuildTeamTools.getInstance().getSLF4JLogger(), false); + BuildTeamTools.getInstance().getComponentLogger().info("Successfully downloaded Reverse Geocode local database and enabled local database support for Reverse Geocoding."); + }); + } catch (Exception e) { + BuildTeamTools.getInstance().getComponentLogger().error("Failed to download Reverse Geocode local database from the configured url, disabling local database support for Reverse Geocoding.", e); + navConfig.set(ConfigPaths.Navigation.RGC_LOCAL_DB_ENABLED, false); + } + }); + } + } + // Check if BlueMap plugin is enabled and config allows BlueMap integration - boolean bluemapConfigEnabled = BuildTeamTools.getInstance().getConfig(ConfigUtil.NAVIGATION) - .getBoolean(ConfigPaths.Navigation.BLUEMAP_ENABLED, true); + boolean bluemapConfigEnabled = navConfig.getBoolean(ConfigPaths.Navigation.BLUEMAP_ENABLED, true); if (Bukkit.getPluginManager().isPluginEnabled("BlueMap") && bluemapConfigEnabled) { bluemapComponent = new BluemapComponent(); diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/WarpsComponent.java b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/WarpsComponent.java index fb883591..466e1bd2 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/WarpsComponent.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/WarpsComponent.java @@ -168,6 +168,8 @@ public static void createWarp(@NonNull Player creator, WarpGroup group) { String regionName = result[0]; String countryCodeCCA2 = result[1].toUpperCase(); + if (countryCodeCCA2.isEmpty()) countryCodeCCA2 = NavUtils.getCCA2FromCountryName(regionName, creator); + //Check if the team owns this region/country boolean ownsRegion = NetworkModule.getInstance().ownsRegion(regionName, countryCodeCCA2); diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/menu/WarpEditMenu.java b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/menu/WarpEditMenu.java index 0230cd55..cc50cee9 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/menu/WarpEditMenu.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/menu/WarpEditMenu.java @@ -6,6 +6,7 @@ import net.buildtheearth.OutOfProjectionBoundsException; import net.buildtheearth.Projection; import net.buildtheearth.buildteamtools.BuildTeamTools; +import net.buildtheearth.buildteamtools.modules.navigation.NavUtils; import net.buildtheearth.buildteamtools.modules.navigation.components.warps.model.Warp; import net.buildtheearth.buildteamtools.modules.network.NetworkModule; import net.buildtheearth.buildteamtools.modules.network.api.OpenStreetMapAPI; @@ -144,6 +145,9 @@ protected void setItemClickEventsAsync() { String regionName = result[0]; String countryCodeCCA2 = result[1].toUpperCase(); + if (countryCodeCCA2.isEmpty()) + countryCodeCCA2 = NavUtils.getCCA2FromCountryName(regionName, clickPlayer); + //Check if the team owns this region/country boolean ownsRegion = NetworkModule.getInstance().ownsRegion(regionName, countryCodeCCA2); diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/network/api/OpenStreetMapAPI.java b/src/main/java/net/buildtheearth/buildteamtools/modules/network/api/OpenStreetMapAPI.java index 92d7993e..c48dbeb6 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/network/api/OpenStreetMapAPI.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/network/api/OpenStreetMapAPI.java @@ -1,11 +1,16 @@ package net.buildtheearth.buildteamtools.modules.network.api; +import com.alpsbte.alpslib.geo.AdminLevel; import com.alpsbte.alpslib.utils.ChatHelper; +import net.buildtheearth.buildteamtools.BuildTeamTools; +import net.buildtheearth.buildteamtools.modules.navigation.NavigationModule; +import org.bukkit.Bukkit; import org.jetbrains.annotations.NotNull; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import java.io.IOException; +import java.util.Objects; import java.util.concurrent.CompletableFuture; public class OpenStreetMapAPI extends API { @@ -16,6 +21,37 @@ public class OpenStreetMapAPI extends API { */ public static @NotNull CompletableFuture getCountryFromLocationAsync(double @NotNull [] coordinates) { CompletableFuture future = new CompletableFuture<>(); + + if (NavigationModule.getInstance().isEnabled() && NavigationModule.getInstance().getRgcHandler() != null) { + ChatHelper.logDebug("Using custom file API to get country from location: %s, %s", coordinates[0], coordinates[1]); + + // If we are not on main thread, schedule lookup on main thread and complete the outer future there + if (!Bukkit.isPrimaryThread()) { + ChatHelper.logDebug("Not on main thread: scheduling RGC lookup on main thread..."); + Bukkit.getScheduler().runTask(BuildTeamTools.getInstance(), () -> { + try { + var rgcGeoLocation = NavigationModule.getInstance().getRgcHandler() + .locationFromCoordinates((float) coordinates[0], (float) coordinates[1]); + ChatHelper.logDebug("RGC lookup successful: %s", rgcGeoLocation); + future.complete(new String[]{rgcGeoLocation.get(AdminLevel.COUNTRY), ""}); + } catch (Exception ex) { + future.completeExceptionally(ex); + } + }); + return future; + } + + // We're on the main thread already — run synchronously + try { + var location = Objects.requireNonNull(NavigationModule.getInstance().getRgcHandler()).locationFromCoordinates((float) coordinates[0], (float) coordinates[1]); + ChatHelper.logDebug("RGC lookup successful: %s", location); + future.complete(new String[]{location.get(AdminLevel.COUNTRY), ""}); + } catch (Exception ex) { + future.completeExceptionally(ex); + } + return future; + } + String url = "https://photon.komoot.io/reverse?lat=" + coordinates[0] + "&lon=" + coordinates[1] + "&lang=en"; ChatHelper.logDebug("Requesting country from location: %s", url); diff --git a/src/main/java/net/buildtheearth/buildteamtools/utils/io/ConfigPaths.java b/src/main/java/net/buildtheearth/buildteamtools/utils/io/ConfigPaths.java index 671d0222..51dadc8d 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/utils/io/ConfigPaths.java +++ b/src/main/java/net/buildtheearth/buildteamtools/utils/io/ConfigPaths.java @@ -45,6 +45,12 @@ public static class Navigation { // BlueMap Integration private static final String BLUEMAP = "bluemap."; public static final String BLUEMAP_ENABLED = BLUEMAP + "enabled"; + + // Reverse Geocode + private static final String RGC_LOCAL_DB = "reverse-geocode.local-database."; + public static final String RGC_LOCAL_DB_ENABLED = RGC_LOCAL_DB + "enabled"; + public static final String RGC_LOCAL_DB_UPDATE_URL = RGC_LOCAL_DB + "url"; + public static final String RGC_LOCAL_DB_PATH = RGC_LOCAL_DB + "path"; } public static class PlotSystem { diff --git a/src/main/java/net/buildtheearth/buildteamtools/utils/io/ConfigUtil.java b/src/main/java/net/buildtheearth/buildteamtools/utils/io/ConfigUtil.java index e7b20228..472e9968 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/utils/io/ConfigUtil.java +++ b/src/main/java/net/buildtheearth/buildteamtools/utils/io/ConfigUtil.java @@ -42,7 +42,7 @@ public static void init() throws ConfigNotImplementedException { configUtilInstance = new ConfigurationUtil(new ConfigurationUtil.ConfigFile[]{ new ConfigurationUtil.ConfigFile(Paths.get("config.yml"), 1.4, false), new ConfigurationUtil.ConfigFile(Paths.get("modules", "plotsystem", "config.yml"), 1.6, false), - new ConfigurationUtil.ConfigFile(Paths.get("modules", "navigation", "config.yml"), 1.6, false), + new ConfigurationUtil.ConfigFile(Paths.get("modules", "navigation", "config.yml"), 1.7, false), }); } diff --git a/src/main/resources/modules/navigation/config.yml b/src/main/resources/modules/navigation/config.yml index 4b8d116c..f32043da 100644 --- a/src/main/resources/modules/navigation/config.yml +++ b/src/main/resources/modules/navigation/config.yml @@ -52,5 +52,16 @@ bluemap: # Enables or disables the BlueMap integration for displaying warps [true|false] enabled: true +reverse-geocode: + local-database: + # Whether to use the local database for reverse geocoding or not. If false, the online Photon/Komoot API will be used. + # Currently, we use only https://photon.komoot.io/ for online reverse geocoding. + enabled: true + # The URL to the local database. More infos: https://github.com/kno10/reversegeocode/blob/master/data/README.md + # The file is automatically downloaded once if it does not exist. + url: "https://data.ub.uni-muenchen.de/61/8/osm-20151130-0.001-2.bin" + # The relative path to the local database. + path: "reversegeocode/osm-20151130-0.001-2.bin" + # NOTE: Do not change -config-version: 1.6 \ No newline at end of file +config-version: 1.7 \ No newline at end of file From c80436e220ce7be29fb7806a3bbe52a68bc28d40 Mon Sep 17 00:00:00 2001 From: Zoriot Date: Tue, 9 Jun 2026 21:02:53 +0200 Subject: [PATCH 2/2] =?UTF-8?q?refactor(navigation):=20=E2=99=BB=EF=B8=8F?= =?UTF-8?q?=20extract=20helpers=20and=20flatten=20nested=20conditionals?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address PR review by splitting RGC setup, region lookup, and OpenStreetMap country resolution into focused methods and switch-based flows. --- .../modules/navigation/NavUtils.java | 68 ++++++++----- .../modules/navigation/NavigationModule.java | 98 +++++++++++++------ .../modules/network/api/OpenStreetMapAPI.java | 89 +++++++++-------- 3 files changed, 158 insertions(+), 97 deletions(-) diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/NavUtils.java b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/NavUtils.java index 2a0c9053..43f59088 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/NavUtils.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/NavUtils.java @@ -10,6 +10,7 @@ import net.buildtheearth.buildteamtools.modules.navigation.components.warps.model.WarpGroup; import net.buildtheearth.buildteamtools.modules.network.NetworkModule; import net.buildtheearth.buildteamtools.modules.network.model.BuildTeam; +import net.buildtheearth.buildteamtools.modules.network.model.Region; import net.buildtheearth.model.GeographicalCoordinate; import net.buildtheearth.model.MinecraftCoordinate; import net.md_5.bungee.api.chat.ClickEvent; @@ -21,10 +22,10 @@ import org.bukkit.UnsafeValues; import org.bukkit.World; import org.bukkit.entity.Player; +import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; - -import java.util.Objects; +import org.jspecify.annotations.NonNull; @UtilityClass public class NavUtils { @@ -92,17 +93,19 @@ public static void sendNoIpMessage(@NotNull Player player, String buildteam) { public static @Nullable NavSwitchType determineSwitchPossibilityOrMsgPlayerIfNone(@NotNull Player player, @NotNull BuildTeam targetBuildTeam) { if (targetBuildTeam.isConnected() && targetBuildTeam.getServerName() != null) { return NavSwitchType.NETWORK; - } else if (targetBuildTeam.getIP() != null) { - if (isTransferCapable(player, targetBuildTeam)) { - return NavSwitchType.TRANSFER; - } else { - sendNotConnectedMessage(player, targetBuildTeam.getIP(), targetBuildTeam.getName()); - return null; - } - } else { + } + + if (targetBuildTeam.getIP() == null) { sendNoIpMessage(player, targetBuildTeam.getName()); return null; } + + if (isTransferCapable(player, targetBuildTeam)) { + return NavSwitchType.TRANSFER; + } + + sendNotConnectedMessage(player, targetBuildTeam.getIP(), targetBuildTeam.getName()); + return null; } public enum NavSwitchType { @@ -110,14 +113,14 @@ public enum NavSwitchType { } public static void switchToTeam(BuildTeam team, Player clickPlayer) { - var type = NavUtils.determineSwitchPossibilityOrMsgPlayerIfNone(clickPlayer, team); - - if (type != null) { - if (type == NavUtils.NavSwitchType.NETWORK) { - NavUtils.sendPlayerToConnectedServer(clickPlayer, team.getServerName()); - } else if (type == NavUtils.NavSwitchType.TRANSFER) { - NavUtils.transferPlayer(clickPlayer, team.getIP()); - } + NavSwitchType type = determineSwitchPossibilityOrMsgPlayerIfNone(clickPlayer, team); + if (type == null) { + return; + } + + switch (type) { + case NETWORK -> sendPlayerToConnectedServer(clickPlayer, team.getServerName()); + case TRANSFER -> transferPlayer(clickPlayer, team.getIP()); } } @@ -142,7 +145,8 @@ public static void switchToTeam(BuildTeam team, Player clickPlayer) { * @param pitch Player's pitch * @return A bukkit location matching the coordinates, yaw and pitch specified. Height is terrain elevation +2. */ - public static Location getLocationFromCoordinatesYawPitch(GeographicalCoordinate coordinate, float yaw, float pitch) { + @Contract("_, _, _ -> new") + public static @NonNull Location getLocationFromCoordinatesYawPitch(GeographicalCoordinate coordinate, float yaw, float pitch) { try { MinecraftCoordinate mcCoord = Projection.toMinecraft(coordinate); @@ -172,7 +176,8 @@ public static Location getLocationFromCoordinatesYawPitch(GeographicalCoordinate * @param coordinate Latitude and longitude of the location * @return A bukkit location matching the coordinates. Height is terrain elevation +2. */ - public static Location getLocationFromCoordinates(GeographicalCoordinate coordinate) { + @Contract("_ -> new") + public static @NonNull Location getLocationFromCoordinates(GeographicalCoordinate coordinate) { return getLocationFromCoordinatesYawPitch(coordinate, 0, 0); } @@ -180,13 +185,24 @@ public static Location getLocationFromCoordinates(GeographicalCoordinate coordin * Returns the CCA2 code of the country of the given country name. */ public static String getCCA2FromCountryName(String countryName, Player clickPlayer) { - var region = Objects.requireNonNull(NetworkModule.getInstance().getBuildTeam()).getRegions().stream().filter(regionF -> regionF.getName().equals(countryName)).findFirst(); - if (region.isPresent()) { - return region.get().getCountryCodeCca2(); - } else { - clickPlayer.sendMessage(ChatHelper.getErrorString("Could not find the country of the location! Please report that")); - return ""; + Region region = findRegionByName(countryName); + if (region != null) { + return region.getCountryCodeCca2(); + } + + clickPlayer.sendMessage(ChatHelper.getErrorString("Could not find the country of the location! Please report that")); + return ""; + } + + private static @Nullable Region findRegionByName(String countryName) { + BuildTeam buildTeam = NetworkModule.getInstance().getBuildTeam(); + if (buildTeam == null) { + return null; } + return buildTeam.getRegions().stream() + .filter(region -> region.getName().equals(countryName)) + .findFirst() + .orElse(null); } } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/NavigationModule.java b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/NavigationModule.java index 72e78ed4..dd0ce1cf 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/NavigationModule.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/NavigationModule.java @@ -23,10 +23,14 @@ import net.buildtheearth.buildteamtools.utils.io.ConfigPaths; import net.buildtheearth.buildteamtools.utils.io.ConfigUtil; import org.bukkit.Bukkit; +import org.bukkit.configuration.file.FileConfiguration; +import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Nullable; +import org.jspecify.annotations.NonNull; import java.io.File; import java.io.FileOutputStream; +import java.io.IOException; import java.net.URI; import java.net.URL; import java.nio.channels.Channels; @@ -74,46 +78,78 @@ public void enable() { tpllComponent = new TpllComponent(); var navConfig = BuildTeamTools.getInstance().getConfig(ConfigUtil.NAVIGATION); + initializeRgcHandler(navConfig); + initializeBluemapComponent(navConfig); - if (navConfig.getBoolean(ConfigPaths.Navigation.RGC_LOCAL_DB_ENABLED, false)) { - File rgcFile = BuildTeamTools.getInstance().getDataPath().resolve("modules/navigation").resolve(navConfig.getString(ConfigPaths.Navigation.RGC_LOCAL_DB_PATH, "bs.file")).toFile(); - ChatHelper.logDebug("Reverse Geocode local database support is enabled. Checking for local database file at: %s", rgcFile.getAbsolutePath()); - if (rgcFile.exists()) { - rgcHandler = new RgcHandler(rgcFile, BuildTeamTools.getInstance().getSLF4JLogger(), false); - } else { - BuildTeamTools.getInstance().getComponentLogger().info("Reverse Geocode local database is enabled but the file does not exist at the specified path, installing it from the configured url."); - Bukkit.getScheduler().runTaskAsynchronously(BuildTeamTools.getInstance(), () -> { - try { - if (!rgcFile.getParentFile().mkdirs()) { - BuildTeamTools.getInstance().getComponentLogger().warn("Failed to create parent directories for Reverse Geocode local database file. Make sure the plugin has the necessary permissions to create directories and files in the plugin data folder."); - } - URL url = URI.create(navConfig.getString(ConfigPaths.Navigation.RGC_LOCAL_DB_UPDATE_URL, "")).toURL(); - try (ReadableByteChannel readableByteChannel = Channels.newChannel(url.openStream())) { - try (FileOutputStream fileOutputStream = new FileOutputStream(rgcFile)) { - FileChannel fileChannel = fileOutputStream.getChannel(); - fileChannel.transferFrom(readableByteChannel, 0, Long.MAX_VALUE); - } - } - Bukkit.getScheduler().runTask(BuildTeamTools.getInstance(), () -> { - rgcHandler = new RgcHandler(rgcFile, BuildTeamTools.getInstance().getSLF4JLogger(), false); - BuildTeamTools.getInstance().getComponentLogger().info("Successfully downloaded Reverse Geocode local database and enabled local database support for Reverse Geocoding."); - }); - } catch (Exception e) { - BuildTeamTools.getInstance().getComponentLogger().error("Failed to download Reverse Geocode local database from the configured url, disabling local database support for Reverse Geocoding.", e); - navConfig.set(ConfigPaths.Navigation.RGC_LOCAL_DB_ENABLED, false); - } + super.enable(); + } + + private void initializeRgcHandler(@NonNull FileConfiguration navConfig) { + if (!navConfig.getBoolean(ConfigPaths.Navigation.RGC_LOCAL_DB_ENABLED, false)) { + return; + } + + File rgcFile = resolveRgcDatabaseFile(navConfig); + ChatHelper.logDebug("Reverse Geocode local database support is enabled. Checking for local database file at: %s", rgcFile.getAbsolutePath()); + + if (rgcFile.exists()) { + rgcHandler = createRgcHandler(rgcFile); + return; + } + + downloadRgcDatabaseAsync(rgcFile, navConfig); + } + + private @NonNull File resolveRgcDatabaseFile(@NonNull FileConfiguration navConfig) { + String path = navConfig.getString(ConfigPaths.Navigation.RGC_LOCAL_DB_PATH, "bs.file"); + return BuildTeamTools.getInstance().getDataPath() + .resolve("modules/navigation") + .resolve(path) + .toFile(); + } + + @Contract("_ -> new") + private @NonNull RgcHandler createRgcHandler(File rgcFile) { + return new RgcHandler(rgcFile, BuildTeamTools.getInstance().getSLF4JLogger(), false); + } + + private void downloadRgcDatabaseAsync(File rgcFile, FileConfiguration navConfig) { + BuildTeamTools.getInstance().getComponentLogger().info( + "Reverse Geocode local database is enabled but the file does not exist at the specified path, installing it from the configured url."); + Bukkit.getScheduler().runTaskAsynchronously(BuildTeamTools.getInstance(), () -> { + try { + downloadRgcDatabase(rgcFile, navConfig); + Bukkit.getScheduler().runTask(BuildTeamTools.getInstance(), () -> { + rgcHandler = createRgcHandler(rgcFile); + BuildTeamTools.getInstance().getComponentLogger().info( + "Successfully downloaded Reverse Geocode local database and enabled local database support for Reverse Geocoding."); }); + } catch (Exception e) { + BuildTeamTools.getInstance().getComponentLogger().error( + "Failed to download Reverse Geocode local database from the configured url, disabling local database support for Reverse Geocoding.", e); + navConfig.set(ConfigPaths.Navigation.RGC_LOCAL_DB_ENABLED, false); } + }); + } + + private void downloadRgcDatabase(@NonNull File rgcFile, FileConfiguration navConfig) throws IOException { + if (!rgcFile.getParentFile().mkdirs()) { + BuildTeamTools.getInstance().getComponentLogger().warn( + "Failed to create parent directories for Reverse Geocode local database file. Make sure the plugin has the necessary permissions to create directories and files in the plugin data folder."); } + URL url = URI.create(navConfig.getString(ConfigPaths.Navigation.RGC_LOCAL_DB_UPDATE_URL, "")).toURL(); + try (ReadableByteChannel readableByteChannel = Channels.newChannel(url.openStream()); + FileOutputStream fileOutputStream = new FileOutputStream(rgcFile)) { + FileChannel fileChannel = fileOutputStream.getChannel(); + fileChannel.transferFrom(readableByteChannel, 0, Long.MAX_VALUE); + } + } - // Check if BlueMap plugin is enabled and config allows BlueMap integration + private void initializeBluemapComponent(@NonNull FileConfiguration navConfig) { boolean bluemapConfigEnabled = navConfig.getBoolean(ConfigPaths.Navigation.BLUEMAP_ENABLED, true); - if (Bukkit.getPluginManager().isPluginEnabled("BlueMap") && bluemapConfigEnabled) { bluemapComponent = new BluemapComponent(); } - - super.enable(); } @Override diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/network/api/OpenStreetMapAPI.java b/src/main/java/net/buildtheearth/buildteamtools/modules/network/api/OpenStreetMapAPI.java index c48dbeb6..d6cc05e3 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/network/api/OpenStreetMapAPI.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/network/api/OpenStreetMapAPI.java @@ -8,9 +8,9 @@ import org.jetbrains.annotations.NotNull; import org.json.simple.JSONArray; import org.json.simple.JSONObject; +import org.jspecify.annotations.NonNull; import java.io.IOException; -import java.util.Objects; import java.util.concurrent.CompletableFuture; public class OpenStreetMapAPI extends API { @@ -20,38 +20,45 @@ public class OpenStreetMapAPI extends API { * @return The country name and country code belonging to this location */ public static @NotNull CompletableFuture getCountryFromLocationAsync(double @NotNull [] coordinates) { - CompletableFuture future = new CompletableFuture<>(); + if (canUseRgcHandler()) { + return getCountryFromRgcAsync(coordinates); + } + return getCountryFromPhotonAsync(coordinates); + } - if (NavigationModule.getInstance().isEnabled() && NavigationModule.getInstance().getRgcHandler() != null) { - ChatHelper.logDebug("Using custom file API to get country from location: %s, %s", coordinates[0], coordinates[1]); - - // If we are not on main thread, schedule lookup on main thread and complete the outer future there - if (!Bukkit.isPrimaryThread()) { - ChatHelper.logDebug("Not on main thread: scheduling RGC lookup on main thread..."); - Bukkit.getScheduler().runTask(BuildTeamTools.getInstance(), () -> { - try { - var rgcGeoLocation = NavigationModule.getInstance().getRgcHandler() - .locationFromCoordinates((float) coordinates[0], (float) coordinates[1]); - ChatHelper.logDebug("RGC lookup successful: %s", rgcGeoLocation); - future.complete(new String[]{rgcGeoLocation.get(AdminLevel.COUNTRY), ""}); - } catch (Exception ex) { - future.completeExceptionally(ex); - } - }); - return future; - } + private static boolean canUseRgcHandler() { + return NavigationModule.getInstance().isEnabled() + && NavigationModule.getInstance().getRgcHandler() != null; + } - // We're on the main thread already — run synchronously - try { - var location = Objects.requireNonNull(NavigationModule.getInstance().getRgcHandler()).locationFromCoordinates((float) coordinates[0], (float) coordinates[1]); - ChatHelper.logDebug("RGC lookup successful: %s", location); - future.complete(new String[]{location.get(AdminLevel.COUNTRY), ""}); - } catch (Exception ex) { - future.completeExceptionally(ex); - } + private static @NotNull CompletableFuture getCountryFromRgcAsync(double @NonNull [] coordinates) { + CompletableFuture future = new CompletableFuture<>(); + ChatHelper.logDebug("Using custom file API to get country from location: %s, %s", coordinates[0], coordinates[1]); + + if (!Bukkit.isPrimaryThread()) { + ChatHelper.logDebug("Not on main thread: scheduling RGC lookup on main thread..."); + Bukkit.getScheduler().runTask(BuildTeamTools.getInstance(), () -> completeRgcLookup(coordinates, future)); return future; } + completeRgcLookup(coordinates, future); + return future; + } + + private static void completeRgcLookup(double[] coordinates, CompletableFuture future) { + try { + if (NavigationModule.getInstance().getRgcHandler() == null) throw new AssertionError("RgcHandler have to be initialized first"); + var location = NavigationModule.getInstance().getRgcHandler() + .locationFromCoordinates((float) coordinates[0], (float) coordinates[1]); + ChatHelper.logDebug("RGC lookup successful: %s", location); + future.complete(new String[]{location.get(AdminLevel.COUNTRY), ""}); + } catch (Exception ex) { + future.completeExceptionally(ex); + } + } + + private static @NotNull CompletableFuture getCountryFromPhotonAsync(double @NonNull [] coordinates) { + CompletableFuture future = new CompletableFuture<>(); String url = "https://photon.komoot.io/reverse?lat=" + coordinates[0] + "&lon=" + coordinates[1] + "&lang=en"; ChatHelper.logDebug("Requesting country from location: %s", url); @@ -59,18 +66,7 @@ public class OpenStreetMapAPI extends API { API.getAsync(url, new API.ApiResponseCallback() { @Override public void onResponse(String response) { - JSONObject jsonObject = API.createJSONObject(response); - - ChatHelper.logDebug("Response from OpenStreetMap: %s", jsonObject); - - JSONObject featuresObject = (JSONObject) ((JSONArray) jsonObject.get("features")).getFirst(); - - JSONObject propertiesObject = (JSONObject) featuresObject.get("properties"); - - String countryCodeCca2 = (String) propertiesObject.get("countrycode"); - String countryName = (String) propertiesObject.get("country"); - - future.complete(new String[]{countryName, countryCodeCca2}); + completePhotonLookup(response, future); } @Override @@ -80,4 +76,17 @@ public void onFailure(IOException e) { }); return future; } + + private static void completePhotonLookup(String response, @NonNull CompletableFuture future) { + JSONObject jsonObject = API.createJSONObject(response); + ChatHelper.logDebug("Response from OpenStreetMap: %s", jsonObject); + + JSONObject featuresObject = (JSONObject) ((JSONArray) jsonObject.get("features")).getFirst(); + JSONObject propertiesObject = (JSONObject) featuresObject.get("properties"); + + String countryCodeCca2 = (String) propertiesObject.get("countrycode"); + String countryName = (String) propertiesObject.get("country"); + + future.complete(new String[]{countryName, countryCodeCca2}); + } }