-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
ccf2b92
commit cbf8f85
Showing
18 changed files
with
497 additions
and
46 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
44 changes: 44 additions & 0 deletions
44
frontend/src/features/Base/components/BaseOverlay/BaseCard/Item.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
` |
87 changes: 87 additions & 0 deletions
87
frontend/src/features/Base/components/BaseOverlay/BaseCard/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
` |
Oops, something went wrong.