diff --git a/backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/Application.java b/backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/Application.java index 9f905e4..49b7afb 100644 --- a/backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/Application.java +++ b/backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/Application.java @@ -3,9 +3,6 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.web.servlet.config.annotation.CorsRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @SpringBootApplication public class Application { diff --git a/backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/member/application/services/MemberService.java b/backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/member/application/services/MemberService.java index 62b274e..23a5bd7 100644 --- a/backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/member/application/services/MemberService.java +++ b/backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/member/application/services/MemberService.java @@ -2,6 +2,7 @@ import be.informatievlaanderen.vsds.demonstrator.member.application.valueobjects.IngestedMemberDto; import be.informatievlaanderen.vsds.demonstrator.member.application.valueobjects.MemberDto; +import be.informatievlaanderen.vsds.demonstrator.member.rest.dtos.LineChartDto; import org.locationtech.jts.geom.Geometry; import java.time.LocalDateTime; @@ -15,4 +16,6 @@ public interface MemberService { MemberDto getMemberById(String memberId); long getNumberOfMembers(); + + LineChartDto getLineChartDto(); } diff --git a/backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/member/application/services/MemberServiceImpl.java b/backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/member/application/services/MemberServiceImpl.java index 7b74e07..2c9a382 100644 --- a/backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/member/application/services/MemberServiceImpl.java +++ b/backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/member/application/services/MemberServiceImpl.java @@ -5,8 +5,10 @@ import be.informatievlaanderen.vsds.demonstrator.member.application.exceptions.ResourceNotFoundException; import be.informatievlaanderen.vsds.demonstrator.member.application.valueobjects.IngestedMemberDto; import be.informatievlaanderen.vsds.demonstrator.member.application.valueobjects.MemberDto; +import be.informatievlaanderen.vsds.demonstrator.member.domain.member.entities.LineChart; import be.informatievlaanderen.vsds.demonstrator.member.domain.member.entities.Member; import be.informatievlaanderen.vsds.demonstrator.member.domain.member.repositories.MemberRepository; +import be.informatievlaanderen.vsds.demonstrator.member.rest.dtos.LineChartDto; import be.informatievlaanderen.vsds.demonstrator.member.rest.websocket.MessageController; import org.locationtech.jts.geom.Geometry; import org.opengis.referencing.operation.TransformException; @@ -19,6 +21,7 @@ import java.time.Duration; import java.time.LocalDateTime; import java.util.List; +import java.util.Map; @Service public class MemberServiceImpl implements MemberService { @@ -68,4 +71,13 @@ public MemberDto getMemberById(String memberId) { public long getNumberOfMembers() { return repository.getNumberOfMembers(); } + + @Override + public LineChartDto getLineChartDto() { + long numberOfMembers = getNumberOfMembers(); + LocalDateTime startDate = LocalDateTime.now().minusDays(7); + List membersAfterLocalDateTime = repository.findMembersAfterLocalDateTime(startDate); + LineChart lineChart = new LineChart(startDate,numberOfMembers-membersAfterLocalDateTime.size(),membersAfterLocalDateTime); + return new LineChartDto(lineChart.getLabels(), lineChart.getValues()); + } } diff --git a/backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/member/domain/member/entities/LineChart.java b/backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/member/domain/member/entities/LineChart.java new file mode 100644 index 0000000..fe32d97 --- /dev/null +++ b/backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/member/domain/member/entities/LineChart.java @@ -0,0 +1,57 @@ +package be.informatievlaanderen.vsds.demonstrator.member.domain.member.entities; + +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class LineChart { + private final List memberList; + private final long numberOfMembersOutsideFrame; + private final List labels; + private final List> values; + private final LocalDateTime startDate; + + public LineChart(LocalDateTime startDate, long numberOfMembersOutsideFrame, List memberList) { + this.startDate = startDate; + this.numberOfMembersOutsideFrame = numberOfMembersOutsideFrame; + this.memberList = memberList; + this.labels = new ArrayList<>(); + this.values = new ArrayList<>(); + } + + private void calculatePointElements() { + Map collect = memberList + .stream() + .map(Member::getTimestamp) + .map(localDateTime -> localDateTime.truncatedTo(ChronoUnit.HOURS)) + .collect(Collectors.groupingBy(Function.identity(), Collectors.summingInt(e -> 1))); + LocalDateTime startTime = startDate.truncatedTo(ChronoUnit.HOURS); + long memberCount = numberOfMembersOutsideFrame; + values.add(new ArrayList<>()); + while (startTime.isBefore(LocalDateTime.now())) { + + memberCount = memberCount + collect.getOrDefault(startTime, 0); + if (memberCount > 0) { + labels.add(startTime.toString()); + values.get(0).add((int) memberCount); + } + startTime = startTime.plusHours(1); + } + } + + public List getLabels() { + if (labels.isEmpty()) + calculatePointElements(); + return labels; + } + + public List> getValues() { + if (values.isEmpty()) + calculatePointElements(); + return values; + } +} diff --git a/backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/member/domain/member/repositories/MemberRepository.java b/backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/member/domain/member/repositories/MemberRepository.java index a87d8a8..d169c0a 100644 --- a/backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/member/domain/member/repositories/MemberRepository.java +++ b/backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/member/domain/member/repositories/MemberRepository.java @@ -11,6 +11,6 @@ public interface MemberRepository { void saveMember(Member geometry); List getMembersByGeometry(Geometry geometry, LocalDateTime startTime, LocalDateTime endTime); Optional findByMemberId(String memberId); - long getNumberOfMembers(); + List findMembersAfterLocalDateTime(LocalDateTime localDateTime); } diff --git a/backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/member/infra/MemberEntityJpaRepository.java b/backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/member/infra/MemberEntityJpaRepository.java index c7da968..db3faf6 100644 --- a/backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/member/infra/MemberEntityJpaRepository.java +++ b/backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/member/infra/MemberEntityJpaRepository.java @@ -7,9 +7,13 @@ import java.time.LocalDateTime; import java.util.List; +import java.util.stream.Stream; public interface MemberEntityJpaRepository extends JpaRepository { @Query(value = "select l from member_entity l where intersects(l.geometry, :geometry) = true and l.timestamp >= :startTime and l.timestamp <= :endTime") List getMemberGeometryEntitiesCoveredByGeometryInTimePeriod(@Param("geometry") Geometry geometry, @Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime); + + List findByTimestampAfter(LocalDateTime localDateTime); + } diff --git a/backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/member/infra/MemberRepositoryImpl.java b/backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/member/infra/MemberRepositoryImpl.java index 7042e7d..ab80a3a 100644 --- a/backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/member/infra/MemberRepositoryImpl.java +++ b/backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/member/infra/MemberRepositoryImpl.java @@ -42,4 +42,13 @@ public Optional findByMemberId(String memberId) { public long getNumberOfMembers() { return memberGeometryJpaRepo.count(); } + + @Override + public List findMembersAfterLocalDateTime(LocalDateTime localDateTime) { + return memberGeometryJpaRepo + .findByTimestampAfter(localDateTime) + .stream() + .map(entity->new Member(entity.getMemberId(), entity.getGeometry(), entity.getTimestamp())) + .toList(); + } } diff --git a/backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/member/rest/dtos/LineChartDto.java b/backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/member/rest/dtos/LineChartDto.java new file mode 100644 index 0000000..70dd92a --- /dev/null +++ b/backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/member/rest/dtos/LineChartDto.java @@ -0,0 +1,21 @@ +package be.informatievlaanderen.vsds.demonstrator.member.rest.dtos; + +import java.util.List; + +public class LineChartDto { + private final List labels; + private final List> values; + + public LineChartDto(List labels, List> values) { + this.labels = labels; + this.values = values; + } + + public List getLabels() { + return labels; + } + + public List> getValues() { + return values; + } +} diff --git a/backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/member/rest/websocket/LineChartController.java b/backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/member/rest/websocket/LineChartController.java new file mode 100644 index 0000000..dc43b0c --- /dev/null +++ b/backend/src/main/java/be/informatievlaanderen/vsds/demonstrator/member/rest/websocket/LineChartController.java @@ -0,0 +1,24 @@ +package be.informatievlaanderen.vsds.demonstrator.member.rest.websocket; + +import be.informatievlaanderen.vsds.demonstrator.member.application.services.MemberService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Controller; + +@Controller +public class LineChartController { + private final SimpMessagingTemplate template; + private final MemberService memberService; + + @Autowired + public LineChartController(SimpMessagingTemplate template, MemberService memberService) { + this.template = template; + this.memberService = memberService; + } + + @Scheduled(fixedDelay = 1000) + public void send() { + this.template.convertAndSend("/broker/linechart", memberService.getLineChartDto()); + } +} diff --git a/backend/src/test/java/be/informatievlaanderen/vsds/demonstrator/member/rest/websocket/LineChartControllerTest.java b/backend/src/test/java/be/informatievlaanderen/vsds/demonstrator/member/rest/websocket/LineChartControllerTest.java new file mode 100644 index 0000000..c344513 --- /dev/null +++ b/backend/src/test/java/be/informatievlaanderen/vsds/demonstrator/member/rest/websocket/LineChartControllerTest.java @@ -0,0 +1,37 @@ +package be.informatievlaanderen.vsds.demonstrator.member.rest.websocket; + +import be.informatievlaanderen.vsds.demonstrator.member.application.services.MemberService; +import be.informatievlaanderen.vsds.demonstrator.member.rest.dtos.LineChartDto; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.messaging.simp.SimpMessagingTemplate; + +import java.util.List; + +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class LineChartControllerTest { + + @Mock + private MemberService memberService; + @Mock + private SimpMessagingTemplate simpMessagingTemplate; + @InjectMocks + private LineChartController lineChartController; + + @Test + void test_lineChartIsTransferredToWebsocket(){ + LineChartDto lineChartDto = new LineChartDto(List.of(), List.of()); + when(memberService.getLineChartDto()).thenReturn(lineChartDto); + lineChartController.send(); + + verify(memberService).getLineChartDto(); + verify(simpMessagingTemplate).convertAndSend("/broker/linechart",lineChartDto); + } + +} \ No newline at end of file diff --git a/backend/src/test/java/be/informatievlaanderen/vsds/demonstrator/member/rest/websocket/MemberCounterControllerTest.java b/backend/src/test/java/be/informatievlaanderen/vsds/demonstrator/member/rest/websocket/MemberCounterControllerTest.java index dab5ed8..f272db1 100644 --- a/backend/src/test/java/be/informatievlaanderen/vsds/demonstrator/member/rest/websocket/MemberCounterControllerTest.java +++ b/backend/src/test/java/be/informatievlaanderen/vsds/demonstrator/member/rest/websocket/MemberCounterControllerTest.java @@ -22,7 +22,7 @@ class MemberCounterControllerTest { private MemberCounterController memberCounterController; @Test - void test(){ + void test_numberOfMembersIsTransferredToWebsocket(){ when(memberService.getNumberOfMembers()).thenReturn(76L); memberCounterController.send(); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8970972..019f77d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,14 +11,17 @@ "@livereader/graphly-d3-vue": "^1.1.2", "@terraformer/wkt": "^2.2.0", "axios": "^1.5.0", + "chart.js": "^4.1.1", "d3": "^7.8.5", "proj4": "^2.9.0", "proj4leaflet": "^1.0.2", "vue": "^3.3.4", + "vue-chartjs": "^5.2.0", "vue-router": "^4.2.4", "webstomp-client": "^1.2.6" }, "devDependencies": { + "@types/chart.js": "^2.9.38", "@vitejs/plugin-vue": "^4.3.1", "leaflet": "^1.9.4", "vite": "^4.4.9" @@ -392,6 +395,11 @@ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, + "node_modules/@kurkle/color": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", + "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==" + }, "node_modules/@livereader/graphly-d3": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/@livereader/graphly-d3/-/graphly-d3-1.5.2.tgz", @@ -416,6 +424,15 @@ "resolved": "https://registry.npmjs.org/@terraformer/wkt/-/wkt-2.2.0.tgz", "integrity": "sha512-i33rTSqPtmO4sRdeznI0IEc9gpIZZIXN5kGhZ4rTwVtDccDKL3h4uia9cmWdRJlJMlG4Febxatw5b9ylI5YYuA==" }, + "node_modules/@types/chart.js": { + "version": "2.9.38", + "resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.38.tgz", + "integrity": "sha512-rLoHHprkVEDpAXqke/xHalyXR+5Nv+3tfViwT/UnJZ41Wp/XPaSRlJKw2PU3S3tTCqKKyjkYai+VpeHoti79XQ==", + "dev": true, + "dependencies": { + "moment": "^2.10.2" + } + }, "node_modules/@vitejs/plugin-vue": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.3.4.tgz", @@ -566,6 +583,17 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/chart.js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.1.1.tgz", + "integrity": "sha512-P0pCosNXp+LR8zO/QTkZKT6Hb7p0DPFtypEeVOf+6x06hX13NIb75R0DXUA4Ksx/+48chDQKtCCmRCviQRTqsA==", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": "^7.0.0" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -1100,9 +1128,9 @@ "dev": true }, "node_modules/magic-string": { - "version": "0.30.3", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.3.tgz", - "integrity": "sha512-B7xGbll2fG/VjP+SWg4sX3JynwIU0mjoTc6MPpKNuIvftk6u6vqhDnk1R80b8C2GBR6ywqy+1DcKBrevBg+bmw==", + "version": "0.30.4", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.4.tgz", + "integrity": "sha512-Q/TKtsC5BPm0kGqgBIF9oXAs/xEf2vRKiIB4wCRQTJOQIByZ1d+NnUOotvJOvNpi5RNIgVOMC3pOuaP1ZTDlVg==", "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, @@ -1134,6 +1162,15 @@ "node": ">= 0.6" } }, + "node_modules/moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/nanoid": { "version": "3.3.6", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", @@ -1157,9 +1194,9 @@ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" }, "node_modules/postcss": { - "version": "8.4.30", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.30.tgz", - "integrity": "sha512-7ZEao1g4kd68l97aWG/etQKPKq07us0ieSZ2TnFDk11i0ZfDW2AwKHYU8qv4MZKqN2fdBfg+7q0ES06UA73C1g==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "funding": [ { "type": "opencollective", @@ -1227,9 +1264,9 @@ "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" }, "node_modules/rollup": { - "version": "3.29.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.3.tgz", - "integrity": "sha512-T7du6Hum8jOkSWetjRgbwpM6Sy0nECYrYRSmZjayFcOddtKJWU4d17AC3HNUk7HRuqy4p+G7aEZclSHytqUmEg==", + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", "dev": true, "bin": { "rollup": "dist/bin/rollup" @@ -1335,6 +1372,15 @@ "@vue/shared": "3.3.4" } }, + "node_modules/vue-chartjs": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.2.0.tgz", + "integrity": "sha512-d3zpKmGZr2OWHQ1xmxBcAn5ShTG917+/UCLaSpaCDDqT0U7DBsvFzTs69ZnHCgKoXT55GZDW8YEj9Av+dlONLA==", + "peerDependencies": { + "chart.js": "^4.1.1", + "vue": "^3.0.0-0 || ^2.7.0" + } + }, "node_modules/vue-router": { "version": "4.2.5", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.2.5.tgz", diff --git a/frontend/package.json b/frontend/package.json index 7cb0305..c075281 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,14 +11,17 @@ "@livereader/graphly-d3-vue": "^1.1.2", "@terraformer/wkt": "^2.2.0", "axios": "^1.5.0", + "chart.js": "^4.1.1", "d3": "^7.8.5", "proj4": "^2.9.0", "proj4leaflet": "^1.0.2", "vue": "^3.3.4", + "vue-chartjs": "^5.2.0", "vue-router": "^4.2.4", "webstomp-client": "^1.2.6" }, "devDependencies": { + "@types/chart.js": "^2.9.38", "@vitejs/plugin-vue": "^4.3.1", "leaflet": "^1.9.4", "vite": "^4.4.9" diff --git a/frontend/src/App.vue b/frontend/src/App.vue index b70cb03..bd73153 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -2,6 +2,7 @@ import LeafletMap from './components/map/LeafletMap.vue' import GlobalHeader from "@/components/headers/GlobalHeader.vue"; import MemberCounter from './components/membercounter/MemberCounter.vue' +import LineChart from "@/components/linechart/LineChart.vue";