1- import React , { FC } from 'react' ;
1+ import React , { FC , useMemo } from 'react' ;
22import LocationMap from 'components/molecules/LocationMap' ;
33import { IWidgetMostSevereAccidentsData } from 'models/WidgetData' ;
44import { Box } from '@material-ui/core' ;
@@ -8,11 +8,59 @@ import { makeStyles } from '@material-ui/core/styles';
88import { useTranslation } from 'react-i18next' ;
99import { aggregatePoints } from 'utils/map.utils' ;
1010import { IAggregatePointAccident } from 'models/Point' ;
11+ import { ClockPosition } from 'models/ClockPosition' ;
12+
13+
14+ function linearRegression ( points : { longitude : number ; latitude : number } [ ] ) {
15+ const n = points . length ;
16+ if ( n < 2 ) return null ;
17+ const sumX = points . reduce ( ( sum , p ) => sum + p . longitude , 0 ) ;
18+ const sumY = points . reduce ( ( sum , p ) => sum + p . latitude , 0 ) ;
19+ const sumXY = points . reduce ( ( sum , p ) => sum + p . longitude * p . latitude , 0 ) ;
20+ const sumXX = points . reduce ( ( sum , p ) => sum + p . longitude * p . longitude , 0 ) ;
21+ const denominator = n * sumXX - sumX * sumX ;
22+ if ( denominator === 0 ) {
23+ // All points have the same longitude (vertical line)
24+ return null ;
25+ }
26+ const slope = ( n * sumXY - sumX * sumY ) / denominator ;
27+ const intercept = ( sumY - slope * sumX ) / n ;
28+ return { slope, intercept } ;
29+ }
30+
31+
32+ function getLabelClockPosition (
33+ slope : number | null ,
34+ mod : number
35+ ) : ClockPosition {
36+ let degrees : number ;
37+ if ( slope === null || ! isFinite ( slope ) ) {
38+ degrees = mod === 0 ? 0 : 180 ;
39+ } else {
40+ const baseAngle = Math . atan ( slope ) ;
41+ const perpAngle = baseAngle + ( mod === 0 ? Math . PI / 2 : - Math . PI / 2 ) ;
42+ degrees = ( ( perpAngle * 180 ) / Math . PI + 360 ) % 360 ; // convert to [0, 360)
43+ }
44+ const snappedAngle = Math . round ( degrees / 45 ) * 45 % 360 ;
45+ const angleToClockPosition : { [ angle : number ] : ClockPosition } = {
46+ 0 : ClockPosition . RIGHT ,
47+ 45 : ClockPosition . TOPRIGHT ,
48+ 90 : ClockPosition . TOP ,
49+ 135 : ClockPosition . TOPLEFT ,
50+ 180 : ClockPosition . LEFT ,
51+ 225 : ClockPosition . BOTTOMLEFT ,
52+ 270 : ClockPosition . BOTTOM ,
53+ 315 : ClockPosition . BOTTOMRIGHT ,
54+ } ;
55+ return angleToClockPosition [ snappedAngle ] ;
56+ }
1157
1258interface IProps {
1359 data : IWidgetMostSevereAccidentsData ;
1460 sizeOptions : number ;
1561}
62+
63+
1664const useStyles = makeStyles ( {
1765 carLogo : {
1866 height : '20px' ,
@@ -40,16 +88,49 @@ const useStyles = makeStyles({
4088 } ,
4189} ) ;
4290
91+
92+ function getSortedItemsAlongLine (
93+ items : IAggregatePointAccident [ ] ,
94+ slope : number | null
95+ ) : IAggregatePointAccident [ ] {
96+ if ( slope === null || ! isFinite ( slope ) ) {
97+ return [ ...items ] . sort ( ( a , b ) => a . latitude - b . latitude ) ;
98+ }
99+ const dx = 1 ;
100+ const dy = slope ;
101+
102+ return [ ...items ] . sort ( ( a , b ) => {
103+ const projA = a . longitude * dx + a . latitude * dy ;
104+ const projB = b . longitude * dx + b . latitude * dy ;
105+ return projA - projB ;
106+ } ) ;
107+ }
108+
109+
43110const MostSevereAccidentsMapWidget : FC < IProps > = ( { data } ) => {
44111 const classes = useStyles ( ) ;
45112 const { items } = data ;
46113 const { t } = useTranslation ( ) ;
47- const aggregatedItems : IAggregatePointAccident [ ] = aggregatePoints ( items ) ;
114+ const aggregatedItems : IAggregatePointAccident [ ] = useMemo (
115+ ( ) => aggregatePoints ( items ) ,
116+ [ items ]
117+ ) ;
118+ const itemsWithLabelPosition = useMemo ( ( ) => {
119+ const regression = linearRegression ( aggregatedItems ) ;
120+ const slope = regression ? regression . slope : null ;
121+ const sortedItems = getSortedItemsAlongLine ( aggregatedItems , slope ) ;
122+ return sortedItems . map ( ( item , idx ) => ( {
123+ ...item ,
124+ labelClockPosition : getLabelClockPosition ( slope , idx % 2 ) ,
125+ } ) ) ;
126+ } , [ aggregatedItems ] ) ;
48127
49128 return (
50129 < Box height = { '100%' } >
51130 < Box className = { classes . mapBox } >
52- < LocationMap items = { aggregatedItems } />
131+ { itemsWithLabelPosition . length > 0 && (
132+ < LocationMap key = { itemsWithLabelPosition . map ( i => i . accident_timestamp ) . join ( ',' ) } items = { itemsWithLabelPosition } />
133+ ) }
53134 </ Box >
54135 < Box className = { classes . iconsContainer } >
55136 < Box className = { classes . singleIcon } >
0 commit comments