diff --git a/pom.xml b/pom.xml index 5d994c9..768827b 100644 --- a/pom.xml +++ b/pom.xml @@ -83,6 +83,9 @@ maven-surefire-plugin 2.22.2 + + -Xmx1024m + diff --git a/src/main/java/com/masterisehomes/geometryapi/geodesy/Harversine.java b/src/main/java/com/masterisehomes/geometryapi/geodesy/Harversine.java index 264d68e..b06e9bf 100644 --- a/src/main/java/com/masterisehomes/geometryapi/geodesy/Harversine.java +++ b/src/main/java/com/masterisehomes/geometryapi/geodesy/Harversine.java @@ -2,6 +2,8 @@ import java.lang.Math; +import com.masterisehomes.geometryapi.hexagon.Coordinates; + public class Harversine { /* * Earth's radius at the equator (WGS-84) @@ -15,20 +17,41 @@ public class Harversine { */ public static double distance(double lat1, double lng1, double lat2, double lng2) { // Convert latitudes to Radians - double phi_1 = Math.toRadians(lat1); - double phi_2 = Math.toRadians(lat2); + final double phi_1 = Math.toRadians(lat1); + final double phi_2 = Math.toRadians(lat2); // Distance between latitudes and longitudes - double delta_phi = Math.toRadians(lat2 - lat1); - double delta_lambda = Math.toRadians(lng2 - lng1); + final double delta_phi = Math.toRadians(lat2 - lat1); + final double delta_lambda = Math.toRadians(lng2 - lng1); // Apply Harversine formula - double a = Math.pow(Math.sin(delta_phi / 2), 2) + final double a = Math.pow(Math.sin(delta_phi / 2), 2) + Math.pow(Math.sin(delta_lambda / 2), 2) * Math.cos(phi_1) * Math.cos(phi_2); + final double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + final double distance = R * c; // meters + + return distance; + } + + public static double distance(Coordinates coordinates1, Coordinates coordinates2) { + final double lat1 = coordinates1.getLatitude(); + final double lng1 = coordinates1.getLongitude(); + final double lat2 = coordinates2.getLatitude(); + final double lng2 = coordinates2.getLongitude(); - double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + // Convert latitudes to Radians + final double phi_1 = Math.toRadians(lat1); + final double phi_2 = Math.toRadians(lat2); + + // Distance between latitudes and longitudes + final double delta_phi = Math.toRadians(lat2 - lat1); + final double delta_lambda = Math.toRadians(lng2 - lng1); - double distance = R * c; // meters + // Apply Harversine formula + final double a = Math.pow(Math.sin(delta_phi / 2), 2) + + Math.pow(Math.sin(delta_lambda / 2), 2) * Math.cos(phi_1) * Math.cos(phi_2); + final double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + final double distance = R * c; // meters return distance; } diff --git a/src/main/java/com/masterisehomes/geometryapi/tessellation/AxialClockwiseTessellation.java b/src/main/java/com/masterisehomes/geometryapi/tessellation/AxialClockwiseTessellation.java index 0980cdf..096afc2 100644 --- a/src/main/java/com/masterisehomes/geometryapi/tessellation/AxialClockwiseTessellation.java +++ b/src/main/java/com/masterisehomes/geometryapi/tessellation/AxialClockwiseTessellation.java @@ -1,6 +1,8 @@ package com.masterisehomes.geometryapi.tessellation; import java.lang.Math; +import java.math.RoundingMode; +import java.text.DecimalFormat; import java.util.List; import java.util.ArrayList; @@ -87,6 +89,8 @@ public class AxialClockwiseTessellation { /* Basic stats here */ @Getter private int totalHexagons = 0; + @Getter + private double tessellationDistance = 0; /* * Corner Hexagons - used to find Edge Hexagons (based on nthRing) @@ -491,7 +495,7 @@ private final List generateGisEdgeHexagons(Hexagon gisCornerHexagon, Ne /* Calculate RequiredRings */ private final int calculateRequiredRings(Boundary boundary) { /* - * ARBITRARY RING ADJUSTMENT CONSTANT + * ERROR MARGINS - TODO: needs to update descriptions * * Geometrically, at the outer most ring, 1/6 the area of each hexagon can be * missed. @@ -504,17 +508,29 @@ private final int calculateRequiredRings(Boundary boundary) { final int CENTROID_PLACEMENT_ERROR_MARGIN = 1; final int RING_ERROR_MARGIN = GRID_GEOMETRIC_ERROR_MARGIN + CENTROID_PLACEMENT_ERROR_MARGIN; - /* Coordinates */ - final double minLat = boundary.getMinLat(); - final double minLng = boundary.getMinLng(); - final double maxLat = boundary.getMaxLat(); - final double maxLng = boundary.getMaxLng(); - /* - * Calculate the Great-circle Distance between the START and END boundary - * coordinates + * Calculate the requiredTessellationDistance from boundary by Harversine formula + * + * is the minimum distance required to generate Grid Map that can cover boundary + * fully - this distance can grow up to maxBoundaryDistance, depending where the + * rootHexagon is placed within Boundary. */ - final double maxBoundaryDistance = Harversine.distance(minLat, minLng, maxLat, maxLng); + final Coordinates minCoordinates = boundary.getMinCoordinates(); + final Coordinates maxCoordinates = boundary.getMaxCoordinates(); + final Coordinates centroidCoordinates = rootHexagon.getCentroid(); + + // Calculate distance between Boundary's minCoordinates/maxCoordinates to rootCentroid + final double minCoordinatesDistance = Harversine.distance(minCoordinates, centroidCoordinates); + final double maxCoordinatesDistance = Harversine.distance(maxCoordinates, centroidCoordinates); + + // Determine requiredTessellationDistance: the larger distance == the requiredTessellationDistance + final double requiredTessellationDistance; + if (minCoordinatesDistance >= maxCoordinatesDistance) { + requiredTessellationDistance = minCoordinatesDistance; + } else { + requiredTessellationDistance = maxCoordinatesDistance; + } + this.tessellationDistance = requiredTessellationDistance; /* * Neighbor's distance - distance between each hexagon's neighbor centroid: @@ -531,10 +547,10 @@ private final int calculateRequiredRings(Boundary boundary) { /* * In Hexagons grids, we can look at it with 3 primary axes (the 6 neighbor * directions): - * - minAxialHexagons is the minimum amount of hexagons that required to stack - * up (from edges) in those 3 axes to cover the grid map largest diameter. + * - requiredAxialHexagons is the minimum amount of hexagons that required to stack + * up (from edges) in those 3 axes to cover the boundary largest diameter. */ - final int requiredAxialHexagons = (int) Math.ceil(maxBoundaryDistance / neighborDistance); // round up + final int requiredAxialHexagons = (int) Math.ceil(requiredTessellationDistance / neighborDistance); // round up /* * Calculate the Minimum Required Rings @@ -598,33 +614,57 @@ private final void clearHexagons() { public static void main(String[] args) { Gson gson = new GsonBuilder().create(); - Coordinates origin = new Coordinates(106.7064, 10.7744); + Coordinates origin = new Coordinates(105.8121224, 21.0358791); + // Coordinates origin = new Coordinates(109.466667, 23.383333); - Hexagon hexagon = new Hexagon(origin, 200); + Hexagon hexagon = new Hexagon(origin, 500); Neighbors neighbors = new Neighbors(hexagon); AxialClockwiseTessellation tessellation = new AxialClockwiseTessellation(hexagon); Boundary boundary = new Boundary( - new Coordinates(106.6959, 10.7826), - new Coordinates(106.7064, 10.7743)); + new Coordinates(105, 21), + new Coordinates(109.466667, 23.383333)); // Test harversine double greatCircleDistance = Harversine.distance(boundary.getMinLat(), boundary.getMinLng(), boundary.getMaxLat(), boundary.getMaxLng()); tessellation.tessellate(boundary); - tessellation.tessellate(boundary); - System.out.println("Great-circle distance: " + greatCircleDistance); - System.out.println("Total rings: " + tessellation.totalRings); - System.out.println("Minimum required rings: " + tessellation.requiredRings); - System.out.println("Current nthRing: " + tessellation.nthRing); - System.out.println("Total hexagons: " + tessellation.totalHexagons); - System.out.println("Boundary: " + tessellation.boundary + "\n"); + // Set rounding format + DecimalFormat df = new DecimalFormat("#.##"); + df.setRoundingMode(RoundingMode.CEILING); + + final double oldCoverageDistance = 531312.330953638; + final int oldRequiredRings = 616; + final int oldTotalHexagons = 1136521; + + final String newCoverageDistanceDiffPct = df.format( + (tessellation.tessellationDistance / oldCoverageDistance - 1) * 100); + final String newRingsDiffPct = df.format( + (tessellation.requiredRings / (double) oldRequiredRings - 1) * 100); + final String newTotalHexagonsDiffPct = df.format( + (tessellation.totalHexagons / (double) oldTotalHexagons - 1) * 100); + + System.out.println("\n------------ Tessellation optimization stats ------------"); + System.out.println("Boundary: " + tessellation.boundary.gisBoundary()); + System.out.println("Hexagon's circumradius: " + hexagon.getCircumradius()); + + System.out.println("\nOld coverage Distance: " + oldCoverageDistance); + System.out.println("New Coverage Distance: " + tessellation.tessellationDistance); + System.out.println("*Difference percentage: " + newCoverageDistanceDiffPct + "%"); + + System.out.println("\nOld minimum required Rings: " + oldRequiredRings); + System.out.println("New minimum required Rings: " + tessellation.requiredRings); + System.out.println("*Difference percentage: " + newRingsDiffPct + "%"); + + System.out.println("\nOld total Hexagons: " + oldTotalHexagons); + System.out.println("New total hexagons: " + tessellation.totalHexagons); + System.out.println("*Difference percentage: " + newTotalHexagonsDiffPct + "%"); - GeoJsonManager tessellationManager = new GeoJsonManager(tessellation); - System.out.println( - gson.toJson(tessellationManager.getFeatureCollection())); + // GeoJsonManager tessellationManager = new GeoJsonManager(tessellation); + // System.out.println( + // gson.toJson(tessellationManager.getFeatureCollection())); } } \ No newline at end of file diff --git a/src/main/java/com/masterisehomes/geometryapi/tessellation/AxialClockwiseTessellationDto.java b/src/main/java/com/masterisehomes/geometryapi/tessellation/AxialClockwiseTessellationDto.java index 6201ffa..a246499 100644 --- a/src/main/java/com/masterisehomes/geometryapi/tessellation/AxialClockwiseTessellationDto.java +++ b/src/main/java/com/masterisehomes/geometryapi/tessellation/AxialClockwiseTessellationDto.java @@ -6,6 +6,11 @@ import lombok.Getter; import com.google.gson.JsonObject; +import com.google.gson.Gson; +import com.masterisehomes.geometryapi.neighbors.Neighbors; +import com.masterisehomes.geometryapi.geodesy.Harversine; +import com.masterisehomes.geometryapi.geojson.GeoJsonManager; + import com.masterisehomes.geometryapi.hexagon.Coordinates; import com.masterisehomes.geometryapi.hexagon.Hexagon; @@ -93,4 +98,35 @@ public AxialClockwiseTessellationDto(JsonObject payload) { this.totalRings = this.tessellation.getTotalRings(); this.totalHexagons = this.tessellation.getTotalHexagons(); } + + + public static void main(String[] args) { + Gson gson = new Gson(); + + Coordinates origin = new Coordinates(107, 23); + + Hexagon hexagon = new Hexagon(origin, 250); + Neighbors neighbors = new Neighbors(hexagon); + + AxialClockwiseTessellation tessellation = new AxialClockwiseTessellation(hexagon); + + Boundary boundary = new Boundary( + new Coordinates(106, 20), + new Coordinates(109.466667, 23.383333)); + + // Test harversine + double greatCircleDistance = Harversine.distance(boundary.getMinLat(), boundary.getMinLng(), + boundary.getMaxLat(), boundary.getMaxLng()); + + tessellation.tessellate(boundary); + + AxialClockwiseTessellationDto dto = new AxialClockwiseTessellationDto(tessellation); + + System.out.println("Great-circle distance: " + greatCircleDistance); + System.out.println("Total rings: " + dto.getTessellation().getTotalRings()); + System.out.println("Total hexagons: " + dto.getTessellation().getTotalHexagons()); + System.out.println("Boundary: " + dto.getTessellation().getBoundary() + "\n"); + + GeoJsonManager tessellationManager = new GeoJsonManager(tessellation); + } } diff --git a/src/main/java/com/masterisehomes/geometryapi/tessellation/Boundary.java b/src/main/java/com/masterisehomes/geometryapi/tessellation/Boundary.java index 1ea425f..988a9e5 100644 --- a/src/main/java/com/masterisehomes/geometryapi/tessellation/Boundary.java +++ b/src/main/java/com/masterisehomes/geometryapi/tessellation/Boundary.java @@ -3,6 +3,7 @@ import lombok.Getter; import lombok.ToString; +import com.masterisehomes.geometryapi.geodesy.Harversine; import com.masterisehomes.geometryapi.hexagon.Coordinates; /* Similar to setup() in Processing @@ -15,33 +16,53 @@ */ @ToString -@Getter public class Boundary { + @Getter + private final Coordinates minCoordinates, maxCoordinates; + // Processing attributes private int width, height; - private Coordinates start, end; // WGS84 Coordinates attributes - private double minLat, minLng; - private double maxLat, maxLng; + @Getter + private final double minLat, minLng; + @Getter + private final double maxLat, maxLng; // Builder pattern to take in dimension , public Boundary(float x, float y, int width, int height) { - this.start = new Coordinates(x, y); + this.minCoordinates = new Coordinates(x, y); this.width = width; this.height = height; - this.end = new Coordinates(x + width, y + height); + this.maxCoordinates = new Coordinates(x + width, y + height); + + this.minLat = minCoordinates.getLatitude(); + this.minLng = minCoordinates.getLongitude(); + this.maxLat = maxCoordinates.getLatitude(); + this.maxLng = maxCoordinates.getLongitude(); } // WGS84 Coordinates Boundary public Boundary(Coordinates minCoordinates, Coordinates maxCoordinates) { + this.minCoordinates = minCoordinates; + this.maxCoordinates = maxCoordinates; + this.minLat = minCoordinates.getLatitude(); this.minLng = minCoordinates.getLongitude(); this.maxLat = maxCoordinates.getLatitude(); this.maxLng = maxCoordinates.getLongitude(); } - // Comparison methods + /* Calculate Great-circle distance */ + public double greatCircleDistance() { + double greatCircleDistance = Harversine.distance( + minLat, minLng, + maxLat, maxLng); + + return greatCircleDistance; + } + + /* Comparison methods */ public boolean contains(Coordinates centroid) { double centroidLat = centroid.getLatitude(); double centroidLng = centroid.getLongitude(); @@ -69,4 +90,9 @@ private boolean containsLng(double lng) { return false; } } + + /* Getters */ + public String gisBoundary() { + return String.format("GisBoundary(minLat=%s, minLng=%s, maxLat=%s, maxLng=%s)", minLat, minLng, maxLat, maxLng); + } } \ No newline at end of file