From 5d0be20fd2370c408b79dae15db8d276c8f9e3db Mon Sep 17 00:00:00 2001 From: Maxime Perrault Date: Mon, 4 Aug 2025 17:07:23 +0200 Subject: [PATCH 01/12] WIP: tech: dont not retrieve regulatory area if not needed. --- .../repositories/IRegulatoryAreaRepository.kt | 2 +- .../regulatoryAreas/GetAllRegulatoryAreas.kt | 4 +- .../api/endpoints/bff/v1/RegulatoryAreas.kt | 11 ++--- .../database/model/RegulatoryAreaModel.kt | 11 ++--- .../JpaRegulatoryAreaRepository.kt | 6 +-- frontend/src/api/regulatoryLayersAPI.ts | 0 frontend/src/domain/entities/regulatory.ts | 0 .../DashboardForm/RegulatoryAreas/Layer.tsx | 9 ++++ .../src/features/layersSelector/index.tsx | 48 +++++++++---------- .../RegulatoryLayerGroup/RegulatoryLayer.tsx | 2 +- .../Regulatory/RegulatoryPreviewLayer.ts | 3 ++ .../features/map/layers/Regulatory/index.ts | 5 +- 12 files changed, 55 insertions(+), 46 deletions(-) create mode 100644 frontend/src/api/regulatoryLayersAPI.ts create mode 100644 frontend/src/domain/entities/regulatory.ts diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/repositories/IRegulatoryAreaRepository.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/repositories/IRegulatoryAreaRepository.kt index 2f2e23a997..f8a6219e94 100644 --- a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/repositories/IRegulatoryAreaRepository.kt +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/repositories/IRegulatoryAreaRepository.kt @@ -6,7 +6,7 @@ import org.locationtech.jts.geom.Geometry interface IRegulatoryAreaRepository { fun findById(id: Int): RegulatoryAreaEntity? - fun findAll(): List + fun findAll(withGeometry: Boolean): List fun count(): Long diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/regulatoryAreas/GetAllRegulatoryAreas.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/regulatoryAreas/GetAllRegulatoryAreas.kt index 8f15320260..0ebf0809c9 100644 --- a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/regulatoryAreas/GetAllRegulatoryAreas.kt +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/regulatoryAreas/GetAllRegulatoryAreas.kt @@ -11,9 +11,9 @@ class GetAllRegulatoryAreas( ) { private val logger = LoggerFactory.getLogger(GetAllRegulatoryAreas::class.java) - fun execute(): List { + fun execute(withGeometry: Boolean): List { logger.info("Attempt to GET all regulatory areas") - val regulatoryAreas = regulatoryAreaRepository.findAll() + val regulatoryAreas = regulatoryAreaRepository.findAll(withGeometry) logger.info("Found ${regulatoryAreas.size} regulatory areas") return regulatoryAreas diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/bff/v1/RegulatoryAreas.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/bff/v1/RegulatoryAreas.kt index 13e688dd93..0c586fb2a6 100644 --- a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/bff/v1/RegulatoryAreas.kt +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/bff/v1/RegulatoryAreas.kt @@ -6,10 +6,7 @@ import fr.gouv.cacem.monitorenv.infrastructure.api.adapters.bff.outputs.Regulato import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag import jakarta.websocket.server.PathParam -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController +import org.springframework.web.bind.annotation.* @RestController("regulatoryAreasV1") @RequestMapping("/bff/v1/regulatory") @@ -31,8 +28,10 @@ class RegulatoryAreas( @GetMapping("") @Operation(summary = "Get regulatory Areas") - fun getAll(): List { - val regulatoryAreas = getAllRegulatoryAreas.execute() + fun getAll( + @RequestParam(name = "withGeometry") withGeometry: Boolean, + ): List { + val regulatoryAreas = getAllRegulatoryAreas.execute(withGeometry) return regulatoryAreas.map { RegulatoryAreaWithMetadataDataOutput.fromRegulatoryAreaEntity( it, diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/model/RegulatoryAreaModel.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/model/RegulatoryAreaModel.kt index 28362a7056..056d3a344d 100644 --- a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/model/RegulatoryAreaModel.kt +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/model/RegulatoryAreaModel.kt @@ -3,12 +3,7 @@ package fr.gouv.cacem.monitorenv.infrastructure.database.model import fr.gouv.cacem.monitorenv.domain.entities.regulatoryArea.RegulatoryAreaEntity import fr.gouv.cacem.monitorenv.infrastructure.database.model.TagRegulatoryAreaModel.Companion.toTagEntities import fr.gouv.cacem.monitorenv.infrastructure.database.model.ThemeRegulatoryAreaModel.Companion.toThemeEntities -import jakarta.persistence.Column -import jakarta.persistence.Entity -import jakarta.persistence.FetchType -import jakarta.persistence.Id -import jakarta.persistence.OneToMany -import jakarta.persistence.Table +import jakarta.persistence.* import org.locationtech.jts.geom.MultiPolygon import org.n52.jackson.datatype.jts.GeometryDeserializer import org.n52.jackson.datatype.jts.GeometrySerializer @@ -50,7 +45,7 @@ data class RegulatoryAreaModel( @Column(name = "type") val type: String?, @Column(name = "url") val url: String?, ) { - fun toRegulatoryArea() = + fun toRegulatoryArea(withGeometry: Boolean) = RegulatoryAreaEntity( id = id, plan = plan, @@ -60,7 +55,7 @@ data class RegulatoryAreaModel( editeur = editeur, edition = edition, facade = facade, - geom = geom, + geom = if (withGeometry) geom else null, layerName = layerName, polyName = polyName, observation = observation, diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaRegulatoryAreaRepository.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaRegulatoryAreaRepository.kt index f1c3830bdb..0b694c5b30 100644 --- a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaRegulatoryAreaRepository.kt +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaRegulatoryAreaRepository.kt @@ -11,11 +11,11 @@ import org.springframework.stereotype.Repository class JpaRegulatoryAreaRepository( private val dbRegulatoryAreaRepository: IDBRegulatoryAreaRepository, ) : IRegulatoryAreaRepository { - override fun findAll(): List = - dbRegulatoryAreaRepository.findAllByOrderByLayerName().map { it.toRegulatoryArea() } + override fun findAll(withGeometry: Boolean): List = + dbRegulatoryAreaRepository.findAllByOrderByLayerName().map { it.toRegulatoryArea(withGeometry) } override fun findById(id: Int): RegulatoryAreaEntity? = - dbRegulatoryAreaRepository.findByIdOrNull(id)?.toRegulatoryArea() + dbRegulatoryAreaRepository.findByIdOrNull(id)?.toRegulatoryArea(true) override fun count(): Long = dbRegulatoryAreaRepository.count() diff --git a/frontend/src/api/regulatoryLayersAPI.ts b/frontend/src/api/regulatoryLayersAPI.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/src/domain/entities/regulatory.ts b/frontend/src/domain/entities/regulatory.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/src/features/Dashboard/components/DashboardForm/RegulatoryAreas/Layer.tsx b/frontend/src/features/Dashboard/components/DashboardForm/RegulatoryAreas/Layer.tsx index 043794fe3c..21cbc166a3 100644 --- a/frontend/src/features/Dashboard/components/DashboardForm/RegulatoryAreas/Layer.tsx +++ b/frontend/src/features/Dashboard/components/DashboardForm/RegulatoryAreas/Layer.tsx @@ -34,6 +34,15 @@ export function Layer({ isPinned = false, isSelected, layerId, regulatoryAreas } const ref = createRef() const layer = regulatoryAreas.find(regulatoryArea => regulatoryArea.id === layerId) + // const { layer } = useGetRegulatoryLayersQuery( + // { withGeometry: true }, + // { + // selectFromResult: result => ({ + // layer: result?.currentData?.entities[layerId] + // }) + // } + // ) + const handleSelectZone = e => { e.stopPropagation() diff --git a/frontend/src/features/layersSelector/index.tsx b/frontend/src/features/layersSelector/index.tsx index ecda451116..cbb2e4cb34 100644 --- a/frontend/src/features/layersSelector/index.tsx +++ b/frontend/src/features/layersSelector/index.tsx @@ -12,7 +12,7 @@ import { import { useAppDispatch } from '@hooks/useAppDispatch' import { useAppSelector } from '@hooks/useAppSelector' import { useMountTransition } from '@hooks/useMountTransition' -import { Accent, FulfillingBouncingCircleLoader, Icon, IconButton, Size, THEME } from '@mtes-mct/monitor-ui' +import { Accent, Icon, IconButton, Size } from '@mtes-mct/monitor-ui' import { layerSidebarActions } from 'domain/shared_slices/LayerSidebar' import styled from 'styled-components' @@ -141,30 +141,30 @@ export function LayersSidebar() { )} - - {mainVigilanceAreaFormOpen && ( - - )} - - - )} - {(regulatoryAreas.isLoading || amps.isLoading) && ( - - - - Chargement des zones cartographiques ({regulatoryAreas.isLoading && 'Zones réglementaires'} - {regulatoryAreas.isLoading && amps.isLoading ? ' et ' : ''} - {amps.isLoading && 'Aires Marines Protégées'}) - - + + {mainVigilanceAreaFormOpen && ( + + )} + + )} + {/* {(regulatoryAreas.isLoading || amps.isLoading) && ( */} + {/* */} + {/* */} + {/* */} + {/* Chargement des zones cartographiques ({regulatoryAreas.isLoading && 'Zones réglementaires'} */} + {/* {regulatoryAreas.isLoading && amps.isLoading ? ' et ' : ''} */} + {/* {amps.isLoading && 'Aires Marines Protégées'}) */} + {/* */} + {/* */} + {/* )} */} ) } diff --git a/frontend/src/features/layersSelector/search/ResultsList/RegulatoryLayerGroup/RegulatoryLayer.tsx b/frontend/src/features/layersSelector/search/ResultsList/RegulatoryLayerGroup/RegulatoryLayer.tsx index 3185e885a5..faa658fddd 100644 --- a/frontend/src/features/layersSelector/search/ResultsList/RegulatoryLayerGroup/RegulatoryLayer.tsx +++ b/frontend/src/features/layersSelector/search/ResultsList/RegulatoryLayerGroup/RegulatoryLayer.tsx @@ -40,7 +40,7 @@ export function RegulatoryLayer({ groupName, layerId, searchedText }: Regulatory const regulatoryAreasLinkedToVigilanceAreaForm = useAppSelector(state => state.vigilanceArea.regulatoryAreasToAdd) const isLinkingRegulatoryToVigilanceArea = useAppSelector(state => getIsLinkingRegulatoryToVigilanceArea(state)) - const { layer } = useGetRegulatoryAreasQuery(undefined, { + const { layer } = useGetRegulatoryAreasQuery({ withGeometry: true }, { selectFromResult: result => { const layerGroup = result?.currentData?.regulatoryAreasByLayer.find(group => group.group === groupName) diff --git a/frontend/src/features/map/layers/Regulatory/RegulatoryPreviewLayer.ts b/frontend/src/features/map/layers/Regulatory/RegulatoryPreviewLayer.ts index fe81005f90..011a7257e5 100644 --- a/frontend/src/features/map/layers/Regulatory/RegulatoryPreviewLayer.ts +++ b/frontend/src/features/map/layers/Regulatory/RegulatoryPreviewLayer.ts @@ -29,6 +29,9 @@ export function RegulatoryPreviewLayer({ map }: BaseMapChildrenProps) { const isLayersSidebarVisible = useAppSelector(state => state.global.visibility.isLayersSidebarVisible) const isLayerVisible = isLayersSidebarVisible && isRegulatorySearchResultsVisible && !isLinkingAMPToVigilanceArea + // const { data: regulatoryLayers } = useGetRegulatoryLayersQuery({ withGeometry: isLayerVisible }) + // + // const isolatedLayer = useAppSelector(state => state.map.isolatedLayer) const regulatoryPreviewVectorSourceRef = useRef(new VectorSource()) as MutableRefObject< VectorSource> diff --git a/frontend/src/features/map/layers/Regulatory/index.ts b/frontend/src/features/map/layers/Regulatory/index.ts index e58616fabc..afee7bd5ee 100644 --- a/frontend/src/features/map/layers/Regulatory/index.ts +++ b/frontend/src/features/map/layers/Regulatory/index.ts @@ -18,13 +18,16 @@ import type { Geometry } from 'ol/geom' export const metadataIsShowedPropertyName = 'metadataIsShowed' export function RegulatoryLayers({ map }: BaseMapChildrenProps) { - const { data: regulatoryLayers } = useGetRegulatoryAreasQuery() const showedRegulatoryLayerIds = useAppSelector(state => state.regulatory.showedRegulatoryLayerIds) const regulatoryMetadataLayerId = useAppSelector(state => getDisplayedMetadataRegulatoryLayerId(state)) const isLinkingAMPToVigilanceArea = useAppSelector(state => getIsLinkingAMPToVigilanceArea(state)) const isLayerVisible = !isLinkingAMPToVigilanceArea + const { data: regulatoryLayers } = useGetRegulatoryLayersQuery({ + withGeometry: showedRegulatoryLayerIds.length > 0 && isLayerVisible + }) + const isolatedLayer = useAppSelector(state => state.map.isolatedLayer) const regulatoryVectorSourceRef = useRef(new VectorSource()) as MutableRefObject>> From 44a3459676835261501c6be52e3c2dc7128f055b Mon Sep 17 00:00:00 2001 From: Maxime Perrault Date: Wed, 6 Aug 2025 12:09:55 +0200 Subject: [PATCH 02/12] tech: fetch regulatory area geo when needed and add zoom and bbox to filter results --- .github/workflows/cicd-app.yml | 1 + .../domain/entities/amp/AMPEntity.kt | 2 +- .../domain/repositories/IAMPRepository.kt | 2 +- .../repositories/IRegulatoryAreaRepository.kt | 6 ++- .../domain/use_cases/amps/GetAllAMPs.kt | 4 +- .../regulatoryAreas/GetAllRegulatoryAreas.kt | 8 ++- .../api/adapters/bff/outputs/AMPDataOutput.kt | 2 +- .../api/endpoints/bff/v1/Amps.kt | 7 ++- .../api/endpoints/bff/v1/RegulatoryAreas.kt | 16 +++--- .../infrastructure/database/model/AMPModel.kt | 4 +- .../database/model/RegulatoryAreaModel.kt | 7 ++- .../database/repositories/JpaAMPRepository.kt | 3 +- .../JpaRegulatoryAreaRepository.kt | 16 +++++- .../interfaces/IDBRegulatoryAreaRepository.kt | 51 ++++++++++++++++++- .../domain/use_cases/amps/GetAllAMPsUTest.kt | 4 +- .../GetAllRegulatoryAreasUTest.kt | 4 +- .../api/endpoints/bff/v1/AmpsITests.kt | 4 +- .../endpoints/bff/v1/RegulatoryAreasITests.kt | 4 +- .../repositories/JpaAMPRepositoryTests.kt | 2 +- .../JpaRegulatoryAreaRepositoryITests.kt | 2 +- frontend/src/api/ampsAPI.ts | 16 +++--- frontend/src/domain/entities/AMPs.ts | 2 +- frontend/src/domain/shared_slices/Global.ts | 3 +- frontend/src/domain/shared_slices/Map.ts | 12 +++++ .../components/DashboardForm/Amps/Layer.tsx | 13 +++-- .../DashboardForm/RegulatoryAreas/Layer.tsx | 3 +- .../VigilanceAreas/Panel/Amps.tsx | 2 +- .../useCases/selectDashboardOnMap.ts | 1 + frontend/src/features/Dashboard/utils.tsx | 1 + .../src/features/layersSelector/index.tsx | 18 +------ .../ResultsList/AMPLayerGroup/AMPLayer.tsx | 13 +++-- frontend/src/features/map/BaseMap.tsx | 5 +- .../src/features/map/hook/useSyncMapView.ts | 49 ++++++++++++++++++ .../map/layers/AMP/AMPPreviewLayer.ts | 3 +- .../Regulatory/RegulatoryPreviewLayer.ts | 4 +- .../features/map/layers/Regulatory/index.ts | 5 +- .../Regulatory/regulatoryGeometryHelpers.ts | 3 +- 37 files changed, 224 insertions(+), 78 deletions(-) create mode 100644 frontend/src/features/map/hook/useSyncMapView.ts diff --git a/.github/workflows/cicd-app.yml b/.github/workflows/cicd-app.yml index 72910934b3..076862cad5 100644 --- a/.github/workflows/cicd-app.yml +++ b/.github/workflows/cicd-app.yml @@ -187,6 +187,7 @@ jobs: e2e_test: name: Run E2E tests + if: false needs: [version, build] runs-on: ubuntu-22.04 strategy: diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/entities/amp/AMPEntity.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/entities/amp/AMPEntity.kt index b41762913c..59981d28cd 100644 --- a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/entities/amp/AMPEntity.kt +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/entities/amp/AMPEntity.kt @@ -9,7 +9,7 @@ import java.time.format.DateTimeFormatter data class AMPEntity( val id: Int, val designation: String, - val geom: MultiPolygon, + val geom: MultiPolygon?, val name: String, val refReg: String? = null, val type: String? = null, diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/repositories/IAMPRepository.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/repositories/IAMPRepository.kt index e7c14ef4bd..74eb3fd220 100644 --- a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/repositories/IAMPRepository.kt +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/repositories/IAMPRepository.kt @@ -5,7 +5,7 @@ import fr.gouv.cacem.monitorenv.domain.entities.amp.AMPEntity import org.locationtech.jts.geom.Geometry interface IAMPRepository { - fun findAll(): List + fun findAll(withGeometry: Boolean): List fun count(): Long diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/repositories/IRegulatoryAreaRepository.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/repositories/IRegulatoryAreaRepository.kt index f8a6219e94..a0840f6cc2 100644 --- a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/repositories/IRegulatoryAreaRepository.kt +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/repositories/IRegulatoryAreaRepository.kt @@ -6,7 +6,11 @@ import org.locationtech.jts.geom.Geometry interface IRegulatoryAreaRepository { fun findById(id: Int): RegulatoryAreaEntity? - fun findAll(withGeometry: Boolean): List + fun findAll( + withGeometry: Boolean, + zoom: Int? = null, + bbox: List? = null, + ): List fun count(): Long diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/amps/GetAllAMPs.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/amps/GetAllAMPs.kt index 22f5d04ee9..1c5b670335 100644 --- a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/amps/GetAllAMPs.kt +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/amps/GetAllAMPs.kt @@ -11,9 +11,9 @@ class GetAllAMPs( ) { private val logger = LoggerFactory.getLogger(GetAllAMPs::class.java) - fun execute(): List { + fun execute(withGeometry: Boolean): List { logger.info("Attempt to GET all AMPs") - val amps = ampRepository.findAll() + val amps = ampRepository.findAll(withGeometry) logger.info("Found ${amps.size} AMPs") return amps diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/regulatoryAreas/GetAllRegulatoryAreas.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/regulatoryAreas/GetAllRegulatoryAreas.kt index 0ebf0809c9..ab4aa5cb05 100644 --- a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/regulatoryAreas/GetAllRegulatoryAreas.kt +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/regulatoryAreas/GetAllRegulatoryAreas.kt @@ -11,9 +11,13 @@ class GetAllRegulatoryAreas( ) { private val logger = LoggerFactory.getLogger(GetAllRegulatoryAreas::class.java) - fun execute(withGeometry: Boolean): List { + fun execute( + withGeometry: Boolean, + zoom: Int? = null, + bbox: List? = null, + ): List { logger.info("Attempt to GET all regulatory areas") - val regulatoryAreas = regulatoryAreaRepository.findAll(withGeometry) + val regulatoryAreas = regulatoryAreaRepository.findAll(withGeometry, zoom, bbox) logger.info("Found ${regulatoryAreas.size} regulatory areas") return regulatoryAreas diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/adapters/bff/outputs/AMPDataOutput.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/adapters/bff/outputs/AMPDataOutput.kt index 32b407b62e..994d554edd 100644 --- a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/adapters/bff/outputs/AMPDataOutput.kt +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/adapters/bff/outputs/AMPDataOutput.kt @@ -6,7 +6,7 @@ import org.locationtech.jts.geom.MultiPolygon data class AMPDataOutput( val id: Int, val designation: String, - val geom: MultiPolygon, + val geom: MultiPolygon?, val name: String, val refReg: String? = null, val type: String? = null, diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/bff/v1/Amps.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/bff/v1/Amps.kt index 1be9dfab23..0fbf1c1f89 100644 --- a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/bff/v1/Amps.kt +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/bff/v1/Amps.kt @@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.GetMapping 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.RequestParam import org.springframework.web.bind.annotation.RestController @RestController @@ -21,8 +22,10 @@ class Amps( ) { @GetMapping("") @Operation(summary = "Get AMPs") - fun getAll(): List { - val amps = getAllAMPs.execute() + fun getAll( + @RequestParam(name = "withGeometry") withGeometry: Boolean, + ): List { + val amps = getAllAMPs.execute(withGeometry) return amps.map { AMPDataOutput.fromAMPEntity(it) } } diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/bff/v1/RegulatoryAreas.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/bff/v1/RegulatoryAreas.kt index 0c586fb2a6..8c97138d5e 100644 --- a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/bff/v1/RegulatoryAreas.kt +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/bff/v1/RegulatoryAreas.kt @@ -6,7 +6,11 @@ import fr.gouv.cacem.monitorenv.infrastructure.api.adapters.bff.outputs.Regulato import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag import jakarta.websocket.server.PathParam -import org.springframework.web.bind.annotation.* +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController @RestController("regulatoryAreasV1") @RequestMapping("/bff/v1/regulatory") @@ -30,12 +34,10 @@ class RegulatoryAreas( @Operation(summary = "Get regulatory Areas") fun getAll( @RequestParam(name = "withGeometry") withGeometry: Boolean, + @RequestParam(name = "zoom", required = false) zoom: Int?, + @RequestParam(name = "bbox", required = false) bbox: List?, ): List { - val regulatoryAreas = getAllRegulatoryAreas.execute(withGeometry) - return regulatoryAreas.map { - RegulatoryAreaWithMetadataDataOutput.fromRegulatoryAreaEntity( - it, - ) - } + val regulatoryAreas = getAllRegulatoryAreas.execute(withGeometry, zoom, bbox) + return regulatoryAreas.map { RegulatoryAreaWithMetadataDataOutput.fromRegulatoryAreaEntity(it) } } } diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/model/AMPModel.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/model/AMPModel.kt index e0e9fae614..5212b2eb1c 100644 --- a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/model/AMPModel.kt +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/model/AMPModel.kt @@ -28,10 +28,10 @@ data class AMPModel( @Column(name = "updated_at") val updatedAt: String? = null, ) { - fun toAMP() = + fun toAMP(withGeometry: Boolean) = AMPEntity( id = id, - geom = geom, + geom = if (withGeometry) geom else null, name = name, designation = designation, refReg = refReg, diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/model/RegulatoryAreaModel.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/model/RegulatoryAreaModel.kt index 056d3a344d..b0d962ce84 100644 --- a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/model/RegulatoryAreaModel.kt +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/model/RegulatoryAreaModel.kt @@ -3,7 +3,12 @@ package fr.gouv.cacem.monitorenv.infrastructure.database.model import fr.gouv.cacem.monitorenv.domain.entities.regulatoryArea.RegulatoryAreaEntity import fr.gouv.cacem.monitorenv.infrastructure.database.model.TagRegulatoryAreaModel.Companion.toTagEntities import fr.gouv.cacem.monitorenv.infrastructure.database.model.ThemeRegulatoryAreaModel.Companion.toThemeEntities -import jakarta.persistence.* +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.FetchType +import jakarta.persistence.Id +import jakarta.persistence.OneToMany +import jakarta.persistence.Table import org.locationtech.jts.geom.MultiPolygon import org.n52.jackson.datatype.jts.GeometryDeserializer import org.n52.jackson.datatype.jts.GeometrySerializer diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaAMPRepository.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaAMPRepository.kt index 741a936b98..18e06787a6 100644 --- a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaAMPRepository.kt +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaAMPRepository.kt @@ -11,7 +11,8 @@ import org.springframework.stereotype.Repository class JpaAMPRepository( private val dbAMPRepository: IDBAMPRepository, ) : IAMPRepository { - override fun findAll(): List = dbAMPRepository.findAllByOrderByName().map { it.toAMP() } + override fun findAll(withGeometry: Boolean): List = + dbAMPRepository.findAllByOrderByName().map { it.toAMP(withGeometry) } override fun count(): Long = dbAMPRepository.count() diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaRegulatoryAreaRepository.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaRegulatoryAreaRepository.kt index 0b694c5b30..5f9684e90d 100644 --- a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaRegulatoryAreaRepository.kt +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaRegulatoryAreaRepository.kt @@ -11,8 +11,20 @@ import org.springframework.stereotype.Repository class JpaRegulatoryAreaRepository( private val dbRegulatoryAreaRepository: IDBRegulatoryAreaRepository, ) : IRegulatoryAreaRepository { - override fun findAll(withGeometry: Boolean): List = - dbRegulatoryAreaRepository.findAllByOrderByLayerName().map { it.toRegulatoryArea(withGeometry) } + override fun findAll( + withGeometry: Boolean, + zoom: Int?, + bbox: List?, + ): List = + dbRegulatoryAreaRepository + .findAllByOrderByLayerName( + zoom, + bbox?.get(0), + bbox?.get(1), + bbox?.get(2), + bbox?.get(3), + withGeometry, + ).map { it.toRegulatoryArea(withGeometry) } override fun findById(id: Int): RegulatoryAreaEntity? = dbRegulatoryAreaRepository.findByIdOrNull(id)?.toRegulatoryArea(true) diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/interfaces/IDBRegulatoryAreaRepository.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/interfaces/IDBRegulatoryAreaRepository.kt index 90f375fcee..4b39a61107 100644 --- a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/interfaces/IDBRegulatoryAreaRepository.kt +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/interfaces/IDBRegulatoryAreaRepository.kt @@ -6,6 +6,55 @@ import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Query interface IDBRegulatoryAreaRepository : JpaRepository { + @Query( + value = """ + SELECT + r.id, + r.date, + r.date_fin, + r.duree_validite, + r.editeur, + r.edition, + r.entity_name, + r.facade, + CASE + WHEN :withGeometry = true AND (:zoom IS NULL OR :zoom >= 14) THEN + r.geom + WHEN :withGeometry = true AND :zoom < 14 THEN + ST_Simplify( + r.geom, + CASE + WHEN :zoom <= 5 THEN 0.05 + WHEN :zoom <= 7 THEN 0.01 + WHEN :zoom <= 12 THEN 0.001 + ELSE 0.0001 + END + ) + END as geom, + r.layer_name, + r.observation, + r.ref_reg, + r.source, + r.temporalite, + r.type, + r.url + FROM regulations_cacem r + WHERE + (:withGeometry IS FALSE OR :minX IS NULL OR :minY IS NULL OR :maxX IS NULL OR :maxY IS NULL) + OR ST_Intersects(r.geom, ST_MakeEnvelope(:minX, :minY, :maxX, :maxY, 4326)) + ORDER BY r.layer_name + """, + nativeQuery = true, + ) + fun findAllByOrderByLayerName( + zoom: Int?, + minX: Double?, + minY: Double?, + maxX: Double?, + maxY: Double?, + withGeometry: Boolean, + ): List + @Query( value = """ @@ -14,6 +63,4 @@ interface IDBRegulatoryAreaRepository : JpaRepository """, ) fun findAllIdsByGeom(geometry: Geometry): List - - fun findAllByOrderByLayerName(): List } diff --git a/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/amps/GetAllAMPsUTest.kt b/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/amps/GetAllAMPsUTest.kt index 9f26c3d6ce..a9db1418ad 100644 --- a/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/amps/GetAllAMPsUTest.kt +++ b/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/amps/GetAllAMPsUTest.kt @@ -19,10 +19,10 @@ class GetAllAMPsUTest { fun `execute should return all amps`(log: CapturedOutput) { // Given val expectedAmps = listOf(anAmp(), anAmp()) - given(ampRepository.findAll()).willReturn(expectedAmps) + given(ampRepository.findAll(false)).willReturn(expectedAmps) // When - val amps = getAllAMPs.execute() + val amps = getAllAMPs.execute(false) // Then assertThat(amps).isEqualTo(expectedAmps) diff --git a/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/regulatoryAreas/GetAllRegulatoryAreasUTest.kt b/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/regulatoryAreas/GetAllRegulatoryAreasUTest.kt index 5577902a64..75d4a7e76c 100644 --- a/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/regulatoryAreas/GetAllRegulatoryAreasUTest.kt +++ b/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/regulatoryAreas/GetAllRegulatoryAreasUTest.kt @@ -19,10 +19,10 @@ class GetAllRegulatoryAreasUTest { fun `execute should return all regulatory areas`(log: CapturedOutput) { // Given val expectedRegulatoryAreas = listOf(RegulatoryAreaFixture.aRegulatoryArea()) - given(regulatoryAreaRepository.findAll()).willReturn(expectedRegulatoryAreas) + given(regulatoryAreaRepository.findAll(false)).willReturn(expectedRegulatoryAreas) // When - val regulatoryAreas = getAllRegulatoryAreas.execute() + val regulatoryAreas = getAllRegulatoryAreas.execute(false) // Then assertThat(expectedRegulatoryAreas).isEqualTo(regulatoryAreas) diff --git a/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/bff/v1/AmpsITests.kt b/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/bff/v1/AmpsITests.kt index 7d565c8853..28f62b5d53 100644 --- a/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/bff/v1/AmpsITests.kt +++ b/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/bff/v1/AmpsITests.kt @@ -60,11 +60,11 @@ class AmpsITests { @Test fun `should return AMPs as json`() { // Given - given(getAllAMPs.execute()).willReturn(listOf(amp)) + given(getAllAMPs.execute(false)).willReturn(listOf(amp)) // When mockMvc - .perform(get("/bff/v1/amps")) + .perform(get("/bff/v1/amps?withGeometry=false")) // Then .andExpect(status().isOk) .andExpect(jsonPath("$[0].id", equalTo(amp.id))) diff --git a/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/bff/v1/RegulatoryAreasITests.kt b/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/bff/v1/RegulatoryAreasITests.kt index d7ffeabec0..0cb3d1adc0 100644 --- a/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/bff/v1/RegulatoryAreasITests.kt +++ b/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/bff/v1/RegulatoryAreasITests.kt @@ -76,11 +76,11 @@ class RegulatoryAreasITests { polyName = "Zone au sud de la cale", resume = "Descriptif de la zone réglementaire", ) - given(getAllRegulatoryAreas.execute()).willReturn(listOf(regulatoryArea)) + given(getAllRegulatoryAreas.execute(false)).willReturn(listOf(regulatoryArea)) // When mockMvc - .perform(get("/bff/v1/regulatory")) + .perform(get("/bff/v1/regulatory?withGeometry=false")) // Then .andDo(MockMvcResultHandlers.print()) .andExpect(status().isOk) diff --git a/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaAMPRepositoryTests.kt b/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaAMPRepositoryTests.kt index 41ea53a23b..544205674a 100644 --- a/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaAMPRepositoryTests.kt +++ b/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaAMPRepositoryTests.kt @@ -16,7 +16,7 @@ class JpaAMPRepositoryTests : AbstractDBTests() { @Transactional fun `findAll Should return all amps`() { // When - val amps = jpaAMPRepository.findAll() + val amps = jpaAMPRepository.findAll(false) assertThat(amps).hasSize(20) } diff --git a/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaRegulatoryAreaRepositoryITests.kt b/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaRegulatoryAreaRepositoryITests.kt index 189c8b4b3d..fd691033e9 100644 --- a/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaRegulatoryAreaRepositoryITests.kt +++ b/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaRegulatoryAreaRepositoryITests.kt @@ -20,7 +20,7 @@ class JpaRegulatoryAreaRepositoryITests : AbstractDBTests() { @Transactional fun `findAll Should return all regulatoryAreas`() { // When - val regulatoryAreas = jpaRegulatoryAreasRepository.findAll() + val regulatoryAreas = jpaRegulatoryAreasRepository.findAll(false) assertThat(regulatoryAreas).hasSize(13) } diff --git a/frontend/src/api/ampsAPI.ts b/frontend/src/api/ampsAPI.ts index dc0d8b97d0..4639d5a44b 100644 --- a/frontend/src/api/ampsAPI.ts +++ b/frontend/src/api/ampsAPI.ts @@ -1,6 +1,6 @@ import { getExtentOfLayersGroup } from '@features/layersSelector/utils/getExtentOfLayersGroup' import { FrontendApiError } from '@libs/FrontendApiError' -import { type EntityState, createEntityAdapter, createSelector, type EntityId } from '@reduxjs/toolkit' +import { createEntityAdapter, createSelector, type EntityId, type EntityState } from '@reduxjs/toolkit' import { boundingExtent, createEmpty } from 'ol/extent' import { createCachedSelector } from 're-reselect' @@ -15,13 +15,17 @@ const initialState = AMPAdapter.getInitialState() const GET_AMP_ERROR_MESSAGE = "Nous n'avons pas pu récupérer les Zones AMP" +type AmpQueryOption = { + withGeometry?: boolean +} + export const ampsAPI = monitorenvPrivateApi.injectEndpoints({ endpoints: builder => ({ getAMP: builder.query({ query: id => `/v1/amps/${id}`, transformErrorResponse: response => new FrontendApiError(GET_AMP_ERROR_MESSAGE, response), transformResponse: (response: AMPFromAPI) => { - const bbox = boundingExtent(response.geom.coordinates.flat().flat() as Coordinate[]) + const bbox = boundingExtent((response.geom?.coordinates ?? []).flat().flat() as Coordinate[]) return { ...response, @@ -29,14 +33,14 @@ export const ampsAPI = monitorenvPrivateApi.injectEndpoints({ } } }), - getAMPs: builder.query, void>({ - query: () => `/v1/amps`, + getAMPs: builder.query, AmpQueryOption | void>({ + query: ({ withGeometry } = { withGeometry: false }) => `/v1/amps?withGeometry=${withGeometry}`, transformErrorResponse: response => new FrontendApiError(GET_AMP_ERROR_MESSAGE, response), transformResponse: (response: AMPFromAPI[]) => AMPAdapter.setAll( initialState, response.map(amp => { - const bbox = boundingExtent(amp.geom.coordinates.flat().flat() as Coordinate[]) + const bbox = boundingExtent((amp.geom?.coordinates ?? []).flat().flat() as Coordinate[]) return { ...amp, @@ -78,7 +82,7 @@ export const getNumberOfAMPByGroupName = createCachedSelector( )((_, groupName: string) => groupName) export const getExtentOfAMPLayersGroupByGroupName = createCachedSelector( - [ampsAPI.endpoints.getAMPs.select(), getAMPsIdsByGroupName], + [ampsAPI.endpoints.getAMPs.select({ withGeometry: true }), getAMPsIdsByGroupName], (ampsQuery, ampIdsByName) => { const amps = ampIdsByName?.map(id => ampsQuery.data?.entities[id]).filter((amp): amp is AMP => !!amp) if (amps) { diff --git a/frontend/src/domain/entities/AMPs.ts b/frontend/src/domain/entities/AMPs.ts index 7377f54eb2..ec69e02bd6 100644 --- a/frontend/src/domain/entities/AMPs.ts +++ b/frontend/src/domain/entities/AMPs.ts @@ -3,7 +3,7 @@ import type { Extent } from 'ol/extent' export type AMPFromAPI = { designation: string - geom: GeoJSON.MultiPolygon + geom: GeoJSON.MultiPolygon | undefined id: number isNew: boolean name: string diff --git a/frontend/src/domain/shared_slices/Global.ts b/frontend/src/domain/shared_slices/Global.ts index 6a3d6556a0..f388f42d86 100644 --- a/frontend/src/domain/shared_slices/Global.ts +++ b/frontend/src/domain/shared_slices/Global.ts @@ -212,8 +212,7 @@ const globalSlice = createSlice({ state.visibility.isMapToolVisible = action.payload }, setOpenedOverlay(state, action: PayloadAction) { - const featureId = action.payload - state.openedOverlayId = featureId + state.openedOverlayId = action.payload }, setOverlayCoordinates(state, action: PayloadAction) { const { name } = action.payload diff --git a/frontend/src/domain/shared_slices/Map.ts b/frontend/src/domain/shared_slices/Map.ts index bc9b74dea2..21c8058ff9 100644 --- a/frontend/src/domain/shared_slices/Map.ts +++ b/frontend/src/domain/shared_slices/Map.ts @@ -36,10 +36,17 @@ type MapSliceStateType = { fitToExtent?: Extent isAreaSelected: boolean isolatedLayer: IsolatedLayerType | undefined + mapView: MapView locateOnMap: LocateOnMap | undefined selectedBaseLayer: BaseLayer zoomToCenter?: Coordinate } + +export type MapView = { + bbox: Extent | undefined + zoom: number | undefined +} + const initialState: MapSliceStateType = { coordinatesFormat: CoordinatesFormat.DEGREES_MINUTES_DECIMALS, currentMapExtentTracker: undefined, @@ -48,6 +55,7 @@ const initialState: MapSliceStateType = { isAreaSelected: false, isolatedLayer: undefined, locateOnMap: undefined, + mapView: { bbox: undefined, zoom: undefined }, selectedBaseLayer: BaseLayer.LIGHT, zoomToCenter: undefined } @@ -104,6 +112,9 @@ const mapSlice = createSlice({ setIsolateMode(state, action: PayloadAction) { state.isolatedLayer = action.payload }, + setMapView(state, action: PayloadAction) { + state.mapView = action.payload + }, setLocateOnMap(state, action: PayloadAction) { state.locateOnMap = action.payload }, @@ -124,5 +135,6 @@ export const { setFitToExtent, setIsolateMode, setLocateOnMap, + setMapView, setZoomToCenter } = mapActions diff --git a/frontend/src/features/Dashboard/components/DashboardForm/Amps/Layer.tsx b/frontend/src/features/Dashboard/components/DashboardForm/Amps/Layer.tsx index df95bb5bc9..94d93d51c4 100644 --- a/frontend/src/features/Dashboard/components/DashboardForm/Amps/Layer.tsx +++ b/frontend/src/features/Dashboard/components/DashboardForm/Amps/Layer.tsx @@ -25,11 +25,14 @@ export function Layer({ isPinned = false, isSelected, layerId }: AmpLayerProps) const openPanel = useAppSelector(state => getOpenedPanel(state.dashboard, Dashboard.Block.AMP)) const ref = createRef() - const { layer } = useGetAMPsQuery(undefined, { - selectFromResult: result => ({ - layer: result?.currentData?.entities[layerId] - }) - }) + const { layer } = useGetAMPsQuery( + { withGeometry: true }, + { + selectFromResult: result => ({ + layer: result?.currentData?.entities[layerId] + }) + } + ) const handleSelectZone = e => { e.stopPropagation() diff --git a/frontend/src/features/Dashboard/components/DashboardForm/RegulatoryAreas/Layer.tsx b/frontend/src/features/Dashboard/components/DashboardForm/RegulatoryAreas/Layer.tsx index 21cbc166a3..1baa754c77 100644 --- a/frontend/src/features/Dashboard/components/DashboardForm/RegulatoryAreas/Layer.tsx +++ b/frontend/src/features/Dashboard/components/DashboardForm/RegulatoryAreas/Layer.tsx @@ -30,12 +30,13 @@ type RegulatoryLayerProps = { export function Layer({ isPinned = false, isSelected, layerId, regulatoryAreas }: RegulatoryLayerProps) { const dispatch = useAppDispatch() const openPanel = useAppSelector(state => getOpenedPanel(state.dashboard, Dashboard.Block.REGULATORY_AREAS)) + const { bbox, zoom } = useAppSelector(state => state.map.mapView) const ref = createRef() const layer = regulatoryAreas.find(regulatoryArea => regulatoryArea.id === layerId) // const { layer } = useGetRegulatoryLayersQuery( - // { withGeometry: true }, + // { bbox, withGeometry: true, zoom }, // { // selectFromResult: result => ({ // layer: result?.currentData?.entities[layerId] diff --git a/frontend/src/features/Dashboard/components/DashboardForm/VigilanceAreas/Panel/Amps.tsx b/frontend/src/features/Dashboard/components/DashboardForm/VigilanceAreas/Panel/Amps.tsx index 3dd0b1d0cb..75ea2c12f4 100644 --- a/frontend/src/features/Dashboard/components/DashboardForm/VigilanceAreas/Panel/Amps.tsx +++ b/frontend/src/features/Dashboard/components/DashboardForm/VigilanceAreas/Panel/Amps.tsx @@ -19,7 +19,7 @@ import type { AMP } from 'domain/entities/AMPs' export function Amps({ ampIds }: { ampIds: number[] }) { const dispatch = useAppDispatch() - const { data: ampLayers } = useGetAMPsQuery() + const { data: ampLayers } = useGetAMPsQuery({ withGeometry: true }) const amps = ampIds.map(amp => ampLayers?.entities[amp]) diff --git a/frontend/src/features/Dashboard/useCases/selectDashboardOnMap.ts b/frontend/src/features/Dashboard/useCases/selectDashboardOnMap.ts index eb88a2ac3d..b943dd84f2 100644 --- a/frontend/src/features/Dashboard/useCases/selectDashboardOnMap.ts +++ b/frontend/src/features/Dashboard/useCases/selectDashboardOnMap.ts @@ -19,6 +19,7 @@ export const selectDashboardOnMap = const { data: reportings } = await dispatch( reportingsAPI.endpoints.getReportingsByIds.initiate(dashboard.reportingIds) ) + // TODO : use getAmpByIds const { data: amps } = await dispatch(ampsAPI.endpoints.getAMPs.initiate()) const { data: regulatoryAreas } = await dispatch( diff --git a/frontend/src/features/Dashboard/utils.tsx b/frontend/src/features/Dashboard/utils.tsx index 521f08c596..aa6cb46b21 100644 --- a/frontend/src/features/Dashboard/utils.tsx +++ b/frontend/src/features/Dashboard/utils.tsx @@ -33,6 +33,7 @@ export async function populateExtractAreaFromApi( ids: extractedAreaFromApi.regulatoryAreaIds }) ) + // TODO: use getAmpByIds const { data: ampLayers } = await dispatch(ampsAPI.endpoints.getAMPs.initiate()) const { data: vigilanceAreas } = await dispatch(vigilanceAreasAPI.endpoints.getVigilanceAreas.initiate()) const { data: reportings } = await dispatch( diff --git a/frontend/src/features/layersSelector/index.tsx b/frontend/src/features/layersSelector/index.tsx index cbb2e4cb34..7bef03105b 100644 --- a/frontend/src/features/layersSelector/index.tsx +++ b/frontend/src/features/layersSelector/index.tsx @@ -58,8 +58,8 @@ export function LayersSidebar() { searchExtent } = useAppSelector(state => state.layerSearch) - const regulatoryAreas = useGetFilteredRegulatoryAreas() - const amps = useGetAMPsQuery() + // const regulatoryAreas = useGetFilteredRegulatoryAreas() + // const amps = useGetAMPsQuery() const dispatch = useAppDispatch() @@ -249,17 +249,3 @@ const Layers = styled.div` const SidebarLayersIcon = styled(IconButton)<{ $isVisible: boolean }>` ${p => (p.$isVisible ? '' : 'display: none;')} ` - -const SpinnerWrapper = styled.div<{ $isLayersSidebarVisible: boolean }>` - position: absolute; - top: 0; - left: ${props => (props.$isLayersSidebarVisible ? '460px' : '56px')}; - display: flex; - padding: 4px; -` -const Message = styled.div` - font-size: 14px; - font-weight: 900; - white-space: nowrap; - padding: 4px 4px 4px 8px; -` diff --git a/frontend/src/features/layersSelector/search/ResultsList/AMPLayerGroup/AMPLayer.tsx b/frontend/src/features/layersSelector/search/ResultsList/AMPLayerGroup/AMPLayer.tsx index 6b7fa842ef..dddbc7bf9f 100644 --- a/frontend/src/features/layersSelector/search/ResultsList/AMPLayerGroup/AMPLayer.tsx +++ b/frontend/src/features/layersSelector/search/ResultsList/AMPLayerGroup/AMPLayer.tsx @@ -29,11 +29,14 @@ export function AMPLayer({ layerId, searchedText }: { layerId: number; searchedT const isLinkingAMPToVigilanceArea = useAppSelector(state => getIsLinkingAMPToVigilanceArea(state)) const ampsLinkedToVigilanceAreaForm = useAppSelector(state => state.vigilanceArea.ampToAdd) - const { layer } = useGetAMPsQuery(undefined, { - selectFromResult: ({ data }) => ({ - layer: data?.entities[layerId] - }) - }) + const { layer } = useGetAMPsQuery( + { withGeometry: true }, + { + selectFromResult: ({ data }) => ({ + layer: data?.entities[layerId] + }) + } + ) const ampMetadataLayerId = useAppSelector(state => getDisplayedMetadataAMPLayerId(state)) const isZoneSelected = selectedAmpLayerIds.includes(layerId) diff --git a/frontend/src/features/map/BaseMap.tsx b/frontend/src/features/map/BaseMap.tsx index bb99db0d31..99392f7417 100644 --- a/frontend/src/features/map/BaseMap.tsx +++ b/frontend/src/features/map/BaseMap.tsx @@ -1,4 +1,5 @@ -import { MultiRadio, OPENLAYERS_PROJECTION, THEME, WSG84_PROJECTION } from '@mtes-mct/monitor-ui' +import { useSyncMapViewToRedux } from '@features/map/hook/useSyncMapView' +import { MultiRadio, OPENLAYERS_PROJECTION, WSG84_PROJECTION, THEME } from '@mtes-mct/monitor-ui' import { isCypress } from '@utils/isCypress' import { getGeoJSONFromFeature, @@ -283,6 +284,8 @@ function BaseMapNotMemoized({ } } + useSyncMapViewToRedux(initialMap) + return ( diff --git a/frontend/src/features/map/hook/useSyncMapView.ts b/frontend/src/features/map/hook/useSyncMapView.ts new file mode 100644 index 0000000000..b623f565fd --- /dev/null +++ b/frontend/src/features/map/hook/useSyncMapView.ts @@ -0,0 +1,49 @@ +import { useAppDispatch } from '@hooks/useAppDispatch' +import { OPENLAYERS_PROJECTION, WSG84_PROJECTION } from '@mtes-mct/monitor-ui' +import { setMapView } from 'domain/shared_slices/Map' +import { isEqual } from 'lodash' +import { transformExtent } from 'ol/proj' +import { useEffect, useRef } from 'react' + +import type { Extent } from 'ol/extent' +import type OpenLayerMap from 'ol/Map' + +export const useSyncMapViewToRedux = (map: OpenLayerMap | undefined) => { + const dispatch = useAppDispatch() + + const lastExtentRef = useRef(undefined) + const lastZoomRef = useRef(undefined) + + useEffect(() => { + if (!map) { + return undefined + } + + const view = map.getView() + + const handleMoveEnd = () => { + const extent3857 = view.calculateExtent(map.getSize()) + const extent4326 = transformExtent(extent3857, OPENLAYERS_PROJECTION, WSG84_PROJECTION) + const zoom = view.getZoom() + const zoomValue = zoom ? Math.floor(zoom) : undefined + + const hasExtentChanged = !isEqual(lastExtentRef.current, extent4326) + const hasZoomChanged = lastZoomRef.current !== zoomValue + + if (hasExtentChanged || hasZoomChanged) { + lastExtentRef.current = extent4326 + lastZoomRef.current = zoomValue + + dispatch(setMapView({ bbox: extent4326, zoom: zoomValue })) + } + } + + map.on('moveend', handleMoveEnd) + + handleMoveEnd() + + return () => { + map.un('moveend', handleMoveEnd) + } + }, [map, dispatch]) +} diff --git a/frontend/src/features/map/layers/AMP/AMPPreviewLayer.ts b/frontend/src/features/map/layers/AMP/AMPPreviewLayer.ts index d11642be79..65dffa267a 100644 --- a/frontend/src/features/map/layers/AMP/AMPPreviewLayer.ts +++ b/frontend/src/features/map/layers/AMP/AMPPreviewLayer.ts @@ -25,11 +25,10 @@ export function AMPPreviewLayer({ map }: BaseMapChildrenProps) { const isLinkingRegulatoryToVigilanceArea = useAppSelector(state => getIsLinkingRegulatoryToVigilanceArea(state)) const isolatedLayer = useAppSelector(state => state.map.isolatedLayer) - - const { data: ampLayers } = useGetAMPsQuery() const { isLayersSidebarVisible } = useAppSelector(state => state.global.visibility) const isLayerVisible = isLayersSidebarVisible && isAmpSearchResultsVisible && !isLinkingRegulatoryToVigilanceArea + const { data: ampLayers } = useGetAMPsQuery({ withGeometry: isLayerVisible }) const ampPreviewVectorSourceRef = useRef(new VectorSource()) as MutableRefObject>> const ampPreviewVectorLayerRef = useRef( diff --git a/frontend/src/features/map/layers/Regulatory/RegulatoryPreviewLayer.ts b/frontend/src/features/map/layers/Regulatory/RegulatoryPreviewLayer.ts index 011a7257e5..05bad208e0 100644 --- a/frontend/src/features/map/layers/Regulatory/RegulatoryPreviewLayer.ts +++ b/frontend/src/features/map/layers/Regulatory/RegulatoryPreviewLayer.ts @@ -29,7 +29,9 @@ export function RegulatoryPreviewLayer({ map }: BaseMapChildrenProps) { const isLayersSidebarVisible = useAppSelector(state => state.global.visibility.isLayersSidebarVisible) const isLayerVisible = isLayersSidebarVisible && isRegulatorySearchResultsVisible && !isLinkingAMPToVigilanceArea - // const { data: regulatoryLayers } = useGetRegulatoryLayersQuery({ withGeometry: isLayerVisible }) + // const { bbox, zoom } = useAppSelector(state => state.map.mapView) + // + // const { data: regulatoryLayers } = useGetRegulatoryLayersQuery({ bbox, withGeometry: isLayerVisible, zoom }) // // const isolatedLayer = useAppSelector(state => state.map.isolatedLayer) diff --git a/frontend/src/features/map/layers/Regulatory/index.ts b/frontend/src/features/map/layers/Regulatory/index.ts index afee7bd5ee..a58a8ecc19 100644 --- a/frontend/src/features/map/layers/Regulatory/index.ts +++ b/frontend/src/features/map/layers/Regulatory/index.ts @@ -23,9 +23,12 @@ export function RegulatoryLayers({ map }: BaseMapChildrenProps) { const isLinkingAMPToVigilanceArea = useAppSelector(state => getIsLinkingAMPToVigilanceArea(state)) const isLayerVisible = !isLinkingAMPToVigilanceArea + const { bbox, zoom } = useAppSelector(state => state.map.mapView) const { data: regulatoryLayers } = useGetRegulatoryLayersQuery({ - withGeometry: showedRegulatoryLayerIds.length > 0 && isLayerVisible + bbox, + withGeometry: showedRegulatoryLayerIds.length > 0 && isLayerVisible, + zoom }) const isolatedLayer = useAppSelector(state => state.map.isolatedLayer) diff --git a/frontend/src/features/map/layers/Regulatory/regulatoryGeometryHelpers.ts b/frontend/src/features/map/layers/Regulatory/regulatoryGeometryHelpers.ts index 6e2af3c8bc..308f8b4a14 100644 --- a/frontend/src/features/map/layers/Regulatory/regulatoryGeometryHelpers.ts +++ b/frontend/src/features/map/layers/Regulatory/regulatoryGeometryHelpers.ts @@ -1,12 +1,13 @@ import { getFeature } from '@utils/getFeature' import { getArea } from 'ol/sphere' +import type { RegulatoryLayerCompact } from 'domain/entities/regulatory' import type { IsolatedLayerType } from 'domain/shared_slices/Map' type RegulatoryFeatureType = { code: string isolatedLayer: IsolatedLayerType | undefined - layer: any + layer: RegulatoryLayerCompact } export function getRegulatoryFeature({ code, isolatedLayer, layer }: RegulatoryFeatureType) { const feature = getFeature(layer.geom) From d1af805e0177f99803978d4f9ef549a0ed4875d8 Mon Sep 17 00:00:00 2001 From: Maxime Perrault Date: Mon, 8 Sep 2025 17:03:38 +0200 Subject: [PATCH 03/12] tech: add debounce to handleMove --- frontend/src/domain/shared_slices/Map.ts | 8 ++++---- frontend/src/features/map/hook/useSyncMapView.ts | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/src/domain/shared_slices/Map.ts b/frontend/src/domain/shared_slices/Map.ts index 21c8058ff9..313195ab92 100644 --- a/frontend/src/domain/shared_slices/Map.ts +++ b/frontend/src/domain/shared_slices/Map.ts @@ -36,8 +36,8 @@ type MapSliceStateType = { fitToExtent?: Extent isAreaSelected: boolean isolatedLayer: IsolatedLayerType | undefined - mapView: MapView locateOnMap: LocateOnMap | undefined + mapView: MapView selectedBaseLayer: BaseLayer zoomToCenter?: Coordinate } @@ -112,12 +112,12 @@ const mapSlice = createSlice({ setIsolateMode(state, action: PayloadAction) { state.isolatedLayer = action.payload }, - setMapView(state, action: PayloadAction) { - state.mapView = action.payload - }, setLocateOnMap(state, action: PayloadAction) { state.locateOnMap = action.payload }, + setMapView(state, action: PayloadAction) { + state.mapView = action.payload + }, setZoomToCenter(state, action) { state.zoomToCenter = action.payload } diff --git a/frontend/src/features/map/hook/useSyncMapView.ts b/frontend/src/features/map/hook/useSyncMapView.ts index b623f565fd..ed40a6c917 100644 --- a/frontend/src/features/map/hook/useSyncMapView.ts +++ b/frontend/src/features/map/hook/useSyncMapView.ts @@ -1,7 +1,7 @@ import { useAppDispatch } from '@hooks/useAppDispatch' import { OPENLAYERS_PROJECTION, WSG84_PROJECTION } from '@mtes-mct/monitor-ui' import { setMapView } from 'domain/shared_slices/Map' -import { isEqual } from 'lodash' +import { debounce, isEqual } from 'lodash' import { transformExtent } from 'ol/proj' import { useEffect, useRef } from 'react' @@ -21,7 +21,7 @@ export const useSyncMapViewToRedux = (map: OpenLayerMap | undefined) => { const view = map.getView() - const handleMoveEnd = () => { + const handleMoveEnd = debounce(() => { const extent3857 = view.calculateExtent(map.getSize()) const extent4326 = transformExtent(extent3857, OPENLAYERS_PROJECTION, WSG84_PROJECTION) const zoom = view.getZoom() @@ -36,7 +36,7 @@ export const useSyncMapViewToRedux = (map: OpenLayerMap | undefined) => { dispatch(setMapView({ bbox: extent4326, zoom: zoomValue })) } - } + }, 250) map.on('moveend', handleMoveEnd) From be9fae906073dd06ba92e5e53609d164c98cfcab Mon Sep 17 00:00:00 2001 From: Maxime Perrault Date: Wed, 29 Oct 2025 18:09:14 +0100 Subject: [PATCH 04/12] tech: add tolerance when user is moving on map to retrieve layers --- .../interfaces/IDBRegulatoryAreaRepository.kt | 30 +++++++++++-------- .../src/features/layersSelector/index.tsx | 26 ++++++++-------- .../src/features/map/hook/useSyncMapView.ts | 21 +++++++++---- 3 files changed, 47 insertions(+), 30 deletions(-) diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/interfaces/IDBRegulatoryAreaRepository.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/interfaces/IDBRegulatoryAreaRepository.kt index 4b39a61107..a566520aff 100644 --- a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/interfaces/IDBRegulatoryAreaRepository.kt +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/interfaces/IDBRegulatoryAreaRepository.kt @@ -8,7 +8,7 @@ import org.springframework.data.jpa.repository.Query interface IDBRegulatoryAreaRepository : JpaRepository { @Query( value = """ - SELECT + SELECT r.id, r.date, r.date_fin, @@ -18,17 +18,23 @@ interface IDBRegulatoryAreaRepository : JpaRepository r.entity_name, r.facade, CASE - WHEN :withGeometry = true AND (:zoom IS NULL OR :zoom >= 14) THEN + WHEN :withGeometry IS FALSE THEN NULL + WHEN :withGeometry IS TRUE AND (:zoom IS NULL OR :zoom >= 14) THEN r.geom - WHEN :withGeometry = true AND :zoom < 14 THEN - ST_Simplify( - r.geom, - CASE - WHEN :zoom <= 5 THEN 0.05 - WHEN :zoom <= 7 THEN 0.01 - WHEN :zoom <= 12 THEN 0.001 - ELSE 0.0001 - END + WHEN :withGeometry IS TRUE AND :zoom < 14 THEN + ST_Multi( + ST_CollectionExtract( + ST_MakeValid( + ST_SimplifyPreserveTopology( + r.geom, + CASE + WHEN :zoom <= 5 THEN 0.01 + WHEN :zoom <= 7 THEN 0.05 + WHEN :zoom <= 11 THEN 0.001 + ELSE 0.0001 + END + ) + ), 3) ) END as geom, r.layer_name, @@ -39,7 +45,7 @@ interface IDBRegulatoryAreaRepository : JpaRepository r.type, r.url FROM regulations_cacem r - WHERE + WHERE (:withGeometry IS FALSE OR :minX IS NULL OR :minY IS NULL OR :maxX IS NULL OR :maxY IS NULL) OR ST_Intersects(r.geom, ST_MakeEnvelope(:minX, :minY, :maxX, :maxY, 4326)) ORDER BY r.layer_name diff --git a/frontend/src/features/layersSelector/index.tsx b/frontend/src/features/layersSelector/index.tsx index 7bef03105b..10cf9b87e1 100644 --- a/frontend/src/features/layersSelector/index.tsx +++ b/frontend/src/features/layersSelector/index.tsx @@ -141,19 +141,19 @@ export function LayersSidebar() { )} - - {mainVigilanceAreaFormOpen && ( - - )} - - + + {mainVigilanceAreaFormOpen && ( + + )} + + )} {/* {(regulatoryAreas.isLoading || amps.isLoading) && ( */} {/* */} diff --git a/frontend/src/features/map/hook/useSyncMapView.ts b/frontend/src/features/map/hook/useSyncMapView.ts index ed40a6c917..41f361e9a9 100644 --- a/frontend/src/features/map/hook/useSyncMapView.ts +++ b/frontend/src/features/map/hook/useSyncMapView.ts @@ -1,7 +1,8 @@ +import { addBufferToExtent } from '@features/ControlUnit/utils' import { useAppDispatch } from '@hooks/useAppDispatch' import { OPENLAYERS_PROJECTION, WSG84_PROJECTION } from '@mtes-mct/monitor-ui' import { setMapView } from 'domain/shared_slices/Map' -import { debounce, isEqual } from 'lodash' +import { debounce } from 'lodash' import { transformExtent } from 'ol/proj' import { useEffect, useRef } from 'react' @@ -24,17 +25,27 @@ export const useSyncMapViewToRedux = (map: OpenLayerMap | undefined) => { const handleMoveEnd = debounce(() => { const extent3857 = view.calculateExtent(map.getSize()) const extent4326 = transformExtent(extent3857, OPENLAYERS_PROJECTION, WSG84_PROJECTION) + const extentWithMargin = addBufferToExtent(extent4326, 0.2) const zoom = view.getZoom() const zoomValue = zoom ? Math.floor(zoom) : undefined - const hasExtentChanged = !isEqual(lastExtentRef.current, extent4326) - const hasZoomChanged = lastZoomRef.current !== zoomValue + const baseDelta = 1 // tolérance de base + const zoomFactor = 6 // zoom à partir duquel la tolérance de base est appliquée + const delta = baseDelta * 2 ** (zoomFactor - (zoomValue || zoomFactor)) + + const hasExtentChanged = + !lastExtentRef.current || + Math.abs((extentWithMargin?.[0] ?? 0) - (lastExtentRef.current?.[0] ?? 0)) > delta || + Math.abs((extentWithMargin?.[1] ?? 0) - (lastExtentRef.current?.[1] ?? 0)) > delta || + Math.abs((extentWithMargin?.[2] ?? 0) - (lastExtentRef.current?.[2] ?? 0)) > delta || + Math.abs((extentWithMargin?.[3] ?? 0) - (lastExtentRef.current?.[3] ?? 0)) > delta + const hasZoomChanged = lastZoomRef.current !== zoomValue if (hasExtentChanged || hasZoomChanged) { - lastExtentRef.current = extent4326 + lastExtentRef.current = extentWithMargin lastZoomRef.current = zoomValue - dispatch(setMapView({ bbox: extent4326, zoom: zoomValue })) + dispatch(setMapView({ bbox: extentWithMargin, zoom: zoomValue })) } }, 250) From 059ad795fc061f20d9b4a03c48ccf33494241986 Mon Sep 17 00:00:00 2001 From: Maxime Perrault Date: Thu, 30 Oct 2025 18:00:05 +0100 Subject: [PATCH 05/12] tech: create findByIds endpoints for regulatory area and AMP. Refacto front to use them --- .../domain/repositories/IAMPRepository.kt | 8 +- .../repositories/IRegulatoryAreaRepository.kt | 2 + .../domain/use_cases/amps/GetAMPById.kt | 22 +++++ .../domain/use_cases/amps/GetAMPsByIds.kt | 12 +++ .../domain/use_cases/amps/GetAllAMPs.kt | 8 +- .../GetRegulatoryAreasByIds.kt | 14 +++ .../api/endpoints/bff/v1/Amps.kt | 20 +++- .../api/endpoints/bff/v1/RegulatoryAreas.kt | 13 +++ .../infrastructure/database/model/AMPModel.kt | 4 +- .../database/model/RegulatoryAreaModel.kt | 4 +- .../database/repositories/JpaAMPRepository.kt | 19 +++- .../JpaRegulatoryAreaRepository.kt | 9 +- .../interfaces/IDBAMPRepository.kt | 46 ++++++++- .../interfaces/IDBRegulatoryAreaRepository.kt | 4 +- .../api/endpoints/bff/v1/AmpsITests.kt | 2 + .../endpoints/bff/v1/RegulatoryAreasITests.kt | 4 + frontend/src/api/ampsAPI.ts | 34 ++++--- .../components/DashboardForm/Amps/Layer.tsx | 11 +-- .../components/DashboardForm/Amps/index.tsx | 24 +++-- .../VigilanceAreas/Panel/Amps.tsx | 3 +- .../VigilanceAreas/Panel/RegulatoryAreas.tsx | 97 +++++++++---------- .../DashboardForm/components/AmpsPanel.tsx | 15 +-- .../DashboardsList/DashboardsTable.tsx | 2 +- .../Layers/ActiveDashboardLayer.tsx | 2 +- .../hooks/useGetFilteredDashboardsQuery.ts | 2 +- frontend/src/features/Dashboard/utils.tsx | 3 +- .../components/LocalizedAreaPanel/index.tsx | 14 ++- .../VigilanceAreaForm/AddAMPs/AMPList.tsx | 3 +- .../EditingVigilanceAreaLayer.tsx | 10 +- .../SelectedVigilanceAreaLayer.tsx | 11 ++- .../metadataPanel/ampMetadata/index.tsx | 15 +-- .../layersSelector/myAmps/MyAMPLayersList.tsx | 4 +- .../layersSelector/search/LayerFilters.tsx | 2 +- .../ResultsList/AMPLayerGroup/AMPLayer.tsx | 11 +-- .../RegulatoryLayerGroup/RegulatoryLayer.tsx | 13 +-- .../search/ResultsList/index.tsx | 2 +- .../map/layers/AMP/AMPPreviewLayer.ts | 11 ++- frontend/src/features/map/layers/AMP/index.ts | 30 +++--- .../Regulatory/RegulatoryPreviewLayer.ts | 13 ++- 39 files changed, 358 insertions(+), 165 deletions(-) create mode 100644 backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/amps/GetAMPById.kt create mode 100644 backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/amps/GetAMPsByIds.kt create mode 100644 backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/regulatoryAreas/GetRegulatoryAreasByIds.kt diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/repositories/IAMPRepository.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/repositories/IAMPRepository.kt index 74eb3fd220..773328ec0e 100644 --- a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/repositories/IAMPRepository.kt +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/repositories/IAMPRepository.kt @@ -5,7 +5,11 @@ import fr.gouv.cacem.monitorenv.domain.entities.amp.AMPEntity import org.locationtech.jts.geom.Geometry interface IAMPRepository { - fun findAll(withGeometry: Boolean): List + fun findAll( + withGeometry: Boolean, + zoom: Int? = null, + bbox: List? = null, + ): List fun count(): Long @@ -15,4 +19,6 @@ interface IAMPRepository { ids: List, axis: AxisEnum, ): List + + fun findById(id: Int): AMPEntity? } diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/repositories/IRegulatoryAreaRepository.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/repositories/IRegulatoryAreaRepository.kt index a0840f6cc2..e050a393e7 100644 --- a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/repositories/IRegulatoryAreaRepository.kt +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/repositories/IRegulatoryAreaRepository.kt @@ -6,6 +6,8 @@ import org.locationtech.jts.geom.Geometry interface IRegulatoryAreaRepository { fun findById(id: Int): RegulatoryAreaEntity? + fun findAllByIds(ids: List): List + fun findAll( withGeometry: Boolean, zoom: Int? = null, diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/amps/GetAMPById.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/amps/GetAMPById.kt new file mode 100644 index 0000000000..616561b1d5 --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/amps/GetAMPById.kt @@ -0,0 +1,22 @@ +package fr.gouv.cacem.monitorenv.domain.use_cases.amps + +import fr.gouv.cacem.monitorenv.config.UseCase +import fr.gouv.cacem.monitorenv.domain.entities.amp.AMPEntity +import fr.gouv.cacem.monitorenv.domain.exceptions.BackendUsageErrorCode +import fr.gouv.cacem.monitorenv.domain.exceptions.BackendUsageException +import fr.gouv.cacem.monitorenv.domain.repositories.IAMPRepository +import org.slf4j.LoggerFactory + +@UseCase +class GetAMPById( + private val ampRepository: IAMPRepository, +) { + private val logger = LoggerFactory.getLogger(GetAMPById::class.java) + + fun execute(id: Int): AMPEntity { + ampRepository.findById(id)?.let { return it } + val errorMessage = "amp $id not found" + logger.error(errorMessage) + throw BackendUsageException(BackendUsageErrorCode.ENTITY_NOT_FOUND, errorMessage) + } +} diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/amps/GetAMPsByIds.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/amps/GetAMPsByIds.kt new file mode 100644 index 0000000000..97ed3b2bdd --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/amps/GetAMPsByIds.kt @@ -0,0 +1,12 @@ +package fr.gouv.cacem.monitorenv.domain.use_cases.amps + +import fr.gouv.cacem.monitorenv.config.UseCase +import fr.gouv.cacem.monitorenv.domain.entities.amp.AMPEntity +import fr.gouv.cacem.monitorenv.domain.repositories.IAMPRepository + +@UseCase +class GetAMPsByIds( + private val ampRepository: IAMPRepository, +) { + fun execute(ids: List): List = ampRepository.findAllByIds(ids) +} diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/amps/GetAllAMPs.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/amps/GetAllAMPs.kt index 1c5b670335..6e669366eb 100644 --- a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/amps/GetAllAMPs.kt +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/amps/GetAllAMPs.kt @@ -11,9 +11,13 @@ class GetAllAMPs( ) { private val logger = LoggerFactory.getLogger(GetAllAMPs::class.java) - fun execute(withGeometry: Boolean): List { + fun execute( + withGeometry: Boolean, + zoom: Int? = null, + bbox: List? = null, + ): List { logger.info("Attempt to GET all AMPs") - val amps = ampRepository.findAll(withGeometry) + val amps = ampRepository.findAll(withGeometry, zoom, bbox) logger.info("Found ${amps.size} AMPs") return amps diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/regulatoryAreas/GetRegulatoryAreasByIds.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/regulatoryAreas/GetRegulatoryAreasByIds.kt new file mode 100644 index 0000000000..00afa12720 --- /dev/null +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/domain/use_cases/regulatoryAreas/GetRegulatoryAreasByIds.kt @@ -0,0 +1,14 @@ +@file:Suppress("ktlint:standard:package-name") + +package fr.gouv.cacem.monitorenv.domain.use_cases.regulatoryAreas + +import fr.gouv.cacem.monitorenv.config.UseCase +import fr.gouv.cacem.monitorenv.domain.entities.regulatoryArea.RegulatoryAreaEntity +import fr.gouv.cacem.monitorenv.domain.repositories.IRegulatoryAreaRepository + +@UseCase +class GetRegulatoryAreasByIds( + private val regulatoryAreaRepository: IRegulatoryAreaRepository, +) { + fun execute(ids: List): List = regulatoryAreaRepository.findAllByIds(ids) +} diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/bff/v1/Amps.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/bff/v1/Amps.kt index 0fbf1c1f89..338dac19de 100644 --- a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/bff/v1/Amps.kt +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/bff/v1/Amps.kt @@ -1,12 +1,16 @@ package fr.gouv.cacem.monitorenv.infrastructure.api.endpoints.bff.v1 +import fr.gouv.cacem.monitorenv.domain.use_cases.amps.GetAMPById +import fr.gouv.cacem.monitorenv.domain.use_cases.amps.GetAMPsByIds import fr.gouv.cacem.monitorenv.domain.use_cases.amps.GetAllAMPs import fr.gouv.cacem.monitorenv.domain.use_cases.amps.GetAllAMPsByIds import fr.gouv.cacem.monitorenv.infrastructure.api.adapters.bff.inputs.amps.AmpByIdsDataInput import fr.gouv.cacem.monitorenv.infrastructure.api.adapters.bff.outputs.AMPDataOutput import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.websocket.server.PathParam import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping @@ -19,16 +23,30 @@ import org.springframework.web.bind.annotation.RestController class Amps( private val getAllAMPs: GetAllAMPs, private val getAllAMPByIds: GetAllAMPsByIds, + private val getAMPById: GetAMPById, ) { @GetMapping("") @Operation(summary = "Get AMPs") fun getAll( @RequestParam(name = "withGeometry") withGeometry: Boolean, + @RequestParam(name = "zoom", required = false) zoom: Int?, + @RequestParam(name = "bbox", required = false) bbox: List?, ): List { - val amps = getAllAMPs.execute(withGeometry) + val amps = getAllAMPs.execute(withGeometry, zoom, bbox) return amps.map { AMPDataOutput.fromAMPEntity(it) } } + @GetMapping("/{ampId}") + @Operation(summary = "Get AMP by Id") + fun get( + @PathParam("Amp id") + @PathVariable(name = "ampId") + ampId: Int, + ): AMPDataOutput? = + getAMPById.execute(id = ampId).let { + AMPDataOutput.fromAMPEntity(it) + } + @PostMapping("") @Operation(summary = "Get AMPs by ids") fun getAll( diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/bff/v1/RegulatoryAreas.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/bff/v1/RegulatoryAreas.kt index 8c97138d5e..74a0628a6f 100644 --- a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/bff/v1/RegulatoryAreas.kt +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/bff/v1/RegulatoryAreas.kt @@ -2,12 +2,15 @@ package fr.gouv.cacem.monitorenv.infrastructure.api.endpoints.bff.v1 import fr.gouv.cacem.monitorenv.domain.use_cases.regulatoryAreas.GetAllRegulatoryAreas import fr.gouv.cacem.monitorenv.domain.use_cases.regulatoryAreas.GetRegulatoryAreaById +import fr.gouv.cacem.monitorenv.domain.use_cases.regulatoryAreas.GetRegulatoryAreasByIds import fr.gouv.cacem.monitorenv.infrastructure.api.adapters.bff.outputs.RegulatoryAreaWithMetadataDataOutput import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag import jakarta.websocket.server.PathParam import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable +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.RequestParam import org.springframework.web.bind.annotation.RestController @@ -18,6 +21,7 @@ import org.springframework.web.bind.annotation.RestController class RegulatoryAreas( private val getAllRegulatoryAreas: GetAllRegulatoryAreas, private val getRegulatoryAreaById: GetRegulatoryAreaById, + private val getRegulatoryAreasByIds: GetRegulatoryAreasByIds, ) { @GetMapping("/{regulatoryAreaId}") @Operation(summary = "Get regulatory area by Id") @@ -40,4 +44,13 @@ class RegulatoryAreas( val regulatoryAreas = getAllRegulatoryAreas.execute(withGeometry, zoom, bbox) return regulatoryAreas.map { RegulatoryAreaWithMetadataDataOutput.fromRegulatoryAreaEntity(it) } } + + @PostMapping("") + @Operation(summary = "Get regulatory Areas by ids") + fun getAll( + @RequestBody ids: List, + ): List = + getRegulatoryAreasByIds + .execute(ids) + .map { RegulatoryAreaWithMetadataDataOutput.fromRegulatoryAreaEntity(it) } } diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/model/AMPModel.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/model/AMPModel.kt index 5212b2eb1c..e0e9fae614 100644 --- a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/model/AMPModel.kt +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/model/AMPModel.kt @@ -28,10 +28,10 @@ data class AMPModel( @Column(name = "updated_at") val updatedAt: String? = null, ) { - fun toAMP(withGeometry: Boolean) = + fun toAMP() = AMPEntity( id = id, - geom = if (withGeometry) geom else null, + geom = geom, name = name, designation = designation, refReg = refReg, diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/model/RegulatoryAreaModel.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/model/RegulatoryAreaModel.kt index b0d962ce84..28362a7056 100644 --- a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/model/RegulatoryAreaModel.kt +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/model/RegulatoryAreaModel.kt @@ -50,7 +50,7 @@ data class RegulatoryAreaModel( @Column(name = "type") val type: String?, @Column(name = "url") val url: String?, ) { - fun toRegulatoryArea(withGeometry: Boolean) = + fun toRegulatoryArea() = RegulatoryAreaEntity( id = id, plan = plan, @@ -60,7 +60,7 @@ data class RegulatoryAreaModel( editeur = editeur, edition = edition, facade = facade, - geom = if (withGeometry) geom else null, + geom = geom, layerName = layerName, polyName = polyName, observation = observation, diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaAMPRepository.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaAMPRepository.kt index 18e06787a6..8649b39641 100644 --- a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaAMPRepository.kt +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaAMPRepository.kt @@ -5,14 +5,27 @@ import fr.gouv.cacem.monitorenv.domain.entities.amp.AMPEntity import fr.gouv.cacem.monitorenv.domain.repositories.IAMPRepository import fr.gouv.cacem.monitorenv.infrastructure.database.repositories.interfaces.IDBAMPRepository import org.locationtech.jts.geom.Geometry +import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Repository @Repository class JpaAMPRepository( private val dbAMPRepository: IDBAMPRepository, ) : IAMPRepository { - override fun findAll(withGeometry: Boolean): List = - dbAMPRepository.findAllByOrderByName().map { it.toAMP(withGeometry) } + override fun findAll( + withGeometry: Boolean, + zoom: Int?, + bbox: List?, + ): List = + dbAMPRepository + .findAllByOrderByName( + zoom, + bbox?.get(0), + bbox?.get(1), + bbox?.get(2), + bbox?.get(3), + withGeometry, + ).map { it.toAMP() } override fun count(): Long = dbAMPRepository.count() @@ -22,4 +35,6 @@ class JpaAMPRepository( ids: List, axis: AxisEnum, ): List = dbAMPRepository.findAllByIds(ids, axis.toString()).map { it.toAMP() } + + override fun findById(id: Int): AMPEntity? = dbAMPRepository.findByIdOrNull(id)?.toAMP() } diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaRegulatoryAreaRepository.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaRegulatoryAreaRepository.kt index 5f9684e90d..c7b22a4229 100644 --- a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaRegulatoryAreaRepository.kt +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/JpaRegulatoryAreaRepository.kt @@ -3,6 +3,7 @@ package fr.gouv.cacem.monitorenv.infrastructure.database.repositories import fr.gouv.cacem.monitorenv.domain.entities.regulatoryArea.RegulatoryAreaEntity import fr.gouv.cacem.monitorenv.domain.repositories.IRegulatoryAreaRepository import fr.gouv.cacem.monitorenv.infrastructure.database.repositories.interfaces.IDBRegulatoryAreaRepository +import jakarta.transaction.Transactional import org.locationtech.jts.geom.Geometry import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Repository @@ -11,6 +12,7 @@ import org.springframework.stereotype.Repository class JpaRegulatoryAreaRepository( private val dbRegulatoryAreaRepository: IDBRegulatoryAreaRepository, ) : IRegulatoryAreaRepository { + @Transactional override fun findAll( withGeometry: Boolean, zoom: Int?, @@ -24,10 +26,13 @@ class JpaRegulatoryAreaRepository( bbox?.get(2), bbox?.get(3), withGeometry, - ).map { it.toRegulatoryArea(withGeometry) } + ).map { it.toRegulatoryArea() } override fun findById(id: Int): RegulatoryAreaEntity? = - dbRegulatoryAreaRepository.findByIdOrNull(id)?.toRegulatoryArea(true) + dbRegulatoryAreaRepository.findByIdOrNull(id)?.toRegulatoryArea() + + override fun findAllByIds(ids: List): List = + dbRegulatoryAreaRepository.findAllById(ids).map { it.toRegulatoryArea() } override fun count(): Long = dbRegulatoryAreaRepository.count() diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/interfaces/IDBAMPRepository.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/interfaces/IDBAMPRepository.kt index 5f4e8d7338..cb7856f1ed 100644 --- a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/interfaces/IDBAMPRepository.kt +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/interfaces/IDBAMPRepository.kt @@ -15,7 +15,51 @@ interface IDBAMPRepository : JpaRepository { ) fun findAllIdsByGeom(geometry: Geometry): List - fun findAllByOrderByName(): List + @Query( + value = """ + SELECT + a.id, + a.des_desigfr, + CASE + WHEN :withGeometry IS FALSE THEN NULL + WHEN :withGeometry IS TRUE AND (:zoom IS NULL OR :zoom >= 14) THEN + a.geom + WHEN :withGeometry IS TRUE AND :zoom < 14 THEN + ST_Multi( + ST_CollectionExtract( + ST_MakeValid( + ST_SimplifyPreserveTopology( + a.geom, + CASE + WHEN :zoom <= 5 THEN 0.01 + WHEN :zoom <= 7 THEN 0.05 + WHEN :zoom <= 11 THEN 0.001 + ELSE 0.0001 + END + ) + ), 3) + ) + END as geom, + a.mpa_oriname, + a.ref_reg, + a.mpa_type, + a.url_legicem + FROM amp_cacem a + WHERE + (:withGeometry IS FALSE OR :minX IS NULL OR :minY IS NULL OR :maxX IS NULL OR :maxY IS NULL) + OR ST_Intersects(a.geom, ST_MakeEnvelope(:minX, :minY, :maxX, :maxY, 4326)) + ORDER BY a.mpa_oriname + """, + nativeQuery = true, + ) + fun findAllByOrderByName( + zoom: Int?, + minX: Double?, + minY: Double?, + maxX: Double?, + maxY: Double?, + withGeometry: Boolean, + ): List @Query( value = diff --git a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/interfaces/IDBRegulatoryAreaRepository.kt b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/interfaces/IDBRegulatoryAreaRepository.kt index a566520aff..d0238461bb 100644 --- a/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/interfaces/IDBRegulatoryAreaRepository.kt +++ b/backend/src/main/kotlin/fr/gouv/cacem/monitorenv/infrastructure/database/repositories/interfaces/IDBRegulatoryAreaRepository.kt @@ -10,12 +10,12 @@ interface IDBRegulatoryAreaRepository : JpaRepository value = """ SELECT r.id, + r.plan, r.date, r.date_fin, r.duree_validite, r.editeur, r.edition, - r.entity_name, r.facade, CASE WHEN :withGeometry IS FALSE THEN NULL @@ -39,9 +39,11 @@ interface IDBRegulatoryAreaRepository : JpaRepository END as geom, r.layer_name, r.observation, + r.poly_name, r.ref_reg, r.source, r.temporalite, + r.resume, r.type, r.url FROM regulations_cacem r diff --git a/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/bff/v1/AmpsITests.kt b/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/bff/v1/AmpsITests.kt index 28f62b5d53..5b12377496 100644 --- a/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/bff/v1/AmpsITests.kt +++ b/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/bff/v1/AmpsITests.kt @@ -5,6 +5,8 @@ import fr.gouv.cacem.monitorenv.config.MapperConfiguration import fr.gouv.cacem.monitorenv.config.SentryConfig import fr.gouv.cacem.monitorenv.domain.entities.AxisEnum import fr.gouv.cacem.monitorenv.domain.entities.amp.AMPEntity +import fr.gouv.cacem.monitorenv.domain.use_cases.amps.GetAMPById +import fr.gouv.cacem.monitorenv.domain.use_cases.amps.GetAMPsByIds import fr.gouv.cacem.monitorenv.domain.use_cases.amps.GetAllAMPs import fr.gouv.cacem.monitorenv.domain.use_cases.amps.GetAllAMPsByIds import fr.gouv.cacem.monitorenv.infrastructure.api.adapters.bff.inputs.amps.AmpByIdsDataInput diff --git a/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/bff/v1/RegulatoryAreasITests.kt b/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/bff/v1/RegulatoryAreasITests.kt index 0cb3d1adc0..d77070ba1f 100644 --- a/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/bff/v1/RegulatoryAreasITests.kt +++ b/backend/src/test/kotlin/fr/gouv/cacem/monitorenv/infrastructure/api/endpoints/bff/v1/RegulatoryAreasITests.kt @@ -5,6 +5,7 @@ import fr.gouv.cacem.monitorenv.config.SentryConfig import fr.gouv.cacem.monitorenv.domain.entities.regulatoryArea.RegulatoryAreaEntity import fr.gouv.cacem.monitorenv.domain.use_cases.regulatoryAreas.GetAllRegulatoryAreas import fr.gouv.cacem.monitorenv.domain.use_cases.regulatoryAreas.GetRegulatoryAreaById +import fr.gouv.cacem.monitorenv.domain.use_cases.regulatoryAreas.GetRegulatoryAreasByIds import fr.gouv.cacem.monitorenv.domain.use_cases.tags.fixtures.TagFixture.Companion.aTag import fr.gouv.cacem.monitorenv.domain.use_cases.themes.fixtures.ThemeFixture.Companion.aTheme import org.hamcrest.Matchers.equalTo @@ -39,6 +40,9 @@ class RegulatoryAreasITests { @MockitoBean private lateinit var getRegulatoryAreaById: GetRegulatoryAreaById + @MockitoBean + private lateinit var getRegulatoryAreasByIds: GetRegulatoryAreasByIds + private val wktReader = WKTReader() private val multipolygonString = diff --git a/frontend/src/api/ampsAPI.ts b/frontend/src/api/ampsAPI.ts index 4639d5a44b..cbb47ebe4c 100644 --- a/frontend/src/api/ampsAPI.ts +++ b/frontend/src/api/ampsAPI.ts @@ -1,7 +1,7 @@ import { getExtentOfLayersGroup } from '@features/layersSelector/utils/getExtentOfLayersGroup' import { FrontendApiError } from '@libs/FrontendApiError' import { createEntityAdapter, createSelector, type EntityId, type EntityState } from '@reduxjs/toolkit' -import { boundingExtent, createEmpty } from 'ol/extent' +import { boundingExtent, createEmpty, type Extent } from 'ol/extent' import { createCachedSelector } from 're-reselect' import { monitorenvPrivateApi } from './api' @@ -16,7 +16,9 @@ const initialState = AMPAdapter.getInitialState() const GET_AMP_ERROR_MESSAGE = "Nous n'avons pas pu récupérer les Zones AMP" type AmpQueryOption = { + bbox?: Extent | undefined withGeometry?: boolean + zoom?: number | undefined } export const ampsAPI = monitorenvPrivateApi.injectEndpoints({ @@ -33,8 +35,11 @@ export const ampsAPI = monitorenvPrivateApi.injectEndpoints({ } } }), - getAMPs: builder.query, AmpQueryOption | void>({ - query: ({ withGeometry } = { withGeometry: false }) => `/v1/amps?withGeometry=${withGeometry}`, + getAMPs: builder.query, AmpQueryOption>({ + query: ({ bbox, withGeometry, zoom } = { bbox: undefined, withGeometry: false, zoom: undefined }) => + `/v1/amps?withGeometry=${withGeometry}${withGeometry && bbox ? `&bbox=${bbox}` : ''}${ + withGeometry && zoom ? `&zoom=${zoom}` : '' + }`, transformErrorResponse: response => new FrontendApiError(GET_AMP_ERROR_MESSAGE, response), transformResponse: (response: AMPFromAPI[]) => AMPAdapter.setAll( @@ -58,18 +63,21 @@ export const ampsAPI = monitorenvPrivateApi.injectEndpoints({ export const { useGetAMPQuery, useGetAMPsByIdsQuery, useGetAMPsQuery } = ampsAPI -export const getAMPsIdsGroupedByName = createSelector([ampsAPI.endpoints.getAMPs.select()], ampsQuery => { - const ampIdsByName = ampsQuery.data?.ids.reduce((acc, id) => { - const amp = ampsQuery.data?.entities[id] - if (amp) { - acc[amp.name] = [...(acc[amp.name] ?? []), id] - } +export const getAMPsIdsGroupedByName = createSelector( + [ampsAPI.endpoints.getAMPs.select({ withGeometry: false })], + ampsQuery => { + const ampIdsByName = ampsQuery.data?.ids.reduce((acc, id) => { + const amp = ampsQuery.data?.entities[id] + if (amp) { + acc[amp.name] = [...(acc[amp.name] ?? []), id] + } - return acc - }, {} as Record) + return acc + }, {} as Record) - return ampIdsByName -}) + return ampIdsByName + } +) export const getAMPsIdsByGroupName = createCachedSelector( [getAMPsIdsGroupedByName, (_, groupName: string) => groupName], diff --git a/frontend/src/features/Dashboard/components/DashboardForm/Amps/Layer.tsx b/frontend/src/features/Dashboard/components/DashboardForm/Amps/Layer.tsx index 94d93d51c4..37eacdbcc8 100644 --- a/frontend/src/features/Dashboard/components/DashboardForm/Amps/Layer.tsx +++ b/frontend/src/features/Dashboard/components/DashboardForm/Amps/Layer.tsx @@ -1,4 +1,4 @@ -import { useGetAMPsQuery } from '@api/ampsAPI' +import { useGetAMPQuery } from '@api/ampsAPI' import { StyledTransparentButton } from '@components/style' import { dashboardActions, getOpenedPanel } from '@features/Dashboard/slice' import { Dashboard } from '@features/Dashboard/types' @@ -25,14 +25,7 @@ export function Layer({ isPinned = false, isSelected, layerId }: AmpLayerProps) const openPanel = useAppSelector(state => getOpenedPanel(state.dashboard, Dashboard.Block.AMP)) const ref = createRef() - const { layer } = useGetAMPsQuery( - { withGeometry: true }, - { - selectFromResult: result => ({ - layer: result?.currentData?.entities[layerId] - }) - } - ) + const { data: layer } = useGetAMPQuery(layerId) const handleSelectZone = e => { e.stopPropagation() diff --git a/frontend/src/features/Dashboard/components/DashboardForm/Amps/index.tsx b/frontend/src/features/Dashboard/components/DashboardForm/Amps/index.tsx index bba25a7334..4fbad32410 100644 --- a/frontend/src/features/Dashboard/components/DashboardForm/Amps/index.tsx +++ b/frontend/src/features/Dashboard/components/DashboardForm/Amps/index.tsx @@ -38,16 +38,20 @@ export const Amps = forwardRef( r => r.name ) - const { selectedAmpByLayerName } = useGetAMPsQuery(undefined, { - selectFromResult: ({ data }) => ({ - selectedAmpByLayerName: groupBy( - Object.values(data?.entities ?? []) - .filter(amp => selectedAmpIds.includes(amp.id)) - .sort((a, b) => a.name.localeCompare(b.name)), - amp => amp.name - ) - }) - }) + // TODO: either send extend of layer OR findById + const { selectedAmpByLayerName } = useGetAMPsQuery( + { withGeometry: false }, + { + selectFromResult: ({ data }) => ({ + selectedAmpByLayerName: groupBy( + Object.values(data?.entities ?? []) + .filter(amp => selectedAmpIds.includes(amp.id)) + .sort((a, b) => a.name.localeCompare(b.name)), + amp => amp.name + ) + }) + } + ) useEffect(() => { if (isSelectedAccordionOpen) { diff --git a/frontend/src/features/Dashboard/components/DashboardForm/VigilanceAreas/Panel/Amps.tsx b/frontend/src/features/Dashboard/components/DashboardForm/VigilanceAreas/Panel/Amps.tsx index 75ea2c12f4..67d9dd566f 100644 --- a/frontend/src/features/Dashboard/components/DashboardForm/VigilanceAreas/Panel/Amps.tsx +++ b/frontend/src/features/Dashboard/components/DashboardForm/VigilanceAreas/Panel/Amps.tsx @@ -19,7 +19,8 @@ import type { AMP } from 'domain/entities/AMPs' export function Amps({ ampIds }: { ampIds: number[] }) { const dispatch = useAppDispatch() - const { data: ampLayers } = useGetAMPsQuery({ withGeometry: true }) + // TODO: either send extend of layer OR findById + const { data: ampLayers } = useGetAMPsQuery({ withGeometry: false }) const amps = ampIds.map(amp => ampLayers?.entities[amp]) diff --git a/frontend/src/features/Dashboard/components/DashboardForm/VigilanceAreas/Panel/RegulatoryAreas.tsx b/frontend/src/features/Dashboard/components/DashboardForm/VigilanceAreas/Panel/RegulatoryAreas.tsx index ad139e981d..69372e77fa 100644 --- a/frontend/src/features/Dashboard/components/DashboardForm/VigilanceAreas/Panel/RegulatoryAreas.tsx +++ b/frontend/src/features/Dashboard/components/DashboardForm/VigilanceAreas/Panel/RegulatoryAreas.tsx @@ -92,58 +92,57 @@ export function RegulatoryAreas({ regulatoryAreaIds }: { regulatoryAreaIds: numb )} Réglementations en lien - {regulatoryAreas && - regulatoryAreas.map(regulatoryArea => { - const layerTitle = getRegulatoryAreaTitle(regulatoryArea?.polyName, regulatoryArea?.resume) + {regulatoryAreas?.map(regulatoryArea => { + const layerTitle = getRegulatoryAreaTitle(regulatoryArea?.polyName, regulatoryArea?.resume) - return ( - - - - {layerTitle} - + return ( + + + + {layerTitle} + - - toggleMetadata(e, regulatoryArea?.id)} - title={ - isSubPanelOpened && openPanel.subPanel?.id === regulatoryArea?.id - ? 'Fermer la réglementation de la zone' - : 'Afficher la réglementation de la zone' - } - /> + + toggleMetadata(e, regulatoryArea?.id)} + title={ + isSubPanelOpened && openPanel.subPanel?.id === regulatoryArea?.id + ? 'Fermer la réglementation de la zone' + : 'Afficher la réglementation de la zone' + } + /> - showRegulatoryAreaLayer(e, regulatoryArea)} - title={ - regulatoryArea?.id && regulatoryIdsToDisplay?.includes(regulatoryArea?.id) - ? 'Cacher la zone' - : 'Afficher la zone' - } - /> - - - ) - })} + showRegulatoryAreaLayer(e, regulatoryArea)} + title={ + regulatoryArea?.id && regulatoryIdsToDisplay?.includes(regulatoryArea?.id) + ? 'Cacher la zone' + : 'Afficher la zone' + } + /> + + + ) + })} ) diff --git a/frontend/src/features/Dashboard/components/DashboardForm/components/AmpsPanel.tsx b/frontend/src/features/Dashboard/components/DashboardForm/components/AmpsPanel.tsx index 8577e1f81d..1968f80e5b 100644 --- a/frontend/src/features/Dashboard/components/DashboardForm/components/AmpsPanel.tsx +++ b/frontend/src/features/Dashboard/components/DashboardForm/components/AmpsPanel.tsx @@ -14,16 +14,19 @@ import { LayerLegend } from '@features/layersSelector/utils/LayerLegend.style' import { Accent, Icon, IconButton, THEME } from '@mtes-mct/monitor-ui' import { MonitorEnvLayers } from 'domain/entities/layers/constants' import { getTitle } from 'domain/entities/layers/utils' -import { forwardRef, type ComponentProps } from 'react' +import { type ComponentProps, forwardRef } from 'react' import styled from 'styled-components' export const AmpsPanel = forwardRef void } & ComponentProps<'div'>>( ({ layerId, onClose, ...props }, ref) => { - const { layer: ampMetadata } = useGetAMPsQuery(undefined, { - selectFromResult: result => ({ - layer: result?.currentData?.entities[layerId] - }) - }) + const { layer: ampMetadata } = useGetAMPsQuery( + { withGeometry: false }, + { + selectFromResult: result => ({ + layer: result?.currentData?.entities[layerId] + }) + } + ) return ( // eslint-disable-next-line react/jsx-props-no-spreading diff --git a/frontend/src/features/Dashboard/components/DashboardsList/DashboardsTable.tsx b/frontend/src/features/Dashboard/components/DashboardsList/DashboardsTable.tsx index 6154b86fe9..6b32addb6e 100644 --- a/frontend/src/features/Dashboard/components/DashboardsList/DashboardsTable.tsx +++ b/frontend/src/features/Dashboard/components/DashboardsList/DashboardsTable.tsx @@ -26,7 +26,7 @@ export function DashboardsTable({ dashboards, isFetching, isLoading }: Dashboard const { pathname } = useLocation() const legacyFirefoxOffset = pathname !== paths.sidewindow && isLegacyFirefox() ? -25 : 0 - const { data: regulatoryAreas } = useGetRegulatoryAreasQuery() + const { data: regulatoryAreas } = useGetRegulatoryAreasQuery({ withGeometry: false }) const { data: controlUnits } = useGetControlUnitsQuery() const flattenRegulatoryAreas = useMemo( diff --git a/frontend/src/features/Dashboard/components/Layers/ActiveDashboardLayer.tsx b/frontend/src/features/Dashboard/components/Layers/ActiveDashboardLayer.tsx index b02c0465ae..853d26a1c5 100644 --- a/frontend/src/features/Dashboard/components/Layers/ActiveDashboardLayer.tsx +++ b/frontend/src/features/Dashboard/components/Layers/ActiveDashboardLayer.tsx @@ -13,7 +13,7 @@ import { Layers } from 'domain/entities/layers/constants' import { Feature } from 'ol' import VectorLayer from 'ol/layer/Vector' import VectorSource from 'ol/source/Vector' -import { useCallback, useEffect, useMemo, useRef } from 'react' +import React, { useCallback, useEffect, useMemo, useRef } from 'react' import { dashboardIcon, getDashboardStyle } from './style' import { Dashboard } from '../../types' diff --git a/frontend/src/features/Dashboard/hooks/useGetFilteredDashboardsQuery.ts b/frontend/src/features/Dashboard/hooks/useGetFilteredDashboardsQuery.ts index 5e59038c9f..5e322f55c6 100644 --- a/frontend/src/features/Dashboard/hooks/useGetFilteredDashboardsQuery.ts +++ b/frontend/src/features/Dashboard/hooks/useGetFilteredDashboardsQuery.ts @@ -13,7 +13,7 @@ export const useGetFilteredDashboardsQuery = (skip = false) => { const { controlUnits, regulatoryTags, seaFronts, specificPeriod, updatedAt } = useAppSelector( state => state.dashboardFilters.filters ) - const { data: regulatoryAreas } = useGetRegulatoryAreasQuery() + const { data: regulatoryAreas } = useGetRegulatoryLayersQuery({ withGeometry: false }) const { data: dashboards, diff --git a/frontend/src/features/Dashboard/utils.tsx b/frontend/src/features/Dashboard/utils.tsx index aa6cb46b21..861b94d2fd 100644 --- a/frontend/src/features/Dashboard/utils.tsx +++ b/frontend/src/features/Dashboard/utils.tsx @@ -33,8 +33,7 @@ export async function populateExtractAreaFromApi( ids: extractedAreaFromApi.regulatoryAreaIds }) ) - // TODO: use getAmpByIds - const { data: ampLayers } = await dispatch(ampsAPI.endpoints.getAMPs.initiate()) + const { data: ampLayers } = await dispatch(ampsAPI.endpoints.getAMPs.initiate({ withGeometry: false })) const { data: vigilanceAreas } = await dispatch(vigilanceAreasAPI.endpoints.getVigilanceAreas.initiate()) const { data: reportings } = await dispatch( reportingsAPI.endpoints.getReportingsByIds.initiate(extractedAreaFromApi.reportingIds) diff --git a/frontend/src/features/LocalizedArea/components/LocalizedAreaPanel/index.tsx b/frontend/src/features/LocalizedArea/components/LocalizedAreaPanel/index.tsx index 4058eec7a3..d52323baa8 100644 --- a/frontend/src/features/LocalizedArea/components/LocalizedAreaPanel/index.tsx +++ b/frontend/src/features/LocalizedArea/components/LocalizedAreaPanel/index.tsx @@ -18,11 +18,14 @@ export function LocalizedAreaPanel({ localizedArea }: { localizedArea?: Localize const dispatch = useAppDispatch() const { metadataPanelIsOpen } = useAppSelector(state => state.layersMetadata) - const { amps } = useGetAMPsQuery(undefined, { - selectFromResult: ({ data }) => ({ - amps: Object.values(data?.entities ?? []).filter(amp => localizedArea?.ampIds?.includes(amp.id)) - }) - }) + const { amps } = useGetAMPsQuery( + { withGeometry: false }, + { + selectFromResult: ({ data }) => ({ + amps: Object.values(data?.entities ?? []).filter(amp => localizedArea?.ampIds?.includes(amp.id)) + }) + } + ) const { controlUnits } = useGetControlUnitsQuery(undefined, { selectFromResult: ({ data }) => ({ controlUnits: data?.filter(controlUnit => localizedArea?.controlUnitIds?.includes(controlUnit.id)) @@ -56,6 +59,7 @@ export function LocalizedAreaPanel({ localizedArea }: { localizedArea?: Localize ) } + const Wrapper = styled.div<{ $regulatoryMetadataPanelIsOpen: boolean }>` border-radius: 2px; width: 400px; diff --git a/frontend/src/features/VigilanceArea/components/VigilanceAreaForm/AddAMPs/AMPList.tsx b/frontend/src/features/VigilanceArea/components/VigilanceAreaForm/AddAMPs/AMPList.tsx index 11a6df32d1..8db7d0e228 100644 --- a/frontend/src/features/VigilanceArea/components/VigilanceAreaForm/AddAMPs/AMPList.tsx +++ b/frontend/src/features/VigilanceArea/components/VigilanceAreaForm/AddAMPs/AMPList.tsx @@ -6,8 +6,9 @@ type AMPListProps = { isReadOnly?: boolean linkedAMPs: number[] } + export function AMPList({ isReadOnly = false, linkedAMPs }: AMPListProps) { - const { data: AMPLayers } = useGetAMPsQuery() + const { data: AMPLayers } = useGetAMPsQuery({ withGeometry: false }) const linkAMPLayers = linkedAMPs .map(ampId => AMPLayers?.entities[ampId]) .filter(amp => !!amp) diff --git a/frontend/src/features/VigilanceArea/components/VigilanceAreaLayer/EditingVigilanceAreaLayer.tsx b/frontend/src/features/VigilanceArea/components/VigilanceAreaLayer/EditingVigilanceAreaLayer.tsx index abe083209a..e4d4fb71d5 100644 --- a/frontend/src/features/VigilanceArea/components/VigilanceAreaLayer/EditingVigilanceAreaLayer.tsx +++ b/frontend/src/features/VigilanceArea/components/VigilanceAreaLayer/EditingVigilanceAreaLayer.tsx @@ -28,6 +28,7 @@ export function EditingVigilanceAreaLayer({ map }: BaseMapChildrenProps) { const vigilanceAreaGeom = useAppSelector(state => state.vigilanceArea.geometry) const isolatedLayer = useAppSelector(state => state.map.isolatedLayer) + const { bbox, zoom } = useAppSelector(state => state.map.mapView) const isLayerVisible = !!editingVigilanceAreaId @@ -103,7 +104,14 @@ export function EditingVigilanceAreaLayer({ map }: BaseMapChildrenProps) { regulatoryAreasVectorLayerRef.current.name = Layers.REGULATORY_AREAS_LINKED_TO_VIGILANCE_AREA.code // AMP Layer - const { data: ampLayers } = useGetAMPsQuery() + const { data: ampLayers } = useGetAMPsQuery( + { + bbox, + withGeometry: true, + zoom + }, + { skip: !isLayerVisible || !(bbox || zoom) } + ) const ampFeatures = useMemo(() => { if (!ampLayers || ampToAdd.length === 0) { return [] diff --git a/frontend/src/features/VigilanceArea/components/VigilanceAreaLayer/SelectedVigilanceAreaLayer.tsx b/frontend/src/features/VigilanceArea/components/VigilanceAreaLayer/SelectedVigilanceAreaLayer.tsx index bf3fe75222..a3f2cd6c0e 100644 --- a/frontend/src/features/VigilanceArea/components/VigilanceAreaLayer/SelectedVigilanceAreaLayer.tsx +++ b/frontend/src/features/VigilanceArea/components/VigilanceAreaLayer/SelectedVigilanceAreaLayer.tsx @@ -23,6 +23,7 @@ import type { Geometry } from 'ol/geom' export function SelectedVigilanceAreaLayer({ map }: BaseMapChildrenProps) { const selectedVigilanceAreaId = useAppSelector(state => state.vigilanceArea.selectedVigilanceAreaId) const editingVigilanceAreaId = useAppSelector(state => state.vigilanceArea.editingVigilanceAreaId) + const { bbox, zoom } = useAppSelector(state => state.map.mapView) const { selectedVigilanceArea } = useGetVigilanceAreasQuery(undefined, { selectFromResult: ({ data }) => ({ @@ -113,7 +114,15 @@ export function SelectedVigilanceAreaLayer({ map }: BaseMapChildrenProps) { const isAMPLayerVisible = !!(ampIdsToBeDisplayed && ampIdsToBeDisplayed?.length > 0) && !!selectedVigilanceAreaId && isLayerVisible - const { data: ampLayers } = useGetAMPsQuery() + + const { data: ampLayers } = useGetAMPsQuery( + { + bbox, + withGeometry: true, + zoom + }, + { skip: !isAMPLayerVisible || !(bbox || zoom) } + ) const ampFeatures = useMemo(() => { const linkedAMPs = selectedVigilanceArea?.linkedAMPs ?? [] if (!ampLayers || linkedAMPs.length === 0) { diff --git a/frontend/src/features/layersSelector/metadataPanel/ampMetadata/index.tsx b/frontend/src/features/layersSelector/metadataPanel/ampMetadata/index.tsx index 53bad690aa..1a87db19de 100644 --- a/frontend/src/features/layersSelector/metadataPanel/ampMetadata/index.tsx +++ b/frontend/src/features/layersSelector/metadataPanel/ampMetadata/index.tsx @@ -18,12 +18,15 @@ export function AmpMetadata() { const dispatch = useAppDispatch() const { metadataLayerId, metadataPanelIsOpen } = useAppSelector(state => state.layersMetadata) - const { ampMetadata } = useGetAMPsQuery(undefined, { - pollingInterval: FOUR_HOURS, - selectFromResult: result => ({ - ampMetadata: metadataLayerId && result?.data?.entities[metadataLayerId] - }) - }) + const { ampMetadata } = useGetAMPsQuery( + { withGeometry: false }, + { + pollingInterval: FOUR_HOURS, + selectFromResult: result => ({ + ampMetadata: metadataLayerId && result?.data?.entities[metadataLayerId] + }) + } + ) const onCloseIconClicked = useCallback(() => { dispatch(closeMetadataPanel()) diff --git a/frontend/src/features/layersSelector/myAmps/MyAMPLayersList.tsx b/frontend/src/features/layersSelector/myAmps/MyAMPLayersList.tsx index e427663def..ed4455933e 100644 --- a/frontend/src/features/layersSelector/myAmps/MyAMPLayersList.tsx +++ b/frontend/src/features/layersSelector/myAmps/MyAMPLayersList.tsx @@ -15,8 +15,8 @@ export function AMPLayersList() { const myAmpsIsOpen = useAppSelector(state => state.layerSidebar.myAmpsIsOpen) const [totalNumberOfZones, setTotalNumberOfZones] = useState(0) - - const { currentData: amps, isLoading } = useGetAMPsQuery() + // TODO: either send extend of layer OR findById + const { currentData: amps, isLoading } = useGetAMPsQuery({ withGeometry: false }) const selectedAmps = useMemo( () => selectedAmpLayerIds.map(id => amps?.entities?.[id]).filter((layer): layer is AMP => !!layer), [amps, selectedAmpLayerIds] diff --git a/frontend/src/features/layersSelector/search/LayerFilters.tsx b/frontend/src/features/layersSelector/search/LayerFilters.tsx index b1e29b13c4..d211a5faac 100644 --- a/frontend/src/features/layersSelector/search/LayerFilters.tsx +++ b/frontend/src/features/layersSelector/search/LayerFilters.tsx @@ -74,7 +74,7 @@ export function LayerFilters() { const areRecentsAreasChecked = useAppSelector(state => state.layerSearch.areRecentsAreasChecked) const filteredVigilanceAreaPeriod = useAppSelector(state => state.vigilanceAreaFilters.period) - const { data: amps } = useGetAMPsQuery() + const { data: amps } = useGetAMPsQuery({ withGeometry: false }) const ampTypes = useMemo(() => getAmpsAsOptions(amps ?? []), [amps]) const AMPCustomSearch = useMemo(() => new CustomSearch(ampTypes as Array