From 6707024792fb0e56f8990efce9ccbcd179d9136d Mon Sep 17 00:00:00 2001 From: AMgr8 <153189709+AMgr8@users.noreply.github.com> Date: Sat, 25 Jul 2026 15:18:26 +0100 Subject: [PATCH 1/5] fixed wrong layout on verification.html --- src/main/resources/static/verification.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/static/verification.html b/src/main/resources/static/verification.html index c394e4c..d008cfc 100644 --- a/src/main/resources/static/verification.html +++ b/src/main/resources/static/verification.html @@ -22,7 +22,7 @@

BTUK.org

-
+

Account Initiation

Log onto btuk.org on Minecraft java edition to find your one time code in chat. This will expire in 15 minutes.

From d6affb09bcce669d7b753afc4242da81f1e853ad Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 6 Aug 2026 00:19:51 +0100 Subject: [PATCH 2/5] got some heatmap stuff done. More testing needed but should be finished --- .../maphub/Security/SecurityConfig.java | 3 +- .../{ => controllers}/AuthController.java | 11 +- .../controllers/BuildingController.java | 39 ++ .../maphub/controllers/MapController.java | 45 ++ .../{ => controllers}/UserController.java | 2 +- .../entities/{ => buildings}/Building.java | 2 +- .../entities/{ => buildings}/BuildingDTO.java | 2 +- .../{ => buildings}/BuildingGridItem.java | 6 +- .../{ => buildings}/BuildingGridRequest.java | 2 +- .../{ => buildings}/BuildingGridRespItem.java | 2 +- .../mapResponses/CloseUpResponse.java | 9 + .../mapResponses/HeatmapRespItem.java | 9 + .../mapResponses/OverviewResponse.java | 10 + .../entities/{ => otc}/NewOTCRequest.java | 2 +- .../{ => otc}/OTCResetPasswordRequest.java | 2 +- .../entities/{ => otc}/OneTimeCode.java | 2 +- .../{ => otc}/VerificationResponse.java | 2 +- .../{ => otc}/VerificationResult.java | 2 +- .../repositories/OneTimeCodeRepository.java | 2 +- .../BuildingService.java} | 58 +-- .../maphub/services/HeatmapService.java | 56 +++ .../example/maphub/services/OTCService.java | 9 +- .../maphub/services/ProxyAPIService.java | 4 +- .../example/maphub/services/UserService.java | 1 - src/main/resources/static/homePage.js | 185 +++---- src/main/resources/static/old.html | 455 ------------------ 26 files changed, 316 insertions(+), 606 deletions(-) rename src/main/java/com/example/maphub/{ => controllers}/AuthController.java (94%) create mode 100644 src/main/java/com/example/maphub/controllers/BuildingController.java create mode 100644 src/main/java/com/example/maphub/controllers/MapController.java rename src/main/java/com/example/maphub/{ => controllers}/UserController.java (98%) rename src/main/java/com/example/maphub/entities/{ => buildings}/Building.java (81%) rename src/main/java/com/example/maphub/entities/{ => buildings}/BuildingDTO.java (82%) rename src/main/java/com/example/maphub/entities/{ => buildings}/BuildingGridItem.java (56%) rename src/main/java/com/example/maphub/entities/{ => buildings}/BuildingGridRequest.java (76%) rename src/main/java/com/example/maphub/entities/{ => buildings}/BuildingGridRespItem.java (61%) create mode 100644 src/main/java/com/example/maphub/entities/mapResponses/CloseUpResponse.java create mode 100644 src/main/java/com/example/maphub/entities/mapResponses/HeatmapRespItem.java create mode 100644 src/main/java/com/example/maphub/entities/mapResponses/OverviewResponse.java rename src/main/java/com/example/maphub/entities/{ => otc}/NewOTCRequest.java (67%) rename src/main/java/com/example/maphub/entities/{ => otc}/OTCResetPasswordRequest.java (74%) rename src/main/java/com/example/maphub/entities/{ => otc}/OneTimeCode.java (95%) rename src/main/java/com/example/maphub/entities/{ => otc}/VerificationResponse.java (67%) rename src/main/java/com/example/maphub/entities/{ => otc}/VerificationResult.java (85%) rename src/main/java/com/example/maphub/{BuildingController.java => services/BuildingService.java} (73%) create mode 100644 src/main/java/com/example/maphub/services/HeatmapService.java delete mode 100644 src/main/resources/static/old.html diff --git a/src/main/java/com/example/maphub/Security/SecurityConfig.java b/src/main/java/com/example/maphub/Security/SecurityConfig.java index f1eb7a2..e518545 100644 --- a/src/main/java/com/example/maphub/Security/SecurityConfig.java +++ b/src/main/java/com/example/maphub/Security/SecurityConfig.java @@ -59,7 +59,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti "/images/**", "/favicon.ico", "/ForgotPassword.html", - "/building/**" + "/building/**", + "/map/**" ).permitAll() .requestMatchers("/user/me","/Profile.html").authenticated() .anyRequest().authenticated() diff --git a/src/main/java/com/example/maphub/AuthController.java b/src/main/java/com/example/maphub/controllers/AuthController.java similarity index 94% rename from src/main/java/com/example/maphub/AuthController.java rename to src/main/java/com/example/maphub/controllers/AuthController.java index f98ac65..70e591f 100644 --- a/src/main/java/com/example/maphub/AuthController.java +++ b/src/main/java/com/example/maphub/controllers/AuthController.java @@ -1,5 +1,9 @@ -package com.example.maphub; +package com.example.maphub.controllers; +import com.example.maphub.PasswordUtil; import com.example.maphub.entities.*; +import com.example.maphub.entities.otc.NewOTCRequest; +import com.example.maphub.entities.otc.OTCResetPasswordRequest; +import com.example.maphub.entities.otc.VerificationResponse; import com.example.maphub.services.OTCService; import com.example.maphub.services.PasswordValidationService; import com.example.maphub.services.ProxyAPIService; @@ -8,11 +12,8 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.validation.Errors; import org.springframework.web.bind.annotation.*; -import javax.swing.plaf.synth.SynthTabbedPaneUI; -import java.security.Principal; import java.util.List; import java.util.Map; @@ -81,7 +82,7 @@ public ResponseEntity register(@RequestBody LoginUser user) { return ResponseEntity.badRequest().body(passwordErrors); } userService.deleteByUuid(uuid); - userService.register(uuid,PasswordUtil.hash(user.password)); + userService.register(uuid, PasswordUtil.hash(user.password)); otcService.createOneTimeCode(uuid,"REGISTER");//should be a const in the future return ResponseEntity.ok("OTC Created"); } diff --git a/src/main/java/com/example/maphub/controllers/BuildingController.java b/src/main/java/com/example/maphub/controllers/BuildingController.java new file mode 100644 index 0000000..7c63365 --- /dev/null +++ b/src/main/java/com/example/maphub/controllers/BuildingController.java @@ -0,0 +1,39 @@ +package com.example.maphub.controllers; + +import com.example.maphub.entities.*; +import com.example.maphub.services.ProxyAPIService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.security.Principal; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.*; + +@RestController +@RequestMapping("/building") +public class BuildingController { + + final private ProxyAPIService proxyAPIService; + + public BuildingController(ProxyAPIService proxyAPIService){ + this.proxyAPIService = proxyAPIService; + } + @GetMapping("/total") + public int getTotalBuildings() { + int count = proxyAPIService.getBuildingCount(null,null,null,null,null,null,null); + return count; + } + @GetMapping("/playerCount") + public int getPlayerCount(Principal principal) { + if (principal == null) { + return 0; + } + String uuid = principal.getName(); + // Fetch buildings filtered specifically by the authenticated player's UUID + int count = proxyAPIService.getBuildingCount(List.of(uuid), null, null, null, null, null, true); + return count; + } + +} diff --git a/src/main/java/com/example/maphub/controllers/MapController.java b/src/main/java/com/example/maphub/controllers/MapController.java new file mode 100644 index 0000000..d64711b --- /dev/null +++ b/src/main/java/com/example/maphub/controllers/MapController.java @@ -0,0 +1,45 @@ +package com.example.maphub.controllers; + +import com.example.maphub.entities.MapBounds; +import com.example.maphub.entities.buildings.BuildingGridItem; +import com.example.maphub.entities.buildings.BuildingGridRequest; +import com.example.maphub.entities.mapResponses.CloseUpResponse; +import com.example.maphub.entities.mapResponses.OverviewResponse; +import com.example.maphub.services.BuildingService; +import com.example.maphub.services.HeatmapService; +import com.example.maphub.services.ProxyAPIService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.security.Principal; +import java.util.List; + +@RestController +@RequestMapping("/map") +public class MapController { + + private final BuildingService buildingService; + private final HeatmapService heatmapService; + public MapController(BuildingService buildingService, HeatmapService heatmapService){ + this.buildingService = buildingService; + this.heatmapService = heatmapService; + } + + @PostMapping("/overview") + public ResponseEntity getOverview(@RequestBody BuildingGridRequest req, Principal principal){ + List buildings = buildingService.getGridCount(req,principal); + OverviewResponse resp = new OverviewResponse(); + resp.buildings = buildingService.getGroupedGridCounts(buildings); + resp.heatmap = heatmapService.getHeatmap(buildings); + return ResponseEntity.ok(resp); + } + @PostMapping("/closeUp") + public ResponseEntity getCloseUp(@RequestBody MapBounds req, Principal principal){ + CloseUpResponse resp = new CloseUpResponse(); + resp.buildings = buildingService.getAllBuildings(req,principal); + return ResponseEntity.ok(resp); + } +} diff --git a/src/main/java/com/example/maphub/UserController.java b/src/main/java/com/example/maphub/controllers/UserController.java similarity index 98% rename from src/main/java/com/example/maphub/UserController.java rename to src/main/java/com/example/maphub/controllers/UserController.java index d529048..2b03cae 100644 --- a/src/main/java/com/example/maphub/UserController.java +++ b/src/main/java/com/example/maphub/controllers/UserController.java @@ -1,4 +1,4 @@ -package com.example.maphub; +package com.example.maphub.controllers; import com.example.maphub.entities.DeleteAccountRequest; import com.example.maphub.entities.PasswordChangeObject; import com.example.maphub.services.PasswordValidationService; diff --git a/src/main/java/com/example/maphub/entities/Building.java b/src/main/java/com/example/maphub/entities/buildings/Building.java similarity index 81% rename from src/main/java/com/example/maphub/entities/Building.java rename to src/main/java/com/example/maphub/entities/buildings/Building.java index ee50594..e707853 100644 --- a/src/main/java/com/example/maphub/entities/Building.java +++ b/src/main/java/com/example/maphub/entities/buildings/Building.java @@ -1,4 +1,4 @@ -package com.example.maphub.entities; +package com.example.maphub.entities.buildings; import java.time.LocalDateTime; diff --git a/src/main/java/com/example/maphub/entities/BuildingDTO.java b/src/main/java/com/example/maphub/entities/buildings/BuildingDTO.java similarity index 82% rename from src/main/java/com/example/maphub/entities/BuildingDTO.java rename to src/main/java/com/example/maphub/entities/buildings/BuildingDTO.java index 6a7750f..d496c8b 100644 --- a/src/main/java/com/example/maphub/entities/BuildingDTO.java +++ b/src/main/java/com/example/maphub/entities/buildings/BuildingDTO.java @@ -1,4 +1,4 @@ -package com.example.maphub.entities; +package com.example.maphub.entities.buildings; public record BuildingDTO( int buildingId, diff --git a/src/main/java/com/example/maphub/entities/BuildingGridItem.java b/src/main/java/com/example/maphub/entities/buildings/BuildingGridItem.java similarity index 56% rename from src/main/java/com/example/maphub/entities/BuildingGridItem.java rename to src/main/java/com/example/maphub/entities/buildings/BuildingGridItem.java index 4c31f05..d996e90 100644 --- a/src/main/java/com/example/maphub/entities/BuildingGridItem.java +++ b/src/main/java/com/example/maphub/entities/buildings/BuildingGridItem.java @@ -1,4 +1,4 @@ -package com.example.maphub.entities; +package com.example.maphub.entities.buildings; public class BuildingGridItem { public double lat; @@ -6,6 +6,10 @@ public class BuildingGridItem { public int count; public int row; public int col; + public double minLat; + public double minLon; + public double maxLat; + public double maxLon; public int count() { return count; diff --git a/src/main/java/com/example/maphub/entities/BuildingGridRequest.java b/src/main/java/com/example/maphub/entities/buildings/BuildingGridRequest.java similarity index 76% rename from src/main/java/com/example/maphub/entities/BuildingGridRequest.java rename to src/main/java/com/example/maphub/entities/buildings/BuildingGridRequest.java index 5986200..86b2b2a 100644 --- a/src/main/java/com/example/maphub/entities/BuildingGridRequest.java +++ b/src/main/java/com/example/maphub/entities/buildings/BuildingGridRequest.java @@ -1,4 +1,4 @@ -package com.example.maphub.entities; +package com.example.maphub.entities.buildings; public record BuildingGridRequest ( double minLat, diff --git a/src/main/java/com/example/maphub/entities/BuildingGridRespItem.java b/src/main/java/com/example/maphub/entities/buildings/BuildingGridRespItem.java similarity index 61% rename from src/main/java/com/example/maphub/entities/BuildingGridRespItem.java rename to src/main/java/com/example/maphub/entities/buildings/BuildingGridRespItem.java index a31d095..1a16e1b 100644 --- a/src/main/java/com/example/maphub/entities/BuildingGridRespItem.java +++ b/src/main/java/com/example/maphub/entities/buildings/BuildingGridRespItem.java @@ -1,4 +1,4 @@ -package com.example.maphub.entities; +package com.example.maphub.entities.buildings; public record BuildingGridRespItem(double lat, double lon, int count) {} diff --git a/src/main/java/com/example/maphub/entities/mapResponses/CloseUpResponse.java b/src/main/java/com/example/maphub/entities/mapResponses/CloseUpResponse.java new file mode 100644 index 0000000..cb9972d --- /dev/null +++ b/src/main/java/com/example/maphub/entities/mapResponses/CloseUpResponse.java @@ -0,0 +1,9 @@ +package com.example.maphub.entities.mapResponses; + +import com.example.maphub.entities.buildings.Building; + +import java.util.List; + +public class CloseUpResponse { + public List buildings; +} diff --git a/src/main/java/com/example/maphub/entities/mapResponses/HeatmapRespItem.java b/src/main/java/com/example/maphub/entities/mapResponses/HeatmapRespItem.java new file mode 100644 index 0000000..9365670 --- /dev/null +++ b/src/main/java/com/example/maphub/entities/mapResponses/HeatmapRespItem.java @@ -0,0 +1,9 @@ +package com.example.maphub.entities.mapResponses; + +public class HeatmapRespItem { + public double maxLat; + public double minLat; + public double maxLon; + public double minLon; + public int magnitude; +} diff --git a/src/main/java/com/example/maphub/entities/mapResponses/OverviewResponse.java b/src/main/java/com/example/maphub/entities/mapResponses/OverviewResponse.java new file mode 100644 index 0000000..8468c5d --- /dev/null +++ b/src/main/java/com/example/maphub/entities/mapResponses/OverviewResponse.java @@ -0,0 +1,10 @@ +package com.example.maphub.entities.mapResponses; + +import com.example.maphub.entities.buildings.BuildingGridRespItem; + +import java.util.List; + +public class OverviewResponse { + public List buildings; + public List heatmap; +} diff --git a/src/main/java/com/example/maphub/entities/NewOTCRequest.java b/src/main/java/com/example/maphub/entities/otc/NewOTCRequest.java similarity index 67% rename from src/main/java/com/example/maphub/entities/NewOTCRequest.java rename to src/main/java/com/example/maphub/entities/otc/NewOTCRequest.java index 8e5a0c9..eaa4a40 100644 --- a/src/main/java/com/example/maphub/entities/NewOTCRequest.java +++ b/src/main/java/com/example/maphub/entities/otc/NewOTCRequest.java @@ -1,4 +1,4 @@ -package com.example.maphub.entities; +package com.example.maphub.entities.otc; public class NewOTCRequest { public String purpose; diff --git a/src/main/java/com/example/maphub/entities/OTCResetPasswordRequest.java b/src/main/java/com/example/maphub/entities/otc/OTCResetPasswordRequest.java similarity index 74% rename from src/main/java/com/example/maphub/entities/OTCResetPasswordRequest.java rename to src/main/java/com/example/maphub/entities/otc/OTCResetPasswordRequest.java index 573780e..9f2b291 100644 --- a/src/main/java/com/example/maphub/entities/OTCResetPasswordRequest.java +++ b/src/main/java/com/example/maphub/entities/otc/OTCResetPasswordRequest.java @@ -1,4 +1,4 @@ -package com.example.maphub.entities; +package com.example.maphub.entities.otc; public class OTCResetPasswordRequest { public String username; diff --git a/src/main/java/com/example/maphub/entities/OneTimeCode.java b/src/main/java/com/example/maphub/entities/otc/OneTimeCode.java similarity index 95% rename from src/main/java/com/example/maphub/entities/OneTimeCode.java rename to src/main/java/com/example/maphub/entities/otc/OneTimeCode.java index 7d69a85..0f58477 100644 --- a/src/main/java/com/example/maphub/entities/OneTimeCode.java +++ b/src/main/java/com/example/maphub/entities/otc/OneTimeCode.java @@ -1,4 +1,4 @@ -package com.example.maphub.entities; +package com.example.maphub.entities.otc; import jakarta.persistence.*; diff --git a/src/main/java/com/example/maphub/entities/VerificationResponse.java b/src/main/java/com/example/maphub/entities/otc/VerificationResponse.java similarity index 67% rename from src/main/java/com/example/maphub/entities/VerificationResponse.java rename to src/main/java/com/example/maphub/entities/otc/VerificationResponse.java index 1c88156..e742d88 100644 --- a/src/main/java/com/example/maphub/entities/VerificationResponse.java +++ b/src/main/java/com/example/maphub/entities/otc/VerificationResponse.java @@ -1,4 +1,4 @@ -package com.example.maphub.entities; +package com.example.maphub.entities.otc; public class VerificationResponse { public String username; diff --git a/src/main/java/com/example/maphub/entities/VerificationResult.java b/src/main/java/com/example/maphub/entities/otc/VerificationResult.java similarity index 85% rename from src/main/java/com/example/maphub/entities/VerificationResult.java rename to src/main/java/com/example/maphub/entities/otc/VerificationResult.java index baeb54c..f7dc382 100644 --- a/src/main/java/com/example/maphub/entities/VerificationResult.java +++ b/src/main/java/com/example/maphub/entities/otc/VerificationResult.java @@ -1,4 +1,4 @@ -package com.example.maphub.entities; +package com.example.maphub.entities.otc; public class VerificationResult { public String username; diff --git a/src/main/java/com/example/maphub/repositories/OneTimeCodeRepository.java b/src/main/java/com/example/maphub/repositories/OneTimeCodeRepository.java index df98231..d54198a 100644 --- a/src/main/java/com/example/maphub/repositories/OneTimeCodeRepository.java +++ b/src/main/java/com/example/maphub/repositories/OneTimeCodeRepository.java @@ -1,6 +1,6 @@ package com.example.maphub.repositories; -import com.example.maphub.entities.OneTimeCode; +import com.example.maphub.entities.otc.OneTimeCode; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; diff --git a/src/main/java/com/example/maphub/BuildingController.java b/src/main/java/com/example/maphub/services/BuildingService.java similarity index 73% rename from src/main/java/com/example/maphub/BuildingController.java rename to src/main/java/com/example/maphub/services/BuildingService.java index 2671134..adb0564 100644 --- a/src/main/java/com/example/maphub/BuildingController.java +++ b/src/main/java/com/example/maphub/services/BuildingService.java @@ -1,10 +1,9 @@ -package com.example.maphub; +package com.example.maphub.services; import com.example.maphub.entities.*; -import com.example.maphub.services.ProxyAPIService; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.Authentication; -import org.springframework.web.bind.annotation.*; +import com.example.maphub.entities.buildings.*; +import org.springframework.stereotype.Service; +import org.springframework.web.bind.annotation.RequestBody; import java.security.Principal; import java.time.Instant; @@ -12,18 +11,14 @@ import java.time.ZoneOffset; import java.util.*; -@RestController -@RequestMapping("/building") -public class BuildingController { - - final private ProxyAPIService proxyAPIService; - - public BuildingController(ProxyAPIService proxyAPIService){ +@Service +public class BuildingService { + private final ProxyAPIService proxyAPIService; + public BuildingService(ProxyAPIService proxyAPIService){ this.proxyAPIService = proxyAPIService; } - @PostMapping("/allBuildings") - public ResponseEntity getAllBuildings(@RequestBody MapBounds req, Principal principal){ + public List getAllBuildings(@RequestBody MapBounds req, Principal principal){ String uuid = ""; if (principal == null){ uuid = null; @@ -52,7 +47,7 @@ else if (!uuid.equals(b.playerId())){ } out.add(new Building(b.buildingId(),uuidDisplay, b.isPublic(), b.playerBuilt(),timeAdded ,b.lat(),b.lon(),colour,username)); } - return ResponseEntity.ok(out); + return out; } private LocalDateTime parseTimestamp(String timestampStr) { @@ -67,20 +62,19 @@ private LocalDateTime parseTimestamp(String timestampStr) { } } - @PostMapping("/gridCount") - public ResponseEntity getGridCount(@RequestBody BuildingGridRequest req, Principal principal) { + public List getGridCount(BuildingGridRequest req, Principal principal) { String uuid = ""; - if (principal == null){ + if (principal == null) { uuid = null; - } - else{ + } else { uuid = principal.getName(); } - ProxyAPIService.BuildingGridResponseDTO resp = proxyAPIService.getBuildingGridCount(req.minLat(), req.maxLat(), req.minLon(), req.maxLon(), req.stepLat(), req.stepLon(),uuid); - + return proxyAPIService.getBuildingGridCount(req.minLat(), req.maxLat(), req.minLon(), req.maxLon(), req.stepLat(), req.stepLon(), uuid).cells(); + } + public List getGroupedGridCounts(List original){ Map gridMap = new HashMap<>(); - for (BuildingGridItem dto : resp.cells()) { + for (BuildingGridItem dto : original) { if (dto.count() > 0) { gridMap.put(new xy(dto.row, dto.col), dto); } @@ -141,25 +135,11 @@ public ResponseEntity getGridCount(@RequestBody BuildingGridRequest req, Prin .map(cell -> new BuildingGridRespItem(cell.lat, cell.lon, cell.count)) .toList(); - return ResponseEntity.ok(out); + return out; } private record xy (int x, int y){} - @PostMapping("/buildingCountTotal") - public ResponseEntity getTotalBuildings() { - int count = proxyAPIService.getBuildingCount(null,null,null,null,null,null,null); - return ResponseEntity.ok(Map.of("count", count)); - } - @PostMapping("/buildingPersonalCount") - public ResponseEntity getPlayerCount(Principal principal) { - if (principal == null) { - return ResponseEntity.ok(Map.of("count", 0)); - } - String uuid = principal.getName(); - // Fetch buildings filtered specifically by the authenticated player's UUID - int count = proxyAPIService.getBuildingCount(List.of(uuid), null, null, null, null, null, true); - return ResponseEntity.ok(Map.of("count", count)); - } + } diff --git a/src/main/java/com/example/maphub/services/HeatmapService.java b/src/main/java/com/example/maphub/services/HeatmapService.java new file mode 100644 index 0000000..d651057 --- /dev/null +++ b/src/main/java/com/example/maphub/services/HeatmapService.java @@ -0,0 +1,56 @@ +package com.example.maphub.services; + +import com.example.maphub.entities.buildings.BuildingGridItem; +import com.example.maphub.entities.mapResponses.HeatmapRespItem; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +@Service +public class HeatmapService { + + public List getHeatmap(List buildings){ + List out = new ArrayList<>(); + if (buildings.isEmpty()) + { + return out; + } + double area = calculateAreaWithLatLon(buildings.getFirst()); + for (BuildingGridItem b : buildings){ + HeatmapRespItem h = new HeatmapRespItem(); + h.maxLat = b.maxLat; + h.maxLon = b.maxLon; + h.minLat = b.minLat; + h.minLon = b.minLon; + //density of buildings /100m^2 + h.magnitude = (int) Math.round(((double) b.count * 10000.0) / area); + out.add(h); + } + return out; + } + public double calculateAreaWithLatLon(BuildingGridItem b) { + if (b == null) { + return 0.0; + } + + final double EARTH_RADIUS_METERS = 6371000.0; + + double minLatRad = Math.toRadians(b.minLat); + double maxLatRad = Math.toRadians(b.maxLat); + double minLonRad = Math.toRadians(b.minLon); + double maxLonRad = Math.toRadians(b.maxLon); + + // Height along latitude (North-South) + double deltaLat = maxLatRad - minLatRad; + double height = deltaLat * EARTH_RADIUS_METERS; + + // Mid-latitude cosine adjustment for longitude (East-West) + double avgLatRad = (minLatRad + maxLatRad) / 2.0; + double deltaLon = maxLonRad - minLonRad; + double width = deltaLon * EARTH_RADIUS_METERS * Math.cos(avgLatRad); + + return Math.abs(width * height); // Area in square meters (m²) + } +} + diff --git a/src/main/java/com/example/maphub/services/OTCService.java b/src/main/java/com/example/maphub/services/OTCService.java index 73022ef..c63edd6 100644 --- a/src/main/java/com/example/maphub/services/OTCService.java +++ b/src/main/java/com/example/maphub/services/OTCService.java @@ -1,19 +1,12 @@ package com.example.maphub.services; -import com.example.maphub.PasswordUtil; -import com.example.maphub.entities.OneTimeCode; -import com.example.maphub.entities.User; -import com.example.maphub.entities.VerificationResult; +import com.example.maphub.entities.otc.OneTimeCode; import com.example.maphub.repositories.OneTimeCodeRepository; -import com.example.maphub.repositories.UserRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.security.SecureRandom; import java.time.LocalDateTime; -import java.time.temporal.ChronoUnit; -import java.util.Optional; -import java.util.Random; @Service @Transactional diff --git a/src/main/java/com/example/maphub/services/ProxyAPIService.java b/src/main/java/com/example/maphub/services/ProxyAPIService.java index 415b7fa..9a57c55 100644 --- a/src/main/java/com/example/maphub/services/ProxyAPIService.java +++ b/src/main/java/com/example/maphub/services/ProxyAPIService.java @@ -2,8 +2,8 @@ -import com.example.maphub.entities.BuildingDTO; -import com.example.maphub.entities.BuildingGridItem; +import com.example.maphub.entities.buildings.BuildingDTO; +import com.example.maphub.entities.buildings.BuildingGridItem; import com.fasterxml.jackson.core.JsonProcessingException; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.ParameterizedTypeReference; diff --git a/src/main/java/com/example/maphub/services/UserService.java b/src/main/java/com/example/maphub/services/UserService.java index 9debaec..3515528 100644 --- a/src/main/java/com/example/maphub/services/UserService.java +++ b/src/main/java/com/example/maphub/services/UserService.java @@ -2,7 +2,6 @@ import com.example.maphub.PasswordUtil; import com.example.maphub.entities.User; -import com.example.maphub.entities.VerificationResult; import com.example.maphub.repositories.UserRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; diff --git a/src/main/resources/static/homePage.js b/src/main/resources/static/homePage.js index 023609e..2997fe1 100644 --- a/src/main/resources/static/homePage.js +++ b/src/main/resources/static/homePage.js @@ -14,9 +14,11 @@ var loggedIn = false; var username = "" var buildingSelected = false; - +const redHeatmapColours = ['#fff5f0','#fee0d2','#fcbba1','#fc9272','#fb6a4a','#ef3b2c','#cb181d','#a50f15','#67000d'] +var heatmapColours = redHeatmapColours const gridMarkerGroup = L.layerGroup().addTo(map); const buildingMarkerGroup = L.layerGroup().addTo(map); +const heatMapGroup = L.layerGroup().addTo(map); var buildings = [] const stepIcon = L.icon({ @@ -105,47 +107,111 @@ map.on("moveend", () => { map.on("click",() => {if (buildingSelected){displayWelcomeBox(); buildingSelected = false}}) -function updateLayers(){ - if (map.getZoom() >= 17){ - gridMarkerGroup.clearLayers() - updateBuildingsInView() - }else - { - buildingMarkerGroup.clearLayers() - updateMarkerGroupCounts(); +async function updateLayers() { + if (map.getZoom() >= 17) { + gridMarkerGroup.clearLayers(); + heatMapGroup.clearLayers(); + const dto = await getCloseUp(); + if (dto && dto.buildings) { + updateBuildingsInView(dto.buildings); + } + } else { + buildingMarkerGroup.clearLayers(); + const dto = await getOverview(); + if (dto) { + if (dto.buildings) { + updateMarkerGroupCounts(dto.buildings); + } + if (dto.heatmap) { + updateHeatmapView(dto.heatmap); + } + } } } +async function getOverview() { + const bounds = map.getBounds(); + const zoom = map.getZoom(); + const { latStep, lonStep } = getGridStepSizes(zoom, map.getCenter().lat); + const requestData = { + minLat: bounds.getSouth(), + minLon: bounds.getWest(), + maxLat: bounds.getNorth(), + maxLon: bounds.getEast(), + stepLat: latStep, + stepLon: lonStep + }; + try { + const response = await fetch("/map/overview", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(requestData) + }); -async function updateBuildingsInView(){ - const bounds = map.getBounds(); + if (!response.ok) throw new Error("Failed to fetch overview data"); + return await response.json(); + } catch (error) { + console.error("Error fetching overview:", error); + return null; + } +} - const minLat = bounds.getSouth(); - const maxLat = bounds.getNorth(); - const minLon = bounds.getWest(); - const maxLon = bounds.getEast(); +async function getCloseUp() { + const bounds = map.getBounds(); const requestData = { - minLat: minLat, - minLon: minLon, - maxLat: maxLat, - maxLon: maxLon, + minLat: bounds.getSouth(), + minLon: bounds.getWest(), + maxLat: bounds.getNorth(), + maxLon: bounds.getEast() }; + try { - const response = await fetch("/building/allBuildings", { + const response = await fetch("/map/closeUp", { method: "POST", - headers: { - "Content-Type": "application/json" - }, + headers: { "Content-Type": "application/json" }, body: JSON.stringify(requestData) }); - if (!response.ok) throw new Error("Failed to fetch grid counts"); + if (!response.ok) throw new Error("Failed to fetch close-up data"); + return await response.json(); + } catch (error) { + console.error("Error fetching closeUp:", error); + return null; + } +} - const data = await response.json(); +function updateHeatmapView(heatmapItems) { + heatMapGroup.clearLayers(); + + heatmapItems.forEach(item => { + // Construct bounding rectangle coordinates: [[south, west], [north, east]] + const bounds = [ + [item.minLat, item.minLon], + [item.maxLat, item.maxLon] + ]; + + const color = getColour(item.magnitude); + + const rectangle = L.rectangle(bounds, { + color: color, + weight: 0, // No border + fillColor: color, + fillOpacity: 0.7 // Semi-transparent overlay + }); + + heatMapGroup.addLayer(rectangle); + }); +} - buildings = data; +function getColour(magnitude) { + // Map non-negative integer magnitudes to color ramp indices (0 to 8) + const index = Math.min(Math.max(0, magnitude), redHeatmapColours.length - 1); + return redHeatmapColours[index]; +} + +function updateBuildingsInView(buildings){ buildingMarkerGroup.clearLayers(); buildings.forEach(building => { @@ -174,13 +240,6 @@ async function updateBuildingsInView(){ // Add marker to layer group buildingMarkerGroup.addLayer(marker); }); - - - } catch (error) { - console.error("Error fetching buildings", error); - } - - } function onBuildingClick(buildingData) { @@ -188,56 +247,16 @@ function onBuildingClick(buildingData) { displayBuildingBox(buildingData) } -async function updateMarkerGroupCounts() { - const bounds = map.getBounds(); - const zoom = map.getZoom(); - - const minLat = bounds.getSouth(); - const maxLat = bounds.getNorth(); - const minLon = bounds.getWest(); - const maxLon = bounds.getEast(); - - // Calculate fixed world grid steps based on current zoom level - const { latStep, lonStep } = getGridStepSizes(zoom, map.getCenter().lat); - - const requestData = { - minLat: minLat, - minLon: minLon, - maxLat: maxLat, - maxLon: maxLon, - stepLat: latStep, - stepLon: lonStep - }; +function updateMarkerGroupCounts(buildingGridItems) { + gridMarkerGroup.clearLayers(); - try { - const response = await fetch("/building/gridCount", { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify(requestData) - }); - - if (!response.ok) throw new Error("Failed to fetch grid counts"); - - const data = await response.json(); - - // 1. Clear previous markers from the map - gridMarkerGroup.clearLayers(); - - // 2. Render new markers - data.forEach(item => { - if (item.count > 0) { - const markerMessage = getGroupIconMessage(item.count); - const marker = L.marker([item.lat, item.lon], { icon: stepIcon }); - - addGroupMarkerToMap(marker, markerMessage); - } - }); - - } catch (error) { - console.error("Error loading grid counts:", error); - } + buildingGridItems.forEach(item => { + if (item.count > 0) { + const markerMessage = getGroupIconMessage(item.count); + const marker = L.marker([item.lat, item.lon], { icon: stepIcon }); + addGroupMarkerToMap(marker, markerMessage); + } + }); } function getGridStepSizes(zoom, centerLat) { @@ -369,7 +388,7 @@ const statProgress = document.getElementById("statProgress"); async function loadServerStats() { try { - const response = await fetch("/building/buildingCountTotal", { + const response = await fetch("/building/total", { method: "POST" }); diff --git a/src/main/resources/static/old.html b/src/main/resources/static/old.html deleted file mode 100644 index 9bf357c..0000000 --- a/src/main/resources/static/old.html +++ /dev/null @@ -1,455 +0,0 @@ - - - - - - - - - - - Title - - - - - -
- - -
- -
-
- -
- - - - - - - - - \ No newline at end of file From fc9488f32bae77506768ea49dc5b05b7d52e28df Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 9 Aug 2026 23:04:01 +0100 Subject: [PATCH 3/5] updated heatmap formula, fixed stats --- .../controllers/BuildingController.java | 10 ++--- .../maphub/controllers/MapController.java | 6 ++- .../maphub/services/HeatmapService.java | 41 +++++++++++++++---- src/main/resources/static/Profile.js | 4 +- src/main/resources/static/homePage.js | 10 +---- 5 files changed, 46 insertions(+), 25 deletions(-) diff --git a/src/main/java/com/example/maphub/controllers/BuildingController.java b/src/main/java/com/example/maphub/controllers/BuildingController.java index 7c63365..a4c07ca 100644 --- a/src/main/java/com/example/maphub/controllers/BuildingController.java +++ b/src/main/java/com/example/maphub/controllers/BuildingController.java @@ -21,19 +21,19 @@ public BuildingController(ProxyAPIService proxyAPIService){ this.proxyAPIService = proxyAPIService; } @GetMapping("/total") - public int getTotalBuildings() { + public ResponseEntity getTotalBuildings() { int count = proxyAPIService.getBuildingCount(null,null,null,null,null,null,null); - return count; + return ResponseEntity.ok(Map.of("count", count)); } @GetMapping("/playerCount") - public int getPlayerCount(Principal principal) { + public ResponseEntity getPlayerCount(Principal principal) { if (principal == null) { - return 0; + return ResponseEntity.status(401).body("Not logged in"); } String uuid = principal.getName(); // Fetch buildings filtered specifically by the authenticated player's UUID int count = proxyAPIService.getBuildingCount(List.of(uuid), null, null, null, null, null, true); - return count; + return ResponseEntity.ok(Map.of("count", count)); } } diff --git a/src/main/java/com/example/maphub/controllers/MapController.java b/src/main/java/com/example/maphub/controllers/MapController.java index d64711b..9b35bf5 100644 --- a/src/main/java/com/example/maphub/controllers/MapController.java +++ b/src/main/java/com/example/maphub/controllers/MapController.java @@ -23,9 +23,11 @@ public class MapController { private final BuildingService buildingService; private final HeatmapService heatmapService; - public MapController(BuildingService buildingService, HeatmapService heatmapService){ + private final ProxyAPIService proxyAPIService; + public MapController(BuildingService buildingService, HeatmapService heatmapService, ProxyAPIService proxyAPIService){ this.buildingService = buildingService; this.heatmapService = heatmapService; + this.proxyAPIService = proxyAPIService; } @PostMapping("/overview") @@ -33,7 +35,7 @@ public ResponseEntity getOverview(@RequestBody BuildingGridRequest req, Princ List buildings = buildingService.getGridCount(req,principal); OverviewResponse resp = new OverviewResponse(); resp.buildings = buildingService.getGroupedGridCounts(buildings); - resp.heatmap = heatmapService.getHeatmap(buildings); + resp.heatmap = heatmapService.getHeatmap(buildings, proxyAPIService.getBuildingCount(null,null,null,null,null,null,null)); return ResponseEntity.ok(resp); } @PostMapping("/closeUp") diff --git a/src/main/java/com/example/maphub/services/HeatmapService.java b/src/main/java/com/example/maphub/services/HeatmapService.java index d651057..7ab4670 100644 --- a/src/main/java/com/example/maphub/services/HeatmapService.java +++ b/src/main/java/com/example/maphub/services/HeatmapService.java @@ -10,23 +10,48 @@ @Service public class HeatmapService { - public List getHeatmap(List buildings){ + // Maximum density at high zoom: + // 10 buildings / 10,000 m² + private static final double MAX_DENSITY = 10.0 / 10_000.0; + + // At large scales, 20% of all buildings = maximum + private static final double MAX_BUILDING_SHARE = 0.20; + + // Controls where the transition from density -> total progress occurs. + // 5 km² is a reasonable starting point. + private static final double TRANSITION_AREA = 20_000_000.0; + + public List getHeatmap(List buildings, int totalBuildings) { List out = new ArrayList<>(); - if (buildings.isEmpty()) - { - return out; + + if (buildings.isEmpty() || totalBuildings <= 0) { + return out; } - double area = calculateAreaWithLatLon(buildings.getFirst()); - for (BuildingGridItem b : buildings){ + for (BuildingGridItem b : buildings) { + double area = calculateAreaWithLatLon(b); + if (area <= 0) { + continue; + } + double buildingCount = b.count; + double density = buildingCount / area; + double densityWeight = Math.exp(-area / TRANSITION_AREA); + double densityScore = density / MAX_DENSITY; + double buildingShare = buildingCount / (double) totalBuildings; + double progressScore = buildingShare / MAX_BUILDING_SHARE; + double score = densityWeight * densityScore + (1.0 - densityWeight) * progressScore; + + //score between 0 and 1 + score = Math.min(score, 1.0); + int magnitude = (int) (Math.sqrt(score) * 8.99); HeatmapRespItem h = new HeatmapRespItem(); h.maxLat = b.maxLat; h.maxLon = b.maxLon; h.minLat = b.minLat; h.minLon = b.minLon; - //density of buildings /100m^2 - h.magnitude = (int) Math.round(((double) b.count * 10000.0) / area); + h.magnitude = magnitude; out.add(h); } + return out; } public double calculateAreaWithLatLon(BuildingGridItem b) { diff --git a/src/main/resources/static/Profile.js b/src/main/resources/static/Profile.js index a726f7b..8f5e8a9 100644 --- a/src/main/resources/static/Profile.js +++ b/src/main/resources/static/Profile.js @@ -82,8 +82,8 @@ window.addEventListener("load", () => { window.location.assign("login.html"); }); - fetch("/building/buildingPersonalCount", { - method: "POST", + fetch("/building/playerCount", { + method: "GET", credentials: "include" }) .then(res => { diff --git a/src/main/resources/static/homePage.js b/src/main/resources/static/homePage.js index 2997fe1..17096ec 100644 --- a/src/main/resources/static/homePage.js +++ b/src/main/resources/static/homePage.js @@ -192,7 +192,7 @@ function updateHeatmapView(heatmapItems) { [item.maxLat, item.maxLon] ]; - const color = getColour(item.magnitude); + const color = heatmapColours[item.magnitude]; const rectangle = L.rectangle(bounds, { color: color, @@ -205,12 +205,6 @@ function updateHeatmapView(heatmapItems) { }); } -function getColour(magnitude) { - // Map non-negative integer magnitudes to color ramp indices (0 to 8) - const index = Math.min(Math.max(0, magnitude), redHeatmapColours.length - 1); - return redHeatmapColours[index]; -} - function updateBuildingsInView(buildings){ buildingMarkerGroup.clearLayers(); @@ -389,7 +383,7 @@ const statProgress = document.getElementById("statProgress"); async function loadServerStats() { try { const response = await fetch("/building/total", { - method: "POST" + method: "GET" }); if (!response.ok) throw new Error("Failed to fetch server stats"); From 8be0518f4218287c5df04bb81a115f68c2aeb913 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 11 Aug 2026 19:40:30 +0100 Subject: [PATCH 4/5] adde workspace.xml to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 4353cc8..6c04543 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ target/ ### IntelliJ IDEA ### .idea/modules.xml +.idea/workspace.xml .idea/jarRepositories.xml .idea/compiler.xml .idea/libraries/ From ba8cb3309b99e4c46320d18d80331b93faadc358 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 20 Aug 2026 23:22:38 +0100 Subject: [PATCH 5/5] added better stats and fixed a couple of other issues --- src/main/java/com/example/maphub/Consts.java | 9 ++ .../maphub/Security/SecurityConfig.java | 4 +- .../controllers/BuildingController.java | 39 ------- .../maphub/controllers/MapController.java | 2 +- .../maphub/controllers/StatsController.java | 35 ++++++ .../maphub/entities/stats/HomeStatsResp.java | 11 ++ .../entities/stats/PlayerBaseStats.java | 11 ++ .../entities/stats/ProfileStatsResp.java | 10 ++ .../maphub/entities/stats/StatsFilters.java | 10 ++ .../maphub/entities/stats/TotalBaseStats.java | 7 ++ .../maphub/services/BuildingService.java | 17 +++ .../maphub/services/ProxyAPIService.java | 42 +++++++- .../example/maphub/services/StatsService.java | 100 ++++++++++++++++++ src/main/resources/application.properties | 2 +- src/main/resources/static/Profile.html | 8 +- src/main/resources/static/Profile.js | 20 +++- src/main/resources/static/StatsUtils.js | 7 ++ src/main/resources/static/homePage.js | 65 +++++++----- src/main/resources/static/index.html | 14 +-- src/main/resources/static/style.css | 1 + 20 files changed, 324 insertions(+), 90 deletions(-) create mode 100644 src/main/java/com/example/maphub/Consts.java delete mode 100644 src/main/java/com/example/maphub/controllers/BuildingController.java create mode 100644 src/main/java/com/example/maphub/controllers/StatsController.java create mode 100644 src/main/java/com/example/maphub/entities/stats/HomeStatsResp.java create mode 100644 src/main/java/com/example/maphub/entities/stats/PlayerBaseStats.java create mode 100644 src/main/java/com/example/maphub/entities/stats/ProfileStatsResp.java create mode 100644 src/main/java/com/example/maphub/entities/stats/StatsFilters.java create mode 100644 src/main/java/com/example/maphub/entities/stats/TotalBaseStats.java create mode 100644 src/main/java/com/example/maphub/services/StatsService.java create mode 100644 src/main/resources/static/StatsUtils.js diff --git a/src/main/java/com/example/maphub/Consts.java b/src/main/java/com/example/maphub/Consts.java new file mode 100644 index 0000000..5f9fa0e --- /dev/null +++ b/src/main/java/com/example/maphub/Consts.java @@ -0,0 +1,9 @@ +package com.example.maphub; + +import java.time.LocalDateTime; + +public class Consts { + public static final LocalDateTime SERVER_START_TIME = LocalDateTime.of(2020, 3,29,22,37); + public static final int TOTAL_UK_BUILDINGS = 30000000; + public static final int MILLIS_IN_DAY = 86400000; +} diff --git a/src/main/java/com/example/maphub/Security/SecurityConfig.java b/src/main/java/com/example/maphub/Security/SecurityConfig.java index e518545..b489cad 100644 --- a/src/main/java/com/example/maphub/Security/SecurityConfig.java +++ b/src/main/java/com/example/maphub/Security/SecurityConfig.java @@ -60,7 +60,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti "/favicon.ico", "/ForgotPassword.html", "/building/**", - "/map/**" + "/map/**", + "/stats/homePage/**", + "/stats/homePage" ).permitAll() .requestMatchers("/user/me","/Profile.html").authenticated() .anyRequest().authenticated() diff --git a/src/main/java/com/example/maphub/controllers/BuildingController.java b/src/main/java/com/example/maphub/controllers/BuildingController.java deleted file mode 100644 index a4c07ca..0000000 --- a/src/main/java/com/example/maphub/controllers/BuildingController.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.example.maphub.controllers; - -import com.example.maphub.entities.*; -import com.example.maphub.services.ProxyAPIService; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.security.Principal; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneOffset; -import java.util.*; - -@RestController -@RequestMapping("/building") -public class BuildingController { - - final private ProxyAPIService proxyAPIService; - - public BuildingController(ProxyAPIService proxyAPIService){ - this.proxyAPIService = proxyAPIService; - } - @GetMapping("/total") - public ResponseEntity getTotalBuildings() { - int count = proxyAPIService.getBuildingCount(null,null,null,null,null,null,null); - return ResponseEntity.ok(Map.of("count", count)); - } - @GetMapping("/playerCount") - public ResponseEntity getPlayerCount(Principal principal) { - if (principal == null) { - return ResponseEntity.status(401).body("Not logged in"); - } - String uuid = principal.getName(); - // Fetch buildings filtered specifically by the authenticated player's UUID - int count = proxyAPIService.getBuildingCount(List.of(uuid), null, null, null, null, null, true); - return ResponseEntity.ok(Map.of("count", count)); - } - -} diff --git a/src/main/java/com/example/maphub/controllers/MapController.java b/src/main/java/com/example/maphub/controllers/MapController.java index 9b35bf5..e88add8 100644 --- a/src/main/java/com/example/maphub/controllers/MapController.java +++ b/src/main/java/com/example/maphub/controllers/MapController.java @@ -34,8 +34,8 @@ public MapController(BuildingService buildingService, HeatmapService heatmapServ public ResponseEntity getOverview(@RequestBody BuildingGridRequest req, Principal principal){ List buildings = buildingService.getGridCount(req,principal); OverviewResponse resp = new OverviewResponse(); + resp.heatmap = heatmapService.getHeatmap(buildings, buildingService.getTotalCount()); resp.buildings = buildingService.getGroupedGridCounts(buildings); - resp.heatmap = heatmapService.getHeatmap(buildings, proxyAPIService.getBuildingCount(null,null,null,null,null,null,null)); return ResponseEntity.ok(resp); } @PostMapping("/closeUp") diff --git a/src/main/java/com/example/maphub/controllers/StatsController.java b/src/main/java/com/example/maphub/controllers/StatsController.java new file mode 100644 index 0000000..9fc9c3c --- /dev/null +++ b/src/main/java/com/example/maphub/controllers/StatsController.java @@ -0,0 +1,35 @@ +package com.example.maphub.controllers; + + +import com.example.maphub.entities.stats.HomeStatsResp; +import com.example.maphub.entities.stats.StatsFilters; +import com.example.maphub.services.StatsService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.security.Principal; + +@RestController +@RequestMapping("/stats") +public class StatsController { + + private final StatsService statsService; + public StatsController(StatsService statsService){ + this.statsService = statsService; + } + @GetMapping("/homePage") + public ResponseEntity getHomePageStats(){ + HomeStatsResp resp = statsService.getHomepageStats(); + return ResponseEntity.ok(resp); + } + @GetMapping("/profile") + public ResponseEntity getProfileStats(Principal principal){ + if (principal == null){ + return ResponseEntity.status(401).body("User not logged in"); + } + return ResponseEntity.ok(statsService.getProfileStats(principal.getName())); + } +} diff --git a/src/main/java/com/example/maphub/entities/stats/HomeStatsResp.java b/src/main/java/com/example/maphub/entities/stats/HomeStatsResp.java new file mode 100644 index 0000000..c6a6b66 --- /dev/null +++ b/src/main/java/com/example/maphub/entities/stats/HomeStatsResp.java @@ -0,0 +1,11 @@ +package com.example.maphub.entities.stats; + +import java.time.LocalDateTime; + +public class HomeStatsResp { + public int buildings; + public double percentage; + public int buildingsRecent; //buildings in the last month? + public LocalDateTime estimatedFinishDate; + public int previousRecentBuildings; +} diff --git a/src/main/java/com/example/maphub/entities/stats/PlayerBaseStats.java b/src/main/java/com/example/maphub/entities/stats/PlayerBaseStats.java new file mode 100644 index 0000000..a3d2e86 --- /dev/null +++ b/src/main/java/com/example/maphub/entities/stats/PlayerBaseStats.java @@ -0,0 +1,11 @@ +package com.example.maphub.entities.stats; + +public class PlayerBaseStats { + public int buildings; + public int tplls; + public int timePlayed; + public int messagesSent; + public int reviewsCompleted; + + +} diff --git a/src/main/java/com/example/maphub/entities/stats/ProfileStatsResp.java b/src/main/java/com/example/maphub/entities/stats/ProfileStatsResp.java new file mode 100644 index 0000000..ed546ea --- /dev/null +++ b/src/main/java/com/example/maphub/entities/stats/ProfileStatsResp.java @@ -0,0 +1,10 @@ +package com.example.maphub.entities.stats; + +public class ProfileStatsResp { + public int buildings; + public int tplls; + public float timeNonAFK; + public String productivity; + public int messagesSent; + public int reviewsCompleted; +} diff --git a/src/main/java/com/example/maphub/entities/stats/StatsFilters.java b/src/main/java/com/example/maphub/entities/stats/StatsFilters.java new file mode 100644 index 0000000..7336b43 --- /dev/null +++ b/src/main/java/com/example/maphub/entities/stats/StatsFilters.java @@ -0,0 +1,10 @@ +package com.example.maphub.entities.stats; + +import java.time.LocalDateTime; +import java.util.List; + +public class StatsFilters { + public List playeruuid; + public LocalDateTime timeStart; + public LocalDateTime timeEnd; +} diff --git a/src/main/java/com/example/maphub/entities/stats/TotalBaseStats.java b/src/main/java/com/example/maphub/entities/stats/TotalBaseStats.java new file mode 100644 index 0000000..0cda71f --- /dev/null +++ b/src/main/java/com/example/maphub/entities/stats/TotalBaseStats.java @@ -0,0 +1,7 @@ +package com.example.maphub.entities.stats; + +public class TotalBaseStats { + public int buildings; + public int recentBuildings; + public int previousRecentBuildings; +} diff --git a/src/main/java/com/example/maphub/services/BuildingService.java b/src/main/java/com/example/maphub/services/BuildingService.java index adb0564..4b1130b 100644 --- a/src/main/java/com/example/maphub/services/BuildingService.java +++ b/src/main/java/com/example/maphub/services/BuildingService.java @@ -2,13 +2,16 @@ import com.example.maphub.entities.*; import com.example.maphub.entities.buildings.*; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestBody; import java.security.Principal; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneOffset; +import java.time.temporal.TemporalAmount; import java.util.*; @Service @@ -140,6 +143,20 @@ public List getGroupedGridCounts(List or private record xy (int x, int y){} + public int getTotalCount() { + int count = proxyAPIService.getBuildingCount(null,null,null,null,null,null,null,null,null); + return count; + } + public int getPlayerCount(String uuid) { + int count = proxyAPIService.getBuildingCount(List.of(uuid),null,null,null,null,null,null,null,null); + return count; + } + + public int getRecentBuildingCount(){ + int count = proxyAPIService.getBuildingCount(null,null,null,null,null,null,null, LocalDateTime.now().minusMonths(1), LocalDateTime.now()); + return count; + } + } diff --git a/src/main/java/com/example/maphub/services/ProxyAPIService.java b/src/main/java/com/example/maphub/services/ProxyAPIService.java index 9a57c55..db8715a 100644 --- a/src/main/java/com/example/maphub/services/ProxyAPIService.java +++ b/src/main/java/com/example/maphub/services/ProxyAPIService.java @@ -4,6 +4,8 @@ import com.example.maphub.entities.buildings.BuildingDTO; import com.example.maphub.entities.buildings.BuildingGridItem; +import com.example.maphub.entities.stats.PlayerBaseStats; +import com.example.maphub.entities.stats.TotalBaseStats; import com.fasterxml.jackson.core.JsonProcessingException; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.ParameterizedTypeReference; @@ -19,6 +21,7 @@ import java.nio.charset.StandardCharsets; import java.time.Duration; +import java.time.LocalDateTime; import java.util.List; import java.util.Map; @@ -171,7 +174,7 @@ public List getBuildingsByArea(double minLat, double maxLat, double /** * Gets building counts with optional filtering by area, players, visibility, or source */ - public int getBuildingCount(List playerUuids, Double minLat, Double maxLat, Double minLon, Double maxLon, Boolean isPublic, Boolean playerBuilt) { + public int getBuildingCount(List playerUuids, Double minLat, Double maxLat, Double minLon, Double maxLon, Boolean isPublic, Boolean playerBuilt, LocalDateTime from, LocalDateTime to) { try { BuildingCountDTO response = restClient.get() .uri(uriBuilder -> { @@ -182,6 +185,8 @@ public int getBuildingCount(List playerUuids, Double minLat, Double maxL if (maxLon != null) builder.queryParam("maxLon", maxLon); if (isPublic != null) builder.queryParam("isPublic", isPublic); if (playerBuilt != null) builder.queryParam("playerBuilt", playerBuilt); + if (from != null) builder.queryParam("from", from.toString()); + if (to != null) builder.queryParam("to", to.toString()); if (playerUuids != null && !playerUuids.isEmpty()) { for (String uuid : playerUuids) { builder.queryParam("playerUuid", uuid); @@ -237,5 +242,38 @@ public record GridCellDTO( public record BuildingGridResponseDTO( List cells ) {} - + + public TotalBaseStats getTotalStats(){ + try { + return restClient.get() + .uri(uriBuilder -> { + var builder = uriBuilder.path("/stats/total"); + return builder.build(); + }) + .retrieve() + .body(TotalBaseStats.class); + } catch (Exception e) { + System.err.println("Error fetching total stats: " + e.getMessage()); + return null; + } + } + + public PlayerBaseStats getPlayerStats(String uuid){ + try { + return restClient.get() + .uri(uriBuilder -> { + var builder = uriBuilder.path("/stats/player"); + if (uuid != null && !uuid.isBlank()) { + builder.queryParam("uuid", uuid); + } + return builder.build(); + }) + .retrieve() + .body(PlayerBaseStats.class); + } catch (Exception e) { + System.err.println("Error fetching player stats: " + e.getMessage()); + return null; + } + } + } \ No newline at end of file diff --git a/src/main/java/com/example/maphub/services/StatsService.java b/src/main/java/com/example/maphub/services/StatsService.java new file mode 100644 index 0000000..bad8b31 --- /dev/null +++ b/src/main/java/com/example/maphub/services/StatsService.java @@ -0,0 +1,100 @@ +package com.example.maphub.services; + +import com.example.maphub.entities.stats.HomeStatsResp; +import com.example.maphub.entities.stats.PlayerBaseStats; +import com.example.maphub.entities.stats.ProfileStatsResp; +import com.example.maphub.entities.stats.TotalBaseStats; +import org.springframework.stereotype.Service; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; + +import static com.example.maphub.Consts.*; + +@Service +public class StatsService { + private final ProxyAPIService proxyAPIService; + private final BuildingService buildingService; + public StatsService(ProxyAPIService proxyAPIService, BuildingService buildingService){ + this.proxyAPIService = proxyAPIService; + this.buildingService = buildingService; + } + + + public HomeStatsResp getHomepageStats() + { + TotalBaseStats base = proxyAPIService.getTotalStats(); + + if(base == null){ + return new HomeStatsResp(); + } + + double percentage = ((double) base.buildings) / (double) TOTAL_UK_BUILDINGS; + + LocalDateTime expectedFinishTime = null; + //calculating expected completion time + if (percentage > 0) { + long timeSinceStart = Duration.between(SERVER_START_TIME, LocalDateTime.now()).toMillis(); + long expectedTimeToComplete = (long) (timeSinceStart / percentage); + expectedFinishTime = SERVER_START_TIME.plus(expectedTimeToComplete, ChronoUnit.MILLIS); + } + HomeStatsResp resp = new HomeStatsResp(); + resp.buildings = base.buildings; + resp.buildingsRecent = base.recentBuildings; + resp.estimatedFinishDate = expectedFinishTime; + resp.percentage = percentage; + resp.previousRecentBuildings = base.previousRecentBuildings; + return resp; + + } + public ProfileStatsResp getProfileStats(String uuid){ + + PlayerBaseStats base = proxyAPIService.getPlayerStats(uuid); + + if (base == null){ + return new ProfileStatsResp(); + } + + ProfileStatsResp resp = new ProfileStatsResp(); + resp.buildings = base.buildings; + resp.messagesSent = base.messagesSent; + resp.tplls = base.tplls; + resp.timeNonAFK = (float)base.timePlayed / MILLIS_IN_DAY; + + resp.reviewsCompleted = base.reviewsCompleted; + + int buildingWeight = 50; + int tpllWeight = 1; + if (base.timePlayed <= 0) { + resp.productivity = "N/A"; + return resp; + } + float daysPlayed = (float) base.timePlayed / MILLIS_IN_DAY; + float productivity = (buildingWeight * base.buildings + tpllWeight * base.tplls)/daysPlayed; + if(productivity >= 800) + { + resp.productivity = "A"; + } + else if (productivity >= 700 ) + { + resp.productivity = "B"; + } + else if (productivity >= 600) + { + resp.productivity = "C"; + } + else if (productivity >= 500) + { + resp.productivity = "D"; + } + else if(productivity >= 400){ + resp.productivity = "E"; + } + else{ + resp.productivity = "F"; + } + return resp; + + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index ce6af55..da096a3 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -10,7 +10,7 @@ spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect spring.session.store-type=jdbc spring.session.jdbc.initialize-schema=always -server.servlet.session.timeout=30m +server.servlet.session.timeout=24h spring.sql.init.mode=always # Uses Pterodactyl's assigned port, defaults to 8081 if not found diff --git a/src/main/resources/static/Profile.html b/src/main/resources/static/Profile.html index 53e6414..8238dd0 100644 --- a/src/main/resources/static/Profile.html +++ b/src/main/resources/static/Profile.html @@ -30,10 +30,10 @@

name


Stats

-
- Buildings built - -- -
+ + + + diff --git a/src/main/resources/static/Profile.js b/src/main/resources/static/Profile.js index 8f5e8a9..c20108e 100644 --- a/src/main/resources/static/Profile.js +++ b/src/main/resources/static/Profile.js @@ -1,4 +1,5 @@ import {initiatePasswordBoxButtons} from "./PasswordUtils.js"; +import {addStat} from "./StatsUtils.js"; const btnDelete = document.getElementById("btnDeleteAccount") const btnConfirmDelete = document.getElementById("btnConfirmDeleteAccount") @@ -82,7 +83,7 @@ window.addEventListener("load", () => { window.location.assign("login.html"); }); - fetch("/building/playerCount", { + fetch("/stats/profile", { method: "GET", credentials: "include" }) @@ -91,12 +92,23 @@ window.addEventListener("load", () => { return res.json(); }) .then(data => { - if (data && data.count !== undefined) { - buildingCount.innerText = data.count; + if (data) { + updateStatsBox(data) } }) .catch(err => { console.error("Error loading personal building count:", err); buildingCount.innerText = "0"; }); -}); \ No newline at end of file +}); + +function updateStatsBox(data){ + let statsBoxName = "ProfileStatsBox" + addStat(statsBoxName,"Buildings",data.buildings.toString()) + addStat(statsBoxName, "Tplls", data.tplls.toString()) + addStat(statsBoxName,"Time Non-AFK", data.timeNonAFK.toFixed(2) + " days") + addStat(statsBoxName,"Productivity grade", data.productivity) + if (data.reviewsCompleted > 0){ + addStat(statsBoxName, "Reviews", data.reviewsCompleted.toString()) + } +} \ No newline at end of file diff --git a/src/main/resources/static/StatsUtils.js b/src/main/resources/static/StatsUtils.js new file mode 100644 index 0000000..b5e1d9a --- /dev/null +++ b/src/main/resources/static/StatsUtils.js @@ -0,0 +1,7 @@ +export function addStat(containerID, statName, statValue){ + let container = document.getElementById(containerID) + container.innerHTML = container.innerHTML + `
+ ${statName} + ${statValue} +
` +} \ No newline at end of file diff --git a/src/main/resources/static/homePage.js b/src/main/resources/static/homePage.js index 17096ec..642ac60 100644 --- a/src/main/resources/static/homePage.js +++ b/src/main/resources/static/homePage.js @@ -1,4 +1,7 @@ -const map = L.map('map').setView([51.505, -0.09], 10); +import {addStat} from "./StatsUtils.js"; + +//set map centre to cover whole UK +const map = L.map('map').setView([54.06801502799949, -3.8921490108560857], 5); const tiles = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, @@ -347,7 +350,6 @@ function displayWelcomeBox() { const buildingId = document.getElementById("buildingId") const buildingBuilder = document.getElementById("buildingBuilder") const buildingDate = document.getElementById("buildingCreatedDate") -const buildingIsClaimed = document.getElementById("isClaimed") function displayBuildingBox(building) { // Hide default views and tabs @@ -360,53 +362,66 @@ function displayBuildingBox(building) { buildingId.innerText = building.buildingId buildingBuilder.innerText = building.username buildingDate.innerText = formatDate(building.timeAdded) - buildingIsClaimed.innerText = building.playerBuilt } function formatDate(dateStr) { if (!dateStr) return "Unknown"; + + // Handle dates beyond JavaScript Date's representable range + const yearMatch = dateStr.match(/^\+?(\d+)-/); + + if (yearMatch) { + const year = Number(yearMatch[1]); + + if (year > 999999){ + return `~${((year - 2026) / 1_000_000).toFixed(1)} million years`; + } + if (year > 9999) { + return `~${((year - 2026) / 1_000).toFixed(1)} thousand years`; + } + } + const date = new Date(dateStr); - // If the input is not a valid date parseable by JS, return original string if (isNaN(date.getTime())) return dateStr; + console.log("date is valid") + + const cutoffDate = new Date("2026-03-15T00:00:00"); + if (date < cutoffDate) return "Unknown"; + + const year = date.getFullYear(); const day = String(date.getDate()).padStart(2, '0'); const month = String(date.getMonth() + 1).padStart(2, '0'); - const year = date.getFullYear(); return `${day}-${month}-${year}`; } -const statBuildings = document.getElementById("statBuildings"); -const statProgress = document.getElementById("statProgress"); - async function loadServerStats() { try { - const response = await fetch("/building/total", { - method: "GET" + const response = await fetch("/stats/homePage", { + method: "GET", + credentials: "include" }); if (!response.ok) throw new Error("Failed to fetch server stats"); - const data = await response.json(); - const total = data.count || 0; - - // Display total formatted with commas (e.g., 12,345) - statBuildings.innerText = total.toLocaleString(); - - // Calculate percentage complete based on 30,000,000 target - const percentage = (total / 30000000) * 100; + if (data){ + let statBoxName = "ServerStatsBox" + addStat(statBoxName,"Buildings",data.buildings.toString()) + let percentageText = "" + if (data.percentage > 0 && data.percentage < 0.01) { + percentageText = data.percentage.toFixed(4) + "%"; + } else { + percentageText = data.percentage.toFixed(2) + "%"; + } + addStat(statBoxName,"Buildings last month",data.buildingsRecent) + addStat(statBoxName,"Percentage Complete",percentageText) + addStat(statBoxName,"Estimated Completion", formatDate(data.estimatedFinishDate)) - // Show higher precision if percentage is tiny, otherwise 2 decimals - if (percentage > 0 && percentage < 0.01) { - statProgress.innerText = percentage.toFixed(4) + "%"; - } else { - statProgress.innerText = percentage.toFixed(2) + "%"; } } catch (err) { console.error("Error loading server statistics:", err); - statBuildings.innerText = "--"; - statProgress.innerText = "--%"; } } \ No newline at end of file diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html index 617df67..21a2db7 100644 --- a/src/main/resources/static/index.html +++ b/src/main/resources/static/index.html @@ -11,7 +11,7 @@ integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""> - +
@@ -45,14 +45,6 @@

BTUK.org

-
- Is claimed - -- -