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 30, 2023
1 parent 05ffc08 commit 288112c
Show file tree
Hide file tree
Showing 18 changed files with 496 additions and 45 deletions.
Empty file added CONTRIBUTING.md
Empty file.
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
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 288112c

Please sign in to comment.