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/ 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 f1eb7a2..b489cad 100644 --- a/src/main/java/com/example/maphub/Security/SecurityConfig.java +++ b/src/main/java/com/example/maphub/Security/SecurityConfig.java @@ -59,7 +59,10 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti "/images/**", "/favicon.ico", "/ForgotPassword.html", - "/building/**" + "/building/**", + "/map/**", + "/stats/homePage/**", + "/stats/homePage" ).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/MapController.java b/src/main/java/com/example/maphub/controllers/MapController.java new file mode 100644 index 0000000..e88add8 --- /dev/null +++ b/src/main/java/com/example/maphub/controllers/MapController.java @@ -0,0 +1,47 @@ +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; + private final ProxyAPIService proxyAPIService; + public MapController(BuildingService buildingService, HeatmapService heatmapService, ProxyAPIService proxyAPIService){ + this.buildingService = buildingService; + this.heatmapService = heatmapService; + this.proxyAPIService = proxyAPIService; + } + + @PostMapping("/overview") + 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); + 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/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/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/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/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 75% rename from src/main/java/com/example/maphub/BuildingController.java rename to src/main/java/com/example/maphub/services/BuildingService.java index 2671134..4b1130b 100644 --- a/src/main/java/com/example/maphub/BuildingController.java +++ b/src/main/java/com/example/maphub/services/BuildingService.java @@ -1,29 +1,27 @@ -package com.example.maphub; +package com.example.maphub.services; import com.example.maphub.entities.*; -import com.example.maphub.services.ProxyAPIService; +import com.example.maphub.entities.buildings.*; import org.springframework.http.ResponseEntity; -import org.springframework.security.core.Authentication; -import org.springframework.web.bind.annotation.*; +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.*; -@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 +50,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 +65,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 +138,25 @@ 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)); + 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; } - @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)); + 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/HeatmapService.java b/src/main/java/com/example/maphub/services/HeatmapService.java new file mode 100644 index 0000000..7ab4670 --- /dev/null +++ b/src/main/java/com/example/maphub/services/HeatmapService.java @@ -0,0 +1,81 @@ +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 { + + // 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() || totalBuildings <= 0) { + return out; + } + 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; + h.magnitude = magnitude; + 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..db8715a 100644 --- a/src/main/java/com/example/maphub/services/ProxyAPIService.java +++ b/src/main/java/com/example/maphub/services/ProxyAPIService.java @@ -2,8 +2,10 @@ -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.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/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/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 a726f7b..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,8 +83,8 @@ window.addEventListener("load", () => { window.location.assign("login.html"); }); - fetch("/building/buildingPersonalCount", { - method: "POST", + fetch("/stats/profile", { + method: "GET", credentials: "include" }) .then(res => { @@ -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 023609e..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, @@ -14,9 +17,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 +110,105 @@ 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] + ]; - buildings = data; + const color = heatmapColours[item.magnitude]; + + const rectangle = L.rectangle(bounds, { + color: color, + weight: 0, // No border + fillColor: color, + fillOpacity: 0.7 // Semi-transparent overlay + }); + + heatMapGroup.addLayer(rectangle); + }); +} + +function updateBuildingsInView(buildings){ buildingMarkerGroup.clearLayers(); buildings.forEach(building => { @@ -174,13 +237,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 +244,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 - }; - - 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); - } - }); +function updateMarkerGroupCounts(buildingGridItems) { + gridMarkerGroup.clearLayers(); - } 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) { @@ -334,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 @@ -347,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/buildingCountTotal", { - method: "POST" + 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 - -- -
-
+

Account Initiation

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