package kst4contest.view.map; import java.util.ArrayList; import java.util.List; import java.util.Locale; /** * Utility methods for generating visible Maidenhead grid rectangles. * * Supported levels: * - 2 characters (fields) * - 4 characters (squares) * - 6 characters (subsquares) */ public final class MaidenheadGridUtils { private static final double EPSILON = 1e-9; private MaidenheadGridUtils() { } public enum GridPrecision { FIELD_2(2, 20.0, 10.0), SQUARE_4(4, 2.0, 1.0), SUBSQUARE_6(6, 5.0 / 60.0, 2.5 / 60.0); private final int locatorLength; private final double cellWidthDeg; private final double cellHeightDeg; GridPrecision(int locatorLength, double cellWidthDeg, double cellHeightDeg) { this.locatorLength = locatorLength; this.cellWidthDeg = cellWidthDeg; this.cellHeightDeg = cellHeightDeg; } public int locatorLength() { return locatorLength; } public double cellWidthDeg() { return cellWidthDeg; } public double cellHeightDeg() { return cellHeightDeg; } } public record GridCell( String locatorLabel, double southLat, double westLon, double northLat, double eastLon, int rowIndex, int columnIndex ) { } public static GridPrecision precisionForZoom(int leafletZoom) { if (leafletZoom <= 5) { return GridPrecision.FIELD_2; } if (leafletZoom <= 7) { return GridPrecision.SQUARE_4; } return GridPrecision.SUBSQUARE_6; } public static List buildVisibleCells(double southLat, double westLon, double northLat, double eastLon, int leafletZoom) { return buildVisibleCells(southLat, westLon, northLat, eastLon, precisionForZoom(leafletZoom)); } public static List buildVisibleCells(double southLat, double westLon, double northLat, double eastLon, GridPrecision precision) { if (westLon > eastLon) { // Anti-meridian handling is not needed for Europe in this project stage. return List.of(); } double clampedSouth = clamp(southLat, -90.0 + EPSILON, 90.0 - EPSILON); double clampedNorth = clamp(northLat, -90.0 + EPSILON, 90.0 - EPSILON); double clampedWest = clamp(westLon, -180.0 + EPSILON, 180.0 - EPSILON); double clampedEast = clamp(eastLon, -180.0 + EPSILON, 180.0 - EPSILON); return switch (precision) { case FIELD_2 -> build2CharFields(clampedSouth, clampedWest, clampedNorth, clampedEast); case SQUARE_4 -> build4CharSquares(clampedSouth, clampedWest, clampedNorth, clampedEast); case SUBSQUARE_6 -> build6CharSubsquares(clampedSouth, clampedWest, clampedNorth, clampedEast); }; } private static List build2CharFields(double southLat, double westLon, double northLat, double eastLon) { List cells = new ArrayList<>(); int lonStart = clampIndex((int) Math.floor((westLon + 180.0) / 20.0), 0, 17); int lonEnd = clampIndex((int) Math.floor((eastLon + 180.0 - EPSILON) / 20.0), 0, 17); int latStart = clampIndex((int) Math.floor((southLat + 90.0) / 10.0), 0, 17); int latEnd = clampIndex((int) Math.floor((northLat + 90.0 - EPSILON) / 10.0), 0, 17); for (int lonIndex = lonStart; lonIndex <= lonEnd; lonIndex++) { for (int latIndex = latStart; latIndex <= latEnd; latIndex++) { double west = -180.0 + lonIndex * 20.0; double east = west + 20.0; double south = -90.0 + latIndex * 10.0; double north = south + 10.0; String label = "" + (char) ('A' + lonIndex) + (char) ('A' + latIndex); cells.add(new GridCell(label, south, west, north, east, latIndex, lonIndex)); } } return cells; } private static List build4CharSquares(double southLat, double westLon, double northLat, double eastLon) { List cells = new ArrayList<>(); int lonStart = clampIndex((int) Math.floor((westLon + 180.0) / 2.0), 0, 179); int lonEnd = clampIndex((int) Math.floor((eastLon + 180.0 - EPSILON) / 2.0), 0, 179); int latStart = clampIndex((int) Math.floor((southLat + 90.0) / 1.0), 0, 179); int latEnd = clampIndex((int) Math.floor((northLat + 90.0 - EPSILON) / 1.0), 0, 179); for (int lonTotalIndex = lonStart; lonTotalIndex <= lonEnd; lonTotalIndex++) { for (int latTotalIndex = latStart; latTotalIndex <= latEnd; latTotalIndex++) { int lonFieldIndex = lonTotalIndex / 10; int lonSquareIndex = lonTotalIndex % 10; int latFieldIndex = latTotalIndex / 10; int latSquareIndex = latTotalIndex % 10; double west = -180.0 + lonTotalIndex * 2.0; double east = west + 2.0; double south = -90.0 + latTotalIndex; double north = south + 1.0; String label = String.format( Locale.ROOT, "%c%c%d%d", (char) ('A' + lonFieldIndex), (char) ('A' + latFieldIndex), lonSquareIndex, latSquareIndex ); cells.add(new GridCell(label, south, west, north, east, latTotalIndex, lonTotalIndex)); } } return cells; } private static List build6CharSubsquares(double southLat, double westLon, double northLat, double eastLon) { List cells = new ArrayList<>(); double lonStepDeg = 5.0 / 60.0; double latStepDeg = 2.5 / 60.0; int lonStart = clampIndex((int) Math.floor((westLon + 180.0) / lonStepDeg), 0, 4319); int lonEnd = clampIndex((int) Math.floor((eastLon + 180.0 - EPSILON) / lonStepDeg), 0, 4319); int latStart = clampIndex((int) Math.floor((southLat + 90.0) / latStepDeg), 0, 4319); int latEnd = clampIndex((int) Math.floor((northLat + 90.0 - EPSILON) / latStepDeg), 0, 4319); for (int lonTotalIndex = lonStart; lonTotalIndex <= lonEnd; lonTotalIndex++) { for (int latTotalIndex = latStart; latTotalIndex <= latEnd; latTotalIndex++) { int lonFieldIndex = lonTotalIndex / 240; int lonWithinField = lonTotalIndex % 240; int lonSquareIndex = lonWithinField / 24; int lonSubsquareIndex = lonWithinField % 24; int latFieldIndex = latTotalIndex / 240; int latWithinField = latTotalIndex % 240; int latSquareIndex = latWithinField / 24; int latSubsquareIndex = latWithinField % 24; double west = -180.0 + lonTotalIndex * lonStepDeg; double east = west + lonStepDeg; double south = -90.0 + latTotalIndex * latStepDeg; double north = south + latStepDeg; String label = String.format( Locale.ROOT, "%c%c%d%d%c%c", (char) ('A' + lonFieldIndex), (char) ('A' + latFieldIndex), lonSquareIndex, latSquareIndex, (char) ('a' + lonSubsquareIndex), (char) ('a' + latSubsquareIndex) ); cells.add(new GridCell(label, south, west, north, east, latTotalIndex, lonTotalIndex)); } } return cells; } private static int clampIndex(int value, int min, int max) { return Math.max(min, Math.min(max, value)); } private static double clamp(double value, double min, double max) { return Math.max(min, Math.min(max, value)); } }