Skip to content

Commit

Permalink
Fix vessel track UI
Browse files Browse the repository at this point in the history
  • Loading branch information
louptheron committed Mar 28, 2024
1 parent 31f05d5 commit 55ca073
Show file tree
Hide file tree
Showing 9 changed files with 185 additions and 210 deletions.
3 changes: 2 additions & 1 deletion frontend/src/domain/entities/vessel/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import type { ReportingType } from '../../types/reporting'
import type { VesselTrackDepth } from '../vesselTrackDepth'
import type { Vessel } from '@features/Vessel/Vessel.types'
import type { SelectableVesselTrackDepth } from '@features/VesselSidebar/actions/TrackRequest/types'
import type Feature from 'ol/Feature'
import type LineString from 'ol/geom/LineString'
import type Point from 'ol/geom/Point'
Expand Down Expand Up @@ -190,7 +191,7 @@ export type TrackRequestCustom = {
export type TrackRequestPredefined = {
afterDateTime: null
beforeDateTime: null
trackDepth: Exclude<VesselTrackDepth, 'CUSTOM'>
trackDepth: SelectableVesselTrackDepth
}

export interface VesselPointFeature extends Feature<Point> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,10 @@ const TextValue = styled.div<{
min-width: 120px !important;
width: 120px !important;
}
.rs-stack {
min-width: 125px;
}
`

const Body = styled.div`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ type VesselStatusSelectProps = {
isCleanable?: boolean
marginTop?: number | undefined
updateVesselStatus: (beaconMalfunction: BeaconMalfunction | undefined, status: string | null) => void
// TODO Type vesselStatus in constants.ts
// TODO Type vesselStatus in constants.tsx
vesselStatus: { color: string; icon: JSX.Element; label: string; textColor: string; value: string } | undefined
}
export function VesselStatusSelect({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { SELECT_TRACK_DEPTH_OPTIONS } from '@features/VesselSidebar/actions/TrackRequest/constants'
import { Label, Select } from '@mtes-mct/monitor-ui'
import { useMemo } from 'react'
import { Radio, RadioGroup } from 'rsuite'
import styled from 'styled-components'

import { VesselTrackDepth } from '../../../../domain/entities/vesselTrackDepth'

import type { Promisable } from 'type-fest'

type SelectableVesselTrackDepth = Exclude<VesselTrackDepth, VesselTrackDepth.CUSTOM>

type DateRangeRadioProps = {
defaultValue?: VesselTrackDepth
onChange: (nextTrackDepth: Exclude<VesselTrackDepth, 'CUSTOM'>) => Promisable<void>
onChange: (nextTrackDepth: SelectableVesselTrackDepth | undefined) => Promisable<void>
}
export function DateRangeRadio({ defaultValue, onChange }: DateRangeRadioProps) {
const normalizedDefaultValue = useMemo(
Expand All @@ -17,28 +20,19 @@ export function DateRangeRadio({ defaultValue, onChange }: DateRangeRadioProps)
)

return (
<RadioGroup key={defaultValue} defaultValue={normalizedDefaultValue as any} inline onChange={onChange as any}>
<ColumnsBox>
<Column>
<StyledRadio value={VesselTrackDepth.LAST_DEPARTURE}>le dernier DEP</StyledRadio>
<StyledRadio data-cy="vessel-track-depth-twelve-hours" value={VesselTrackDepth.TWELVE_HOURS}>
12 heures
</StyledRadio>
<StyledRadio value={VesselTrackDepth.ONE_DAY}>24 heures</StyledRadio>
<StyledRadio value={VesselTrackDepth.TWO_DAYS}>2 jours</StyledRadio>
</Column>
<Column>
<StyledRadio data-cy="vessel-track-depth-three-days" value={VesselTrackDepth.THREE_DAYS}>
3 jours
</StyledRadio>
<StyledRadio data-cy="vessel-track-depth-one-week" value={VesselTrackDepth.ONE_WEEK}>
1 semaine
</StyledRadio>
<StyledRadio value={VesselTrackDepth.TWO_WEEK}>2 semaines</StyledRadio>
<StyledRadio value={VesselTrackDepth.ONE_MONTH}>1 mois</StyledRadio>
</Column>
</ColumnsBox>
</RadioGroup>
<ColumnsBox>
<ShowFromLabel>Afficher la piste VMS depuis</ShowFromLabel>
<StyledSelect
isCleanable={false}
isErrorMessageHidden
isLabelHidden
label="Afficher la piste VMS depuis"
name="vessel-track-depth"
onChange={nextValue => onChange(nextValue as SelectableVesselTrackDepth | undefined)}
options={SELECT_TRACK_DEPTH_OPTIONS}
value={normalizedDefaultValue}
/>
</ColumnsBox>
)
}

Expand All @@ -47,19 +41,12 @@ const ColumnsBox = styled.div`
flex-grow: 1;
`

const Column = styled.div`
display: flex;
flex-direction: column;
flex-grow: 0.5;
const ShowFromLabel = styled(Label)`
margin-right: 8px;
line-height: 27px;
margin-left: auto;
`

const StyledRadio = styled(Radio)`
margin-bottom: 4px;
.rs-radio-checker {
label {
padding-left: 8px;
vertical-align: -3px;
}
}
const StyledSelect = styled(Select)`
margin-right: auto;
`
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useMainAppDispatch } from '@hooks/useMainAppDispatch'
import { useMainAppSelector } from '@hooks/useMainAppSelector'
import { transform } from 'ol/proj'
import styled from 'styled-components'

import { getCoordinates } from '../../../../coordinates'
import { OPENLAYERS_PROJECTION, WSG84_PROJECTION } from '../../../../domain/entities/map/constants'
import { animateToCoordinates } from '../../../../domain/shared_slices/Map'
import { highlightVesselTrackPosition } from '../../../../domain/shared_slices/Vessel'
import ManualPositionSVG from '../../../icons/Pastille_position_manuelle.svg?react'

import type { VesselPosition } from '../../../../domain/entities/vessel/types'

type HighlightPositionCellProps = {
isManualPositionMarkerShowed?: boolean
row: VesselPosition
value: unknown
}
export function HighlightPositionCell({ isManualPositionMarkerShowed, row, value }: HighlightPositionCellProps) {
const dispatch = useMainAppDispatch()
const coordinatesFormat = useMainAppSelector(state => state.map.coordinatesFormat)

const coordinates = getCoordinates([row.longitude, row.latitude], WSG84_PROJECTION, coordinatesFormat)
const olCoordinates = transform([row.longitude, row.latitude], WSG84_PROJECTION, OPENLAYERS_PROJECTION)

return (
<span
onClick={() => dispatch(animateToCoordinates(olCoordinates))}
onFocus={() => dispatch(highlightVesselTrackPosition(row))}
onKeyDown={() => dispatch(animateToCoordinates(olCoordinates))}
onMouseEnter={() => dispatch(highlightVesselTrackPosition(row))}
role="presentation"
style={{ cursor: 'pointer' }}
title={row && coordinates ? `${coordinates[0]} ${coordinates[1]}` : ''}
>
{value as string}
{isManualPositionMarkerShowed && row.isManual ? <ManualPosition title="Position manuelle (4h-report)" /> : ''}
</span>
)
}

const ManualPosition = styled(ManualPositionSVG)`
margin-left: 3px;
vertical-align: sub;
`
Original file line number Diff line number Diff line change
@@ -1,188 +1,42 @@
import { transform } from 'ol/proj'
import { useCallback, useEffect, useRef, useState } from 'react'
import { Table } from 'rsuite'
import { POSITION_TABLE_COLUMNS } from '@features/VesselSidebar/actions/TrackRequest/constants'
import { useClickOutsideWhenOpened } from '@hooks/useClickOutsideWhenOpened'
import { useMainAppDispatch } from '@hooks/useMainAppDispatch'
import { useMainAppSelector } from '@hooks/useMainAppSelector'
import { DataTable } from '@mtes-mct/monitor-ui'
import { useEffect, useRef } from 'react'
import styled from 'styled-components'

import { getCoordinates } from '../../../../coordinates'
import { OPENLAYERS_PROJECTION, WSG84_PROJECTION } from '../../../../domain/entities/map/constants'
import { animateToCoordinates } from '../../../../domain/shared_slices/Map'
import { highlightVesselTrackPosition } from '../../../../domain/shared_slices/Vessel'
import { useClickOutsideWhenOpened } from '../../../../hooks/useClickOutsideWhenOpened'
import { useMainAppDispatch } from '../../../../hooks/useMainAppDispatch'
import { useMainAppSelector } from '../../../../hooks/useMainAppSelector'
import { isNumeric } from '../../../../utils/isNumeric'
import ManualPositionSVG from '../../../icons/Pastille_position_manuelle.svg?react'
import { CSVOptions } from '../../../VesselList/dataFormatting'
import { sortArrayByColumn, SortType } from '../../../VesselList/tableSort'

import type { VesselPosition } from 'domain/entities/vessel/types'
import type { CellProps } from 'rsuite'

const { Cell, Column, HeaderCell } = Table

export function PositionsTable({ openBox }) {
const dispatch = useMainAppDispatch()
const { coordinatesFormat } = useMainAppSelector(state => state.map)
const { highlightedVesselTrackPosition, selectedVesselPositions } = useMainAppSelector(state => state.vessel)

const [sortColumn, setSortColumn] = useState(CSVOptions.dateTime.code)
const [sortType, setSortType] = useState(SortType.DESC)
const wrapperRef = useRef(null)
const clickedOutsideComponent = useClickOutsideWhenOpened(wrapperRef, openBox)

const handleSortColumn = useCallback((nextSortColumn, nextSortType) => {
setSortColumn(nextSortColumn)
setSortType(nextSortType)
}, [])

const getPositions = useCallback(() => {
if (sortColumn && sortType && Array.isArray(selectedVesselPositions)) {
return selectedVesselPositions.slice().sort((a, b) => sortArrayByColumn(a, b, sortColumn, sortType))
}

return []
}, [sortColumn, sortType, selectedVesselPositions])

useEffect(() => {
if (clickedOutsideComponent && highlightedVesselTrackPosition) {
dispatch(highlightVesselTrackPosition(null))
}
}, [clickedOutsideComponent, dispatch, highlightedVesselTrackPosition])

return (
<div ref={wrapperRef}>
<Table
data={getPositions()}
height={400}
onSortColumn={handleSortColumn}
rowHeight={36}
shouldUpdateScroll={false}
sortColumn={sortColumn}
sortType={sortType}
virtualized
>
<Column flexGrow={1} sortable>
<HeaderCell>GDH</HeaderCell>
<DateTimeCell coordinatesFormat={coordinatesFormat} dataKey="dateTime" />
</Column>
<Column fixed sortable width={96}>
<HeaderCell>Vitesse</HeaderCell>
<SpeedCell coordinatesFormat={coordinatesFormat} dataKey="speed" />
</Column>
<Column fixed sortable width={64}>
<HeaderCell>Cap</HeaderCell>
<CourseCell coordinatesFormat={coordinatesFormat} dataKey="course" />
</Column>
</Table>
</div>
)
}

type SpeedCellProps = CellProps<VesselPosition> & {
coordinatesFormat: string
dataKey: string
rowData?: {
latitude: number
longitude: number
}
}
export function SpeedCell({ coordinatesFormat, dataKey, rowData, ...nativeProps }: SpeedCellProps) {
const dispatch = useMainAppDispatch()

const coordinates = rowData
? getCoordinates([rowData.longitude, rowData.latitude], WSG84_PROJECTION, coordinatesFormat)
: ''
const olCoordinates = rowData
? transform([rowData.longitude, rowData.latitude], WSG84_PROJECTION, OPENLAYERS_PROJECTION)
: []

return (
<Cell
// eslint-disable-next-line react/jsx-props-no-spreading
{...nativeProps}
onClick={() => dispatch(animateToCoordinates(olCoordinates))}
onMouseEnter={() => dispatch(highlightVesselTrackPosition(rowData))}
style={{ cursor: 'pointer' }}
title={rowData && coordinates ? `${coordinates[0]} ${coordinates[1]}` : ''}
>
{!rowData || !isNumeric(rowData[dataKey]) ? '' : `${rowData[dataKey]} nds`}
</Cell>
)
}

type CourseCellProps = CellProps<VesselPosition> & {
coordinatesFormat: string
dataKey: string
rowData?: {
latitude: number
longitude: number
}
}
export function CourseCell({ coordinatesFormat, dataKey, rowData, ...nativeProps }: CourseCellProps) {
const dispatch = useMainAppDispatch()

const coordinates = rowData
? getCoordinates([rowData.longitude, rowData.latitude], WSG84_PROJECTION, coordinatesFormat)
: ''
const olCoordinates = rowData
? transform([rowData.longitude, rowData.latitude], WSG84_PROJECTION, OPENLAYERS_PROJECTION)
: []

return (
<Cell
// eslint-disable-next-line react/jsx-props-no-spreading
{...nativeProps}
onClick={() => dispatch(animateToCoordinates(olCoordinates))}
onMouseEnter={() => dispatch(highlightVesselTrackPosition(rowData))}
style={{ cursor: 'pointer' }}
title={rowData && coordinates ? `${coordinates[0]} ${coordinates[1]}` : ''}
>
{rowData && (rowData[dataKey] || rowData[dataKey] === 0) ? `${rowData[dataKey]}°` : ''}
</Cell>
)
}

type DateTimeCellProps = CellProps<VesselPosition> & {
coordinatesFormat: string
dataKey: string
rowData?: {
latitude: number
longitude: number
}
}
export function DateTimeCell({ coordinatesFormat, dataKey, rowData, ...nativeProps }: DateTimeCellProps) {
const dispatch = useMainAppDispatch()

const coordinates = rowData
? getCoordinates([rowData.longitude, rowData.latitude], WSG84_PROJECTION, coordinatesFormat)
: ''
const olCoordinates = rowData
? transform([rowData.longitude, rowData.latitude], WSG84_PROJECTION, OPENLAYERS_PROJECTION)
: []

let dateTimeStringWithoutMilliSeconds: string | undefined = rowData ? rowData[dataKey].split('.')[0] : undefined
if (!!rowData && rowData[dataKey].includes('Z') && !dateTimeStringWithoutMilliSeconds?.includes('Z')) {
dateTimeStringWithoutMilliSeconds += 'Z'
} else if (!!rowData && rowData[dataKey].includes('+') && !dateTimeStringWithoutMilliSeconds?.includes('+')) {
dateTimeStringWithoutMilliSeconds += `+${rowData[dataKey].split('+')[1]}`
}

return (
<Cell
// eslint-disable-next-line react/jsx-props-no-spreading
{...nativeProps}
onClick={() => dispatch(animateToCoordinates(olCoordinates))}
onMouseEnter={() => dispatch(highlightVesselTrackPosition(rowData))}
style={{ cursor: 'pointer' }}
title={rowData && coordinates ? `${coordinates[0]} ${coordinates[1]}` : ''}
>
{dateTimeStringWithoutMilliSeconds}{' '}
{rowData?.isManual ? <ManualPosition title="Position manuelle (4h-report)" /> : ''}
</Cell>
<Wrapper ref={wrapperRef}>
<DataTable
// TODO Why `accessorFn` is not defined ?
columns={POSITION_TABLE_COLUMNS as any}
data={selectedVesselPositions?.map((position, index) => ({
...position,
id: index
}))}
initialSorting={[{ desc: true, id: 'dateTime' }]}
/>
</Wrapper>
)
}

const ManualPosition = styled(ManualPositionSVG)`
margin-left: 3px;
vertical-align: sub;
const Wrapper = styled.div`
max-height: 500px;
overflow: auto;
`
Loading

0 comments on commit 55ca073

Please sign in to comment.