Skip to content

Commit

Permalink
Add bases on map
Browse files Browse the repository at this point in the history
  • Loading branch information
ivangabriele committed Oct 27, 2023
1 parent ccf2b92 commit cbf8f85
Show file tree
Hide file tree
Showing 18 changed files with 497 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class ApiMissionsController(
private val getMissionById: GetMissionById,
private val deleteMission: DeleteMission,
private val getEngagedControlUnits: GetEngagedControlUnits,
private val getMissionsByIds: GetMissionsByIds
private val getMissionsByIds: GetMissionsByIds,
) {

@GetMapping("")
Expand Down
13 changes: 13 additions & 0 deletions frontend/public/control-unit.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 36 additions & 0 deletions frontend/src/components/OverlayCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Icon, MapMenuDialog } from '@mtes-mct/monitor-ui'
import styled from 'styled-components'

import type { HtmlHTMLAttributes } from 'react'
import type { Promisable } from 'type-fest'

export type DialogProps = HtmlHTMLAttributes<HTMLDivElement> & {
isCloseButtonHidden?: boolean
onClose: () => Promisable<void>
title: string
}
export function OverlayCard({ children, isCloseButtonHidden = false, onClose, title }: DialogProps) {
return (
<StyledMapMenuDialogContainer>
<MapMenuDialog.Header>
<StyledMapMenuDialogTitle>{title}</StyledMapMenuDialogTitle>
<MapMenuDialog.CloseButton
Icon={Icon.Close}
onClick={onClose}
style={{ visibility: isCloseButtonHidden ? 'hidden' : 'visible' }}
/>
</MapMenuDialog.Header>
{children}
</StyledMapMenuDialogContainer>
)
}

const StyledMapMenuDialogContainer = styled(MapMenuDialog.Container)`
margin: 0;
`

const StyledMapMenuDialogTitle = styled(MapMenuDialog.Title)`
flex-grow: 1;
margin-left: 32px;
text-align: center;
`
6 changes: 6 additions & 0 deletions frontend/src/domain/entities/layers/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ export const Layers = {
subZoneFieldKey: null,
type: LayerType.BASE_LAYER
},
BASES: {
code: 'bases',
zIndex: 1200
},
DRAW: {
code: 'draw_layer',
zIndex: 1500
Expand Down Expand Up @@ -197,12 +201,14 @@ export const SelectableLayers = [
Layers.REGULATORY_ENV_PREVIEW.code,
Layers.REGULATORY_ENV.code,
Layers.AMP.code,
Layers.BASES.code,
Layers.SEMAPHORES.code,
Layers.REPORTINGS.code
]
export const HoverableLayers = [
Layers.MISSIONS.code,
Layers.ACTIONS.code,
Layers.BASES.code,
Layers.SEMAPHORES.code,
Layers.REPORTINGS.code
]
9 changes: 8 additions & 1 deletion frontend/src/domain/shared_slices/Global.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// TODO This slice should disappear in favor of `features/MainWindow/slice.ts`.
// TODO This slice should disappear in favor of `features/MainWindow/slice.ts` and "Map" feature should have its own slice.
// TODO "Map" feature should have its own slice where we would transfer the related `display...` props.

import { createSlice, type PayloadAction } from '@reduxjs/toolkit'

Expand Down Expand Up @@ -63,6 +64,9 @@ type GlobalStateType = {
displayReportingEditingLayer: boolean
displayReportingSelectedLayer: boolean

displayBaseLayer: boolean
displayBaseOverlay: boolean

isLayersSidebarVisible: boolean

isMapToolVisible: MapToolType | undefined
Expand Down Expand Up @@ -113,6 +117,9 @@ const initialState: GlobalStateType = {
displayReportingEditingLayer: true,
displayReportingSelectedLayer: true,

displayBaseLayer: true,
displayBaseOverlay: true,

isMapToolVisible: undefined,

healthcheckTextWarning: undefined,
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/domain/shared_slices/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { semaphoresAPI } from '../../api/semaphoresAPI'
import { administrationTablePersistedReducer } from '../../features/Administration/components/AdministrationTable/slice'
import { backOfficeReducer } from '../../features/BackOffice/slice'
import { baseTablePersistedReducer } from '../../features/Base/components/BaseTable/slice'
import { baseReducer } from '../../features/Base/slice'
import { controlUnitDialogReducer } from '../../features/ControlUnit/components/ControlUnitDialog/slice'
import { controlUnitListDialogPersistedReducer } from '../../features/ControlUnit/components/ControlUnitListDialog/slice'
import { controlUnitTablePersistedReducer } from '../../features/ControlUnit/components/ControlUnitTable/slice'
Expand All @@ -48,6 +49,7 @@ export const homeReducers = combineReducers({
backOfficeAdministrationList: administrationTablePersistedReducer,
backOfficeBaseList: baseTablePersistedReducer,
backOfficeControlUnitList: controlUnitTablePersistedReducer,
base: baseReducer,
draw: drawReducer,
global: globalReducer,
interestPoint: interestPointSlicePersistedReducer,
Expand Down
99 changes: 99 additions & 0 deletions frontend/src/features/Base/components/BaseLayer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { THEME } from '@mtes-mct/monitor-ui'
import VectorLayer from 'ol/layer/Vector'
import VectorSource from 'ol/source/Vector'
import { Icon, Style } from 'ol/style'
import { useEffect, useMemo, useRef } from 'react'

import { getBasePointFeature } from './utils'
import { useGetBasesQuery } from '../../../../api/basesAPI'
import { Layers } from '../../../../domain/entities/layers/constants'
import { setOverlayCoordinates } from '../../../../domain/shared_slices/Global'
import { useAppDispatch } from '../../../../hooks/useAppDispatch'
import { useAppSelector } from '../../../../hooks/useAppSelector'
import { baseActions } from '../../slice'

import type { BaseMapChildrenProps } from '../../../map/BaseMap'

export function BaseLayer({ map, mapClickEvent }: BaseMapChildrenProps) {
const vectorSourceRef = useRef(new VectorSource())
const vectorLayerRef = useRef(
new VectorLayer({
renderBuffer: 7,
source: vectorSourceRef.current,
style: FeatureStyle,
updateWhileAnimating: true,
updateWhileInteracting: true,
zIndex: Layers.BASES.zIndex
})
)
;(vectorLayerRef.current as any).name = Layers.BASES.code

const dispatch = useAppDispatch()
const global = useAppSelector(state => state.global)
const base = useAppSelector(state => state.base)
const listener = useAppSelector(state => state.draw.listener)

const { data: bases } = useGetBasesQuery()

const basesAsFeatures = useMemo(() => (bases || []).map(getBasePointFeature), [bases])

useEffect(() => {
if (!vectorSourceRef.current) {
return
}

vectorSourceRef.current.forEachFeature(feature => {
feature.setProperties({
isSelected: feature.getId() === base.selectedBaseFeatureId,
overlayCoordinates: feature.getId() === base.selectedBaseFeatureId ? global.overlayCoordinates : undefined
})
})
}, [base.selectedBaseFeatureId, global.overlayCoordinates])

useEffect(() => {
if (map) {
map.getLayers().push(vectorLayerRef.current)

const scopedVectorLayer = vectorLayerRef.current

return () => map.removeLayer(scopedVectorLayer)
}

return () => {}
}, [map])

useEffect(() => {
vectorLayerRef.current?.setVisible(global.displayBaseLayer && !listener)
}, [global.displayBaseLayer, listener])

useEffect(() => {
const feature = mapClickEvent?.feature
if (!feature) {
return
}

const featureId = feature.getId()?.toString()
if (!featureId?.includes(Layers.BASES.code)) {
return
}

if (feature.getId()?.toString()?.startsWith(Layers.BASES.code)) {
dispatch(baseActions.selectBaseFeatureId(featureId))
dispatch(setOverlayCoordinates(undefined))
}
}, [dispatch, mapClickEvent])

useEffect(() => {
vectorSourceRef.current.clear(true)
vectorSourceRef.current.addFeatures(basesAsFeatures)
}, [basesAsFeatures])

return null
}

export const FeatureStyle = new Style({
image: new Icon({
color: THEME.color.charcoal,
src: 'control-unit.svg'
})
})
33 changes: 33 additions & 0 deletions frontend/src/features/Base/components/BaseLayer/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Feature } from 'ol'
import { GeoJSON } from 'ol/format'

import { Layers } from '../../../../domain/entities/layers/constants'
import { OPENLAYERS_PROJECTION, WSG84_PROJECTION } from '../../../../domain/entities/map/constants'

import type { Base } from '../../../../domain/entities/base'

export const getBasePointFeature = (base: Base.Base) => {
const geoJSON = new GeoJSON()
const geometry = geoJSON.readGeometry(
{
coordinates: [base.longitude, base.latitude],
type: 'Point'
},
{
dataProjection: WSG84_PROJECTION,
featureProjection: OPENLAYERS_PROJECTION
}
)

const feature = new Feature({
geometry
})
feature.setId(`${Layers.BASES.code}:${base.id}`)
feature.setProperties({
base,
isHighlighted: false,
isSelected: false
})

return feature
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import styled from 'styled-components'

import { displayControlUnitResourcesFromControlUnit } from './utils'

import type { ControlUnit } from '../../../../../domain/entities/controlUnit'

type ItemProps = {
controlUnit: ControlUnit.ControlUnit
}
export function Item({ controlUnit }: ItemProps) {
return (
<Wrapper>
<NameText>{controlUnit.name}</NameText>
<AdministrationText>{controlUnit.administration.name}</AdministrationText>
<ResourcesBar>{displayControlUnitResourcesFromControlUnit(controlUnit)}</ResourcesBar>
</Wrapper>
)
}

const Wrapper = styled.div`
background-color: ${p => p.theme.color.gainsboro};
cursor: pointer;
padding: 8px 12px;
&:hover {
background-color: ${p => p.theme.color.lightGray};
}
`

const NameText = styled.div`
color: ${p => p.theme.color.gunMetal};
font-weight: bold;
line-height: 18px;
`

const AdministrationText = styled.div`
color: ${p => p.theme.color.gunMetal};
line-height: 18px;
margin: 2px 0 8px;
`

const ResourcesBar = styled.div`
line-height: 18px;
`
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { MapMenuDialog } from '@mtes-mct/monitor-ui'
import { uniq } from 'lodash/fp'
import { useCallback, useEffect, useRef } from 'react'
import styled from 'styled-components'

import { Item } from './Item'
import { controlUnitsAPI } from '../../../../../api/controlUnitsAPI'
import { OverlayCard } from '../../../../../components/OverlayCard'
import { useAppDispatch } from '../../../../../hooks/useAppDispatch'
import { useAppSelector } from '../../../../../hooks/useAppSelector'
import { FrontendError } from '../../../../../libs/FrontendError'
import { baseActions } from '../../../slice'

import type { Base } from '../../../../../domain/entities/base'
import type { ControlUnit } from '../../../../../domain/entities/controlUnit'
import type { Feature } from 'ol'

export function BaseCard({ feature, selected = false }: { feature: Feature; selected?: boolean }) {
const controlUnitsRef = useRef<ControlUnit.ControlUnit[]>([])

const dispatch = useAppDispatch()
const global = useAppSelector(state => state.global)

const featureProperties = feature.getProperties() as {
base: Base.Base
}

const close = useCallback(() => {
dispatch(baseActions.selectBaseFeatureId(undefined))
}, [dispatch])

const updateControlUnits = useCallback(async () => {
if (!featureProperties.base || !featureProperties.base.controlUnitResources) {
controlUnitsRef.current = []

return
}

const controlUnitIds = uniq(
featureProperties.base.controlUnitResources.map(controlUnitResource => controlUnitResource.controlUnitId)
)

controlUnitsRef.current = await Promise.all(
controlUnitIds.map(async controlUnitResourceId => {
const { data: controlUnit } = await dispatch(
controlUnitsAPI.endpoints.getControlUnit.initiate(controlUnitResourceId)
)
if (!controlUnit) {
throw new FrontendError('`controlUnit` is undefined.')
}

return controlUnit
})
)
}, [dispatch, featureProperties.base])

useEffect(() => {
updateControlUnits()
}, [updateControlUnits])

if (!global.displayBaseLayer || !featureProperties.base) {
return null
}

return (
<OverlayCard
data-cy="base-overlay"
isCloseButtonHidden={!selected}
onClose={close}
title={featureProperties.base.name}
>
<StyledMapMenuDialogBody>
{controlUnitsRef.current.map(controlUnit => (
<Item controlUnit={controlUnit} />
))}
</StyledMapMenuDialogBody>
</OverlayCard>
)
}

const StyledMapMenuDialogBody = styled(MapMenuDialog.Body)`
height: 480px;
> div:not(:first-child) {
margin-top: 8px;
}
`
Loading

0 comments on commit cbf8f85

Please sign in to comment.