diff --git a/build.gradle.kts b/build.gradle.kts index 760775d..ee90e56 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 0ccedcb..e730b6c 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 c6b80f7..ee0ae27 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 04030a1..43f5908 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,8 +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 org.jspecify.annotations.NonNull; @UtilityClass public class NavUtils { @@ -90,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 { @@ -108,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()); } } @@ -140,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); @@ -170,7 +176,33 @@ 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); } + + /** + * Returns the CCA2 code of the country of the given country name. + */ + public static String getCCA2FromCountryName(String countryName, Player clickPlayer) { + 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 00bd28a..dd0ce1c 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,19 @@ 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; +import java.nio.channels.FileChannel; +import java.nio.channels.ReadableByteChannel; /** * Manages all things related to universal tpll @@ -36,7 +51,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,15 +77,79 @@ public void enable() { navigatorComponent = new NavigatorComponent(); tpllComponent = new TpllComponent(); - // Check if BlueMap plugin is enabled and config allows BlueMap integration - boolean bluemapConfigEnabled = BuildTeamTools.getInstance().getConfig(ConfigUtil.NAVIGATION) - .getBoolean(ConfigPaths.Navigation.BLUEMAP_ENABLED, true); + var navConfig = BuildTeamTools.getInstance().getConfig(ConfigUtil.NAVIGATION); + initializeRgcHandler(navConfig); + initializeBluemapComponent(navConfig); + + 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); + } + } + + 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/navigation/components/warps/WarpsComponent.java b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/WarpsComponent.java index fb88359..466e1bd 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 0230cd5..cc50cee 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 92d7993..d6cc05e 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,9 +1,14 @@ 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 org.jspecify.annotations.NonNull; import java.io.IOException; import java.util.concurrent.CompletableFuture; @@ -15,6 +20,44 @@ public class OpenStreetMapAPI extends API { * @return The country name and country code belonging to this location */ public static @NotNull CompletableFuture getCountryFromLocationAsync(double @NotNull [] coordinates) { + if (canUseRgcHandler()) { + return getCountryFromRgcAsync(coordinates); + } + return getCountryFromPhotonAsync(coordinates); + } + + private static boolean canUseRgcHandler() { + return NavigationModule.getInstance().isEnabled() + && NavigationModule.getInstance().getRgcHandler() != null; + } + + 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"; @@ -23,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 @@ -44,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}); + } } 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 671d022..51dadc8 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 e7b2022..472e996 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 4b8d116..f32043d 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