Revize 812b9f90
Přidáno uživatelem Václav Honzík před více než 2 roky(ů)
frontend/package.json | ||
---|---|---|
17 | 17 |
"jwt-decode": "^3.1.2", |
18 | 18 |
"leaflet": "^1.8.0", |
19 | 19 |
"react": "^17.0.2", |
20 |
"react-beautiful-dnd": "^13.1.0", |
|
20 | 21 |
"react-dom": "^17.0.2", |
21 | 22 |
"react-html-parser": "^2.0.2", |
22 | 23 |
"react-leaflet": "3.2.5", |
... | ... | |
31 | 32 |
"swagger-typescript-api": "^9.3.1", |
32 | 33 |
"ts-node": "^10.7.0", |
33 | 34 |
"typescript": "^4.4.2", |
35 |
"uuid": "^8.3.2", |
|
34 | 36 |
"web-vitals": "^2.1.0", |
35 | 37 |
"yup": "^0.32.11" |
36 | 38 |
}, |
... | ... | |
68 | 70 |
"@types/leaflet": "^1.7.9", |
69 | 71 |
"@types/node": "^16.7.13", |
70 | 72 |
"@types/react": "^17.0.43", |
73 |
"@types/react-beautiful-dnd": "^13.1.2", |
|
71 | 74 |
"@types/react-dom": "^17.0.9", |
72 | 75 |
"@types/react-redux": "^7.1.23", |
73 | 76 |
"@types/redux-persist": "^4.3.1", |
77 |
"@types/uuid": "^8.3.4", |
|
74 | 78 |
"@types/yup": "^0.29.13", |
75 | 79 |
"redux-devtools-extension": "^2.13.9" |
76 | 80 |
} |
frontend/src/features/Reusables/DraggableMarker.tsx | ||
---|---|---|
1 |
import { EventedProps } from '@react-leaflet/core' |
|
2 |
import { LatLngExpression, Marker, MarkerOptions } from 'leaflet' |
|
3 |
import { FunctionComponent, ReactNode, useCallback, useMemo, useRef, useState } from 'react' |
|
4 |
|
|
5 |
export interface DraggableMarkerProps extends MarkerOptions, EventedProps { |
|
6 |
children?: ReactNode |
|
7 |
position: LatLngExpression |
|
8 |
} |
|
9 |
|
|
10 |
const DraggableMarker: FunctionComponent<DraggableMarkerProps> = ({ children, position }) => { |
|
11 |
const [draggable, setDraggable] = useState(false) |
|
12 |
const [pos, setPos] = useState(position) |
|
13 |
const markerRef = useRef(null) |
|
14 |
const eventHandlers = useMemo( |
|
15 |
() => ({ |
|
16 |
dragend() { |
|
17 |
const marker: any = markerRef.current |
|
18 |
if (marker != null) { |
|
19 |
setPos(marker.getLatLng()) |
|
20 |
} |
|
21 |
}, |
|
22 |
}), |
|
23 |
[] |
|
24 |
) |
|
25 |
const toggleDraggable = useCallback(() => { |
|
26 |
setDraggable((d) => !d) |
|
27 |
}, []) |
|
28 |
|
|
29 |
return ( |
|
30 |
// @ts-ignore |
|
31 |
<Marker |
|
32 |
draggable={draggable} |
|
33 |
eventHandlers={eventHandlers} |
|
34 |
position={pos} |
|
35 |
ref={markerRef} |
|
36 |
> |
|
37 |
{children} |
|
38 |
</Marker> |
|
39 |
) |
|
40 |
} |
|
41 |
|
|
42 |
export default DraggableMarker |
frontend/src/features/TrackingTool/DraggableList/DraggableList.tsx | ||
---|---|---|
1 |
import { memo } from 'react' |
|
2 |
import { DragDropContext, Droppable, OnDragEndResponder } from 'react-beautiful-dnd' |
|
3 |
import { MapPoint } from '../Map/pathUtils' |
|
4 |
import DraggableListItem from './DraggableListItem' |
|
5 |
|
|
6 |
export interface DraggableListProps { |
|
7 |
items: MapPoint[] |
|
8 |
onDragEnd: OnDragEndResponder |
|
9 |
} |
|
10 |
|
|
11 |
const DraggableList = memo(({ items, onDragEnd }: DraggableListProps) => { |
|
12 |
return ( |
|
13 |
<DragDropContext onDragEnd={onDragEnd}> |
|
14 |
<Droppable droppableId="droppable-list"> |
|
15 |
{(provided) => ( |
|
16 |
<div ref={provided.innerRef} {...provided.droppableProps}> |
|
17 |
{items.map((item, index) => ( |
|
18 |
<DraggableListItem |
|
19 |
list={items} |
|
20 |
index={index} |
|
21 |
key={item.id} |
|
22 |
/> |
|
23 |
))} |
|
24 |
{provided.placeholder} |
|
25 |
</div> |
|
26 |
)} |
|
27 |
</Droppable> |
|
28 |
</DragDropContext> |
|
29 |
) |
|
30 |
}) |
|
31 |
|
|
32 |
export default DraggableList |
frontend/src/features/TrackingTool/DraggableList/DraggableListItem.tsx | ||
---|---|---|
1 |
import { |
|
2 |
Avatar, |
|
3 |
Checkbox, |
|
4 |
ListItem, |
|
5 |
ListItemAvatar, |
|
6 |
ListItemText, |
|
7 |
} from '@mui/material' |
|
8 |
import makeStyles from '@mui/material/styles/makeStyles' |
|
9 |
import { Draggable } from 'react-beautiful-dnd' |
|
10 |
import { MapPoint, PathVariant } from '../Map/pathUtils' |
|
11 |
import LocationOnIcon from '@mui/icons-material/LocationOn' |
|
12 |
import { CatalogItemDto } from '../../../swagger/data-contracts' |
|
13 |
import { useDispatch } from 'react-redux' |
|
14 |
import { updateMapMarker, updateMapMarkerWithId } from '../trackingToolSlice' |
|
15 |
import { Fragment, useEffect, useState } from 'react' |
|
16 |
|
|
17 |
export type DraggableListItemProps = { |
|
18 |
list: PathVariant |
|
19 |
index: number |
|
20 |
} |
|
21 |
|
|
22 |
const getFormattedLocationOrEmpty = (catalogItem: CatalogItemDto) => { |
|
23 |
if (!catalogItem || !catalogItem.latitude || !catalogItem.longitude) { |
|
24 |
return 'Location Unknown' |
|
25 |
} |
|
26 |
|
|
27 |
return `${catalogItem.latitude}°, ${catalogItem.longitude}°` |
|
28 |
} |
|
29 |
|
|
30 |
const DraggableListItem = ({ list, index }: DraggableListItemProps) => { |
|
31 |
const item = list[index] |
|
32 |
const dispatch = useDispatch() |
|
33 |
const toggleHidden = () => { |
|
34 |
dispatch( |
|
35 |
updateMapMarkerWithId({ |
|
36 |
item: { |
|
37 |
...item, |
|
38 |
active: !item?.active, |
|
39 |
} as MapPoint, |
|
40 |
id: item.id, |
|
41 |
}) |
|
42 |
) |
|
43 |
} |
|
44 |
|
|
45 |
return ( |
|
46 |
<Draggable key={`${item.id}`} draggableId={`${item.id}`} index={index}> |
|
47 |
{(provided, snapshot) => ( |
|
48 |
<ListItem |
|
49 |
ref={provided.innerRef} |
|
50 |
{...provided.draggableProps} |
|
51 |
{...provided.dragHandleProps} |
|
52 |
sx={{ |
|
53 |
background: snapshot.isDragging ? 'rgb(235,235,235)' : 'inherit', |
|
54 |
}} |
|
55 |
> |
|
56 |
<ListItemAvatar> |
|
57 |
<Avatar> |
|
58 |
<LocationOnIcon /> |
|
59 |
</Avatar> |
|
60 |
</ListItemAvatar> |
|
61 |
<ListItemText |
|
62 |
primary={item.catalogItem.name ?? 'Unknown name'} |
|
63 |
secondary={getFormattedLocationOrEmpty( |
|
64 |
item.catalogItem |
|
65 |
)} |
|
66 |
/> |
|
67 |
<Checkbox checked={item.active} onChange={toggleHidden} /> |
|
68 |
</ListItem> |
|
69 |
)} |
|
70 |
</Draggable> |
|
71 |
) |
|
72 |
} |
|
73 |
|
|
74 |
export default DraggableListItem |
frontend/src/features/TrackingTool/DraggableList/DraggableMarkerList.tsx | ||
---|---|---|
1 |
import { Paper } from '@mui/material' |
|
2 |
import { useEffect, useState } from 'react' |
|
3 |
import { DropResult } from 'react-beautiful-dnd' |
|
4 |
import { useDispatch, useSelector } from 'react-redux' |
|
5 |
import { RootState } from '../../redux/store' |
|
6 |
import { swapMapMarkerIndices } from '../trackingToolSlice' |
|
7 |
import { PathVariant } from '../Map/pathUtils' |
|
8 |
import DraggableList from './DraggableList' |
|
9 |
|
|
10 |
const DraggableMarkerList = () => { |
|
11 |
const dispatch = useDispatch() |
|
12 |
|
|
13 |
// List of all paths |
|
14 |
const paths = useSelector( |
|
15 |
(state: RootState) => state.trackingTool.pathVariants |
|
16 |
) |
|
17 |
|
|
18 |
// Primary path index - i.e. the selected path |
|
19 |
const primaryPathIdx = useSelector( |
|
20 |
(state: RootState) => state.trackingTool.primaryPathIdx |
|
21 |
) |
|
22 |
|
|
23 |
// Selected path as local state |
|
24 |
const [path, setPath] = useState<PathVariant | undefined>() |
|
25 |
|
|
26 |
// Set localstate path whenever it changes in the store |
|
27 |
useEffect(() => { |
|
28 |
if (!paths || paths.length < primaryPathIdx) { |
|
29 |
setPath(undefined) |
|
30 |
return |
|
31 |
} |
|
32 |
|
|
33 |
setPath(paths[primaryPathIdx]) |
|
34 |
}, [paths, primaryPathIdx]) |
|
35 |
|
|
36 |
const onDragEnd = ({ destination, source }: DropResult) => { |
|
37 |
if (!destination || !source || destination.index === source.index) { |
|
38 |
return |
|
39 |
} |
|
40 |
|
|
41 |
dispatch( |
|
42 |
swapMapMarkerIndices({ |
|
43 |
source: source.index, |
|
44 |
destination: destination.index, |
|
45 |
}) |
|
46 |
) |
|
47 |
} |
|
48 |
|
|
49 |
return ( |
|
50 |
<Paper variant="outlined"> |
|
51 |
<DraggableList items={path ?? []} onDragEnd={onDragEnd} /> |
|
52 |
</Paper> |
|
53 |
) |
|
54 |
} |
|
55 |
|
|
56 |
export default DraggableMarkerList |
frontend/src/features/TrackingTool/Map/MapMarker.tsx | ||
---|---|---|
1 |
import { LatLngTuple, Marker as MarkerPOJO } from 'leaflet' |
|
2 |
import { FunctionComponent, ReactNode, useMemo, useRef, useState } from 'react' |
|
3 |
import { Marker } from 'react-leaflet' |
|
4 |
|
|
5 |
export interface MapMarkerProps { |
|
6 |
position: LatLngTuple |
|
7 |
children?: ReactNode |
|
8 |
color?: 'external' | 'disabled' | 'localCatalog' |
|
9 |
updatePositionCallbackFn: (position: LatLngTuple) => void // Callback function to notify MapPath to rerender the path |
|
10 |
} |
|
11 |
|
|
12 |
// Custom Map Marker component |
|
13 |
const MapMarker: FunctionComponent<MapMarkerProps> = ({ |
|
14 |
position, |
|
15 |
children, |
|
16 |
updatePositionCallbackFn, |
|
17 |
}) => { |
|
18 |
const [currentPosition, setCurrentPosition] = useState(position) |
|
19 |
const markerRef = useRef<MarkerPOJO | null>(null) |
|
20 |
const eventHandlers = useMemo( |
|
21 |
() => ({ |
|
22 |
dragend: () => { |
|
23 |
const marker = markerRef.current |
|
24 |
console.log(markerRef) |
|
25 |
if (!marker) { |
|
26 |
return |
|
27 |
} |
|
28 |
const latlng = marker.getLatLng() |
|
29 |
setCurrentPosition([latlng.lat, latlng.lng]) |
|
30 |
updatePositionCallbackFn([latlng.lat, latlng.lng]) |
|
31 |
}, |
|
32 |
}), |
|
33 |
[updatePositionCallbackFn] |
|
34 |
) |
|
35 |
|
|
36 |
return ( |
|
37 |
<Marker |
|
38 |
draggable={true} |
|
39 |
position={currentPosition} |
|
40 |
eventHandlers={eventHandlers} |
|
41 |
ref={markerRef} |
|
42 |
> |
|
43 |
{children} |
|
44 |
</Marker> |
|
45 |
) |
|
46 |
} |
|
47 |
|
|
48 |
export default MapMarker |
frontend/src/features/TrackingTool/Map/MapPath.tsx | ||
---|---|---|
1 |
import { Fragment, FunctionComponent, useEffect, useState } from 'react' |
|
2 |
import { useDispatch, useSelector } from 'react-redux' |
|
3 |
import { RootState } from '../../redux/store' |
|
4 |
import { PathVariant, MapPoint, isMapPointDisplayable } from './pathUtils' |
|
5 |
import TextPath from 'react-leaflet-textpath' |
|
6 |
import { setPrimaryIdx, updateMapMarker } from '../trackingToolSlice' |
|
7 |
import MapMarker from './MapMarker' |
|
8 |
import { LatLngTuple } from 'leaflet' |
|
9 |
import { Popup, Tooltip } from 'react-leaflet' |
|
10 |
import { Checkbox, FormControlLabel, Stack, Typography } from '@mui/material' |
|
11 |
import { formatHtmlStringToReactDom } from '../../../utils/formatting/HtmlUtils' |
|
12 |
import { DialogCatalogItemDetail as CatalogItemDetailDialog } from '../../Catalog/CatalogItemDetail' |
|
13 |
|
|
14 |
export interface MapPathProps { |
|
15 |
idx: number // index of the path in the list |
|
16 |
} |
|
17 |
|
|
18 |
type EdgeElement = any |
|
19 |
|
|
20 |
// Blue |
|
21 |
export const primaryPathColor = '#346eeb' |
|
22 |
|
|
23 |
// Grey |
|
24 |
export const secondaryPathColor = '#878e9c' |
|
25 |
|
|
26 |
const MapPath: FunctionComponent<MapPathProps> = ({ idx }) => { |
|
27 |
const dispatch = useDispatch() |
|
28 |
|
|
29 |
// Get list of all paths from the store |
|
30 |
// And extract path from them |
|
31 |
const paths = useSelector( |
|
32 |
(state: RootState) => state.trackingTool.pathVariants |
|
33 |
) |
|
34 |
const [path, setPath] = useState<PathVariant>([]) |
|
35 |
useEffect(() => { |
|
36 |
// Either set the path if it exists or set it to an empty array |
|
37 |
setPath(paths && paths.length > idx ? paths[idx] : []) |
|
38 |
}, [idx, paths]) |
|
39 |
|
|
40 |
// Primary path index to set the correct color |
|
41 |
const primaryPathIdx = useSelector( |
|
42 |
(state: RootState) => state.trackingTool.primaryPathIdx |
|
43 |
) |
|
44 |
|
|
45 |
// List of all active map points |
|
46 |
const [displayableMapPoints, setDisplayableMapPoints] = useState< |
|
47 |
MapPoint[] |
|
48 |
>([]) |
|
49 |
useEffect(() => { |
|
50 |
// Set all displayable vertices |
|
51 |
setDisplayableMapPoints( |
|
52 |
path.filter((mapPoint) => isMapPointDisplayable(mapPoint)) |
|
53 |
) |
|
54 |
}, [path]) |
|
55 |
|
|
56 |
// List of all edges in the path |
|
57 |
const [edges, setEdges] = useState<EdgeElement[]>([]) |
|
58 |
useEffect(() => { |
|
59 |
// Get all active map points |
|
60 |
const activeMapPoints = displayableMapPoints.filter( |
|
61 |
(item) => item.active |
|
62 |
) |
|
63 |
if (activeMapPoints.length < 2) { |
|
64 |
setEdges([]) |
|
65 |
return |
|
66 |
} |
|
67 |
|
|
68 |
// Build edges |
|
69 |
const edges = [] |
|
70 |
for (let i = 0; i < activeMapPoints.length - 1; i += 1) { |
|
71 |
const [start, end] = [ |
|
72 |
activeMapPoints[i].catalogItem, |
|
73 |
activeMapPoints[i + 1].catalogItem, |
|
74 |
] |
|
75 |
edges.push( |
|
76 |
<TextPath |
|
77 |
// Somehow this refuses to work so let it rerender everything ... |
|
78 |
key={`${start.id}-${end.id}:${start.latitude},${start.longitude}-${end.latitude},${end.longitude}`} |
|
79 |
positions={[ |
|
80 |
[start.latitude, start.longitude], |
|
81 |
[end.latitude, end.longitude], |
|
82 |
]} |
|
83 |
text="►" |
|
84 |
// text=" > > > > " |
|
85 |
attributes={{ |
|
86 |
'font-size': 19, |
|
87 |
// Set to primaryPathColor if primary index in the tracking tool is equal to this index |
|
88 |
fill: |
|
89 |
primaryPathIdx === idx |
|
90 |
? primaryPathColor |
|
91 |
: secondaryPathColor, |
|
92 |
}} |
|
93 |
onClick={() => dispatch(setPrimaryIdx(idx))} |
|
94 |
repeat |
|
95 |
center |
|
96 |
weight={0} |
|
97 |
/> |
|
98 |
) |
|
99 |
} |
|
100 |
setEdges(edges) |
|
101 |
}, [dispatch, displayableMapPoints, idx, primaryPathIdx]) |
|
102 |
|
|
103 |
// List of vertices to display |
|
104 |
const [vertices, setVertices] = useState<JSX.Element[]>([]) |
|
105 |
useEffect(() => { |
|
106 |
// Iterate over all displayable map points and map them to MapMarker |
|
107 |
setVertices( |
|
108 |
displayableMapPoints.map((item) => ( |
|
109 |
<MapMarker |
|
110 |
key={`${item.catalogItem.latitude}${item.catalogItem.longitude}`} |
|
111 |
position={[ |
|
112 |
item.catalogItem.latitude as number, |
|
113 |
item.catalogItem.longitude as number, |
|
114 |
]} |
|
115 |
updatePositionCallbackFn={(position: LatLngTuple) => { |
|
116 |
dispatch( |
|
117 |
updateMapMarker({ |
|
118 |
item: { |
|
119 |
...item, |
|
120 |
catalogItem: { |
|
121 |
...item.catalogItem, |
|
122 |
latitude: position[0], |
|
123 |
longitude: position[1], |
|
124 |
}, |
|
125 |
}, |
|
126 |
}) |
|
127 |
) |
|
128 |
}} |
|
129 |
> |
|
130 |
<Fragment> |
|
131 |
<Tooltip> |
|
132 |
{/* <Typography> */} |
|
133 |
{item.catalogItem.name ?? ''} |
|
134 |
{/* </Typography> */} |
|
135 |
</Tooltip> |
|
136 |
<Popup> |
|
137 |
<Fragment> |
|
138 |
<Stack direction="column" sx={{ m: 0 }}> |
|
139 |
<Typography |
|
140 |
variant="h6" |
|
141 |
fontWeight="bold" |
|
142 |
fontSize={16} |
|
143 |
> |
|
144 |
{formatHtmlStringToReactDom( |
|
145 |
item.catalogItem.name as string |
|
146 |
)} |
|
147 |
</Typography> |
|
148 |
<FormControlLabel |
|
149 |
control={ |
|
150 |
<Checkbox |
|
151 |
checked={item.active} |
|
152 |
onChange={() => { |
|
153 |
dispatch( |
|
154 |
updateMapMarker({ |
|
155 |
item: { |
|
156 |
...item, |
|
157 |
active: !item.active, |
|
158 |
}, |
|
159 |
}) |
|
160 |
) |
|
161 |
}} |
|
162 |
/> |
|
163 |
} |
|
164 |
labelPlacement="end" |
|
165 |
label="Active" |
|
166 |
/> |
|
167 |
<CatalogItemDetailDialog |
|
168 |
itemId={item.catalogItem.id ?? ''} |
|
169 |
/> |
|
170 |
</Stack> |
|
171 |
</Fragment> |
|
172 |
</Popup> |
|
173 |
</Fragment> |
|
174 |
</MapMarker> |
|
175 |
)) |
|
176 |
) |
|
177 |
}, [dispatch, displayableMapPoints, idx]) |
|
178 |
|
|
179 |
return ( |
|
180 |
<Fragment> |
|
181 |
{vertices} |
|
182 |
{edges} |
|
183 |
</Fragment> |
|
184 |
) |
|
185 |
} |
|
186 |
|
|
187 |
export default MapPath |
frontend/src/features/TrackingTool/Map/pathUtils.ts | ||
---|---|---|
1 |
// Business logic for tracking tool |
|
2 |
|
|
3 |
import { CatalogItemDto, PathDto } from '../../../swagger/data-contracts' |
|
4 |
import generateUuid from '../../../utils/id/uuidGenerator' |
|
5 |
|
|
6 |
// For more comprehensive code alias CatalogItemDto[] as path variant |
|
7 |
export type PathVariant = MapPoint[] |
|
8 |
|
|
9 |
export enum MapPointType { |
|
10 |
LocalCatalog, // Fetched from local catalog |
|
11 |
ExternalCatalog, // Fetched from external catalog |
|
12 |
GeoJson, // From GeoJSON file |
|
13 |
FromCoordinates, // From coordinates |
|
14 |
} |
|
15 |
|
|
16 |
// Represents a point on the map - wrapper for CatalogItemDto to make it easier to work with |
|
17 |
export interface MapPoint { |
|
18 |
id: string // unique id for react |
|
19 |
idx: number, |
|
20 |
active: boolean, |
|
21 |
catalogItem: CatalogItemDto, |
|
22 |
type: MapPointType |
|
23 |
} |
|
24 |
|
|
25 |
export const isMapPointDisplayable = (mapPoint: MapPoint): boolean => |
|
26 |
!!mapPoint.catalogItem.latitude && !!mapPoint.catalogItem.longitude |
|
27 |
|
|
28 |
/** |
|
29 |
* Cartesian product of two arrays |
|
30 |
* @param sets |
|
31 |
* @returns |
|
32 |
*/ |
|
33 |
const cartesianProduct = (sets: CatalogItemDto[][]): CatalogItemDto[][] => |
|
34 |
sets.reduce<CatalogItemDto[][]>( |
|
35 |
(results, ids) => |
|
36 |
results |
|
37 |
.map((result) => ids.map((id) => [...result, id])) |
|
38 |
.reduce((nested, result) => [...nested, ...result]), |
|
39 |
[[]] |
|
40 |
) |
|
41 |
|
|
42 |
/** |
|
43 |
* Builds a list of all possible path variants from pathDto |
|
44 |
* @param pathDto |
|
45 |
* @returns |
|
46 |
*/ |
|
47 |
export const buildPathVariants = (pathDto: PathDto, mapPointType: MapPointType = MapPointType.LocalCatalog): PathVariant[] => { |
|
48 |
if (!pathDto.foundCatalogItems) { |
|
49 |
return [] |
|
50 |
} |
|
51 |
|
|
52 |
return ( |
|
53 |
pathDto.foundCatalogItems.length === 1 |
|
54 |
? pathDto.foundCatalogItems |
|
55 |
: cartesianProduct(pathDto.foundCatalogItems) |
|
56 |
).map((variant, _) => |
|
57 |
variant.map( |
|
58 |
(catalogItem, idx) => ( |
|
59 |
{ |
|
60 |
id: generateUuid(), |
|
61 |
idx, |
|
62 |
active: !!catalogItem.latitude && !!catalogItem.longitude, |
|
63 |
catalogItem, |
|
64 |
type: mapPointType, |
|
65 |
}) |
|
66 |
) |
|
67 |
) |
|
68 |
} |
|
69 |
|
|
70 |
export default buildPathVariants |
frontend/src/features/TrackingTool/MapMarker.tsx | ||
---|---|---|
1 |
import { LatLngTuple, Marker as MarkerPOJO } from "leaflet" |
|
2 |
import { FunctionComponent, ReactNode, useMemo, useRef, useState } from "react" |
|
3 |
import { Marker } from "react-leaflet" |
|
4 |
|
|
5 |
export interface MapMarkerProps { |
|
6 |
position: LatLngTuple |
|
7 |
children?: ReactNode |
|
8 |
color?: "external" | "disabled" | "localCatalog" |
|
9 |
updatePositionCallbackFn: (position: LatLngTuple) => void // Callback function to notify MapPath to rerender the path |
|
10 |
} |
|
11 |
|
|
12 |
// Custom Map Marker component |
|
13 |
const MapMarker: FunctionComponent<MapMarkerProps> = ({ |
|
14 |
position, |
|
15 |
children, |
|
16 |
updatePositionCallbackFn, |
|
17 |
}) => { |
|
18 |
const [currentPosition, setCurrentPosition] = useState(position) |
|
19 |
const markerRef = useRef<MarkerPOJO | null>(null) |
|
20 |
const eventHandlers = useMemo( |
|
21 |
() => ({ |
|
22 |
dragend: () => { |
|
23 |
const marker = markerRef.current |
|
24 |
console.log(markerRef) |
|
25 |
if (!marker) { |
|
26 |
return |
|
27 |
} |
|
28 |
const latlng = marker.getLatLng() |
|
29 |
setCurrentPosition([latlng.lat, latlng.lng]) |
|
30 |
updatePositionCallbackFn([latlng.lat, latlng.lng]) |
|
31 |
}, |
|
32 |
}), |
|
33 |
[updatePositionCallbackFn] |
|
34 |
) |
|
35 |
|
|
36 |
return ( |
|
37 |
<Marker |
|
38 |
draggable={true} |
|
39 |
position={currentPosition} |
|
40 |
eventHandlers={eventHandlers} |
|
41 |
ref={markerRef} |
|
42 |
> |
|
43 |
{children} |
|
44 |
</Marker> |
|
45 |
) |
|
46 |
} |
|
47 |
|
|
48 |
export default MapMarker |
frontend/src/features/TrackingTool/MapPath.tsx | ||
---|---|---|
1 |
import { Fragment, FunctionComponent, useEffect, useState } from 'react' |
|
2 |
import { useDispatch, useSelector } from 'react-redux' |
|
3 |
import { RootState } from '../redux/store' |
|
4 |
import { PathVariant, MapPoint, isMapPointDisplayable } from './pathUtils' |
|
5 |
import TextPath from 'react-leaflet-textpath' |
|
6 |
import { setPrimaryIdx, updateMapMarker } from './trackingToolSlice' |
|
7 |
import MapMarker from './MapMarker' |
|
8 |
import { LatLngTuple } from 'leaflet' |
|
9 |
import { Popup, Tooltip } from 'react-leaflet' |
|
10 |
import { Checkbox, FormControlLabel, Stack, Typography } from '@mui/material' |
|
11 |
import { formatHtmlStringToReactDom } from '../../utils/formatting/HtmlUtils' |
|
12 |
import { DialogCatalogItemDetail as CatalogItemDetailDialog } from '../Catalog/CatalogItemDetail' |
|
13 |
|
|
14 |
export interface MapPathProps { |
|
15 |
idx: number // index of the path in the list |
|
16 |
} |
|
17 |
|
|
18 |
type EdgeElement = any |
|
19 |
|
|
20 |
// Blue |
|
21 |
export const primaryPathColor = '#346eeb' |
|
22 |
|
|
23 |
// Grey |
|
24 |
export const secondaryPathColor = '#878e9c' |
|
25 |
|
|
26 |
const MapPath: FunctionComponent<MapPathProps> = ({ idx }) => { |
|
27 |
const dispatch = useDispatch() |
|
28 |
|
|
29 |
// Get list of all paths from the store |
|
30 |
// And extract path from them |
|
31 |
const paths = useSelector( |
|
32 |
(state: RootState) => state.trackingTool.pathVariants |
|
33 |
) |
|
34 |
const [path, setPath] = useState<PathVariant>([]) |
|
35 |
useEffect(() => { |
|
36 |
// Either set the path if it exists or set it to an empty array |
|
37 |
setPath(paths && paths.length > idx ? paths[idx] : []) |
|
38 |
}, [idx, paths]) |
|
39 |
|
|
40 |
// Primary path index to set the correct color |
|
41 |
const primaryPathIdx = useSelector( |
|
42 |
(state: RootState) => state.trackingTool.primaryPathIdx |
|
43 |
) |
|
44 |
|
|
45 |
// List of all active map points |
|
46 |
const [displayableMapPoints, setDisplayableMapPoints] = useState< |
|
47 |
MapPoint[] |
|
48 |
>([]) |
|
49 |
useEffect(() => { |
|
50 |
// Set all displayable vertices |
|
51 |
setDisplayableMapPoints( |
|
52 |
path.filter((mapPoint) => isMapPointDisplayable(mapPoint)) |
|
53 |
) |
|
54 |
}, [path]) |
|
55 |
|
|
56 |
// List of all edges in the path |
|
57 |
const [edges, setEdges] = useState<EdgeElement[]>([]) |
|
58 |
useEffect(() => { |
|
59 |
// Get all active map points |
|
60 |
const activeMapPoints = displayableMapPoints.filter( |
|
61 |
(item) => item.active |
|
62 |
) |
|
63 |
if (activeMapPoints.length < 2) { |
|
64 |
setEdges([]) |
|
65 |
return |
|
66 |
} |
|
67 |
|
|
68 |
// Build edges |
|
69 |
const edges = [] |
|
70 |
for (let i = 0; i < activeMapPoints.length - 1; i += 1) { |
|
71 |
const [start, end] = [ |
|
72 |
activeMapPoints[i].catalogItem, |
|
73 |
activeMapPoints[i + 1].catalogItem, |
|
74 |
] |
|
75 |
edges.push( |
|
76 |
<TextPath |
|
77 |
// Somehow this refuses to work so let it rerender everything ... |
|
78 |
key={`${start.id}-${end.id}:${start.latitude},${start.longitude}-${end.latitude},${end.longitude}`} |
|
79 |
positions={[ |
|
80 |
[start.latitude, start.longitude], |
|
81 |
[end.latitude, end.longitude], |
|
82 |
]} |
|
83 |
text="►" |
|
84 |
// text=" > > > > " |
|
85 |
attributes={{ |
|
86 |
'font-size': 19, |
|
87 |
// Set to primaryPathColor if primary index in the tracking tool is equal to this index |
|
88 |
fill: |
|
89 |
primaryPathIdx === idx |
|
90 |
? primaryPathColor |
|
91 |
: secondaryPathColor, |
|
92 |
}} |
|
93 |
onClick={() => dispatch(setPrimaryIdx(idx))} |
|
94 |
repeat |
|
95 |
center |
|
96 |
weight={0} |
|
97 |
/> |
|
98 |
) |
|
99 |
} |
|
100 |
setEdges(edges) |
|
101 |
}, [dispatch, displayableMapPoints, idx, primaryPathIdx]) |
|
102 |
|
|
103 |
// List of vertices to display |
|
104 |
const [vertices, setVertices] = useState<JSX.Element[]>([]) |
|
105 |
useEffect(() => { |
|
106 |
// Iterate over all displayable map points and map them to MapMarker |
|
107 |
setVertices( |
|
108 |
displayableMapPoints.map((item) => ( |
|
109 |
<MapMarker |
|
110 |
key={`${item.catalogItem.latitude}${item.catalogItem.longitude}`} |
|
111 |
position={[ |
|
112 |
item.catalogItem.latitude as number, |
|
113 |
item.catalogItem.longitude as number, |
|
114 |
]} |
|
115 |
updatePositionCallbackFn={(position: LatLngTuple) => { |
|
116 |
dispatch( |
|
117 |
updateMapMarker({ |
|
118 |
idx, |
|
119 |
item: { |
|
120 |
idx: item.idx, |
|
121 |
active: item.active, |
|
122 |
catalogItem: { |
|
123 |
...item.catalogItem, |
|
124 |
latitude: position[0], |
|
125 |
longitude: position[1], |
|
126 |
}, |
|
127 |
}, |
|
128 |
}) |
|
129 |
) |
|
130 |
}} |
|
131 |
> |
|
132 |
<Fragment> |
|
133 |
<Tooltip> |
|
134 |
{/* <Typography> */} |
|
135 |
{item.catalogItem.name ?? ''} |
|
136 |
{/* </Typography> */} |
|
137 |
</Tooltip> |
|
138 |
<Popup> |
|
139 |
<Fragment> |
|
140 |
<Stack direction="column" sx={{ m: 0 }}> |
|
141 |
<Typography |
|
142 |
variant="h6" |
|
143 |
fontWeight="bold" |
|
144 |
fontSize={16} |
|
145 |
> |
|
146 |
{formatHtmlStringToReactDom( |
|
147 |
item.catalogItem.name as string |
|
148 |
)} |
|
149 |
</Typography> |
|
150 |
<FormControlLabel |
|
151 |
control={ |
|
152 |
<Checkbox |
|
153 |
checked={item.active} |
|
154 |
onChange={() => { |
|
155 |
dispatch( |
|
156 |
updateMapMarker({ |
|
157 |
idx, |
|
158 |
item: { |
|
159 |
...item, |
|
160 |
active: !item.active, |
|
161 |
}, |
|
162 |
}) |
|
163 |
) |
|
164 |
}} |
|
165 |
/> |
|
166 |
} |
|
167 |
labelPlacement="end" |
|
168 |
label="Active" |
|
169 |
/> |
|
170 |
<CatalogItemDetailDialog |
|
171 |
itemId={item.catalogItem.id ?? ''} |
|
172 |
/> |
|
173 |
</Stack> |
|
174 |
</Fragment> |
|
175 |
</Popup> |
|
176 |
</Fragment> |
|
177 |
</MapMarker> |
|
178 |
)) |
|
179 |
) |
|
180 |
}, [dispatch, displayableMapPoints, idx]) |
|
181 |
|
|
182 |
return ( |
|
183 |
<Fragment> |
|
184 |
{vertices} |
|
185 |
{edges} |
|
186 |
</Fragment> |
|
187 |
) |
|
188 |
} |
|
189 |
|
|
190 |
export default MapPath |
frontend/src/features/TrackingTool/ProcessedText/ProcessedTextDisplay.tsx | ||
---|---|---|
1 |
import { Card, CardContent, Stack, Typography } from '@mui/material' |
|
2 |
import { Fragment } from 'react' |
|
3 |
import { useSelector } from 'react-redux' |
|
4 |
import { formatHtmlStringToReactDom } from '../../../utils/formatting/HtmlUtils' |
|
5 |
import { RootState } from '../../redux/store' |
|
6 |
|
|
7 |
const ProcessedTextDisplay = () => { |
|
8 |
const pathDto = useSelector( |
|
9 |
(state: RootState) => state.trackingTool.pathDto |
|
10 |
) |
|
11 |
|
|
12 |
return ( |
|
13 |
<Fragment> |
|
14 |
{pathDto && ( |
|
15 |
<Card variant="outlined" sx={{maxHeight: '50vh'}}> |
|
16 |
<CardContent> |
|
17 |
<Stack direction="column"> |
|
18 |
<Typography |
|
19 |
variant="h5" |
|
20 |
sx={{ mb: 1 }} |
|
21 |
fontWeight="600" |
|
22 |
> |
|
23 |
Processed Text |
|
24 |
</Typography> |
|
25 |
<Typography variant="body2"> |
|
26 |
{formatHtmlStringToReactDom(pathDto.text ?? '')} |
|
27 |
</Typography> |
|
28 |
</Stack> |
|
29 |
</CardContent> |
|
30 |
</Card> |
|
31 |
)} |
|
32 |
</Fragment> |
|
33 |
) |
|
34 |
} |
|
35 |
|
|
36 |
export default ProcessedTextDisplay |
frontend/src/features/TrackingTool/TrackingTool.tsx | ||
---|---|---|
13 | 13 |
import FileUpload from './Upload/FileUpload' |
14 | 14 |
import { Map } from 'leaflet' |
15 | 15 |
import { formatHtmlStringToReactDom } from '../../utils/formatting/HtmlUtils' |
16 |
import MapPath from './MapPath' |
|
16 |
import MapPath from './Map/MapPath'
|
|
17 | 17 |
import { useDispatch, useSelector } from 'react-redux' |
18 | 18 |
import { RootState } from '../redux/store' |
19 | 19 |
import { clear, consumeErr as consumeError } from './trackingToolSlice' |
... | ... | |
21 | 21 |
import ClearIcon from '@mui/icons-material/Clear' |
22 | 22 |
import GeoJsonExportButton from './Upload/GeoJsonExportButton' |
23 | 23 |
import GeoJsonImportDialog from './Upload/GeoJsonImportDialog' |
24 |
import ProcessedTextDisplay from './ProcessedText/ProcessedTextDisplay' |
|
25 |
import DraggableMarkerList from './DraggableList/DraggableMarkerList' |
|
24 | 26 |
|
25 | 27 |
// Page with tracking tool |
26 | 28 |
const TrackingTool = () => { |
... | ... | |
150 | 152 |
<MapPath idx={idx} /> |
151 | 153 |
))} |
152 | 154 |
</MapContainer> |
153 |
{pathDto && ( |
|
154 |
<Fragment> |
|
155 |
<Card variant="outlined" sx={{ mt: 2 }}> |
|
156 |
<CardContent> |
|
157 |
<Stack direction="column"> |
|
158 |
<Typography |
|
159 |
variant="h5" |
|
160 |
sx={{ mb: 1 }} |
|
161 |
fontWeight="600" |
|
162 |
> |
|
163 |
Processed Text |
|
164 |
</Typography> |
|
165 |
<Typography variant="body2"> |
|
166 |
{formatHtmlStringToReactDom( |
|
167 |
pathDto.text ?? '' |
|
168 |
)} |
|
169 |
</Typography> |
|
170 |
</Stack> |
|
171 |
</CardContent> |
|
172 |
</Card> |
|
173 |
</Fragment> |
|
174 |
)} |
|
155 |
</Grid> |
|
156 |
<Grid container sx={{ mt: 1, mb: 20 }} spacing={1}> |
|
157 |
<Grid item xs={12} md={6}> |
|
158 |
<DraggableMarkerList /> |
|
159 |
</Grid> |
|
160 |
<Grid item xs={12} md={6}> |
|
161 |
<ProcessedTextDisplay /> |
|
162 |
</Grid> |
|
175 | 163 |
</Grid> |
176 | 164 |
</Grid> |
177 | 165 |
</Fragment> |
frontend/src/features/TrackingTool/TrackingToolState.ts | ||
---|---|---|
1 | 1 |
import { LatLngTuple } from 'leaflet' |
2 | 2 |
import { PathDto } from '../../swagger/data-contracts' |
3 |
import { PathVariant } from './pathUtils' |
|
3 |
import { PathVariant } from './Map/pathUtils'
|
|
4 | 4 |
|
5 | 5 |
export default interface TrackingToolState { |
6 | 6 |
isLoading: boolean // whether the data is being loaded |
frontend/src/features/TrackingTool/Upload/GeoJsonExportButton.tsx | ||
---|---|---|
2 | 2 |
import { useEffect, useState } from 'react' |
3 | 3 |
import { useSelector } from 'react-redux' |
4 | 4 |
import { RootState } from '../../redux/store' |
5 |
import { isMapPointDisplayable, PathVariant } from '../pathUtils' |
|
5 |
import { isMapPointDisplayable, PathVariant } from '../Map/pathUtils'
|
|
6 | 6 |
import { exportAsGeoJsonString } from './GeoJsonIo' |
7 | 7 |
|
8 | 8 |
const GeoJsonExportButton = () => { |
frontend/src/features/TrackingTool/Upload/GeoJsonImportDialog.tsx | ||
---|---|---|
1 |
import { Button, Dialog, DialogContent, DialogTitle } from '@mui/material'
|
|
1 |
import { DialogContent, DialogTitle } from '@mui/material' |
|
2 | 2 |
import { useFormik } from 'formik' |
3 |
import { Fragment, useState } from 'react' |
|
4 |
import { useDispatch, useSelector } from 'react-redux' |
|
5 |
import { RootState } from '../../redux/store' |
|
3 |
import { useState } from 'react' |
|
4 |
import { useDispatch } from 'react-redux' |
|
6 | 5 |
import ButtonOpenableDialog from '../../Reusables/ButtonOpenableDialog' |
7 |
import { PathVariant } from '../pathUtils' |
|
8 | 6 |
import * as yup from 'yup' |
9 | 7 |
import { showNotification } from '../../Notification/notificationSlice' |
10 | 8 |
import SingleFileSelectionForm from '../../Reusables/SingleFileSelectionForm' |
... | ... | |
58 | 56 |
reader.readAsText(values.file as File) |
59 | 57 |
reader.onload = async () => { |
60 | 58 |
try { |
61 |
const pathVariant = parseGeoJsonToPathVariant(reader.result as string) |
|
62 |
console.log(pathVariant) |
|
59 |
const pathVariant = parseGeoJsonToPathVariant( |
|
60 |
reader.result as string |
|
61 |
) |
|
63 | 62 |
// Merge current path variant with the new one |
64 | 63 |
dispatch(mergeWithCurrentPath(pathVariant)) |
65 | 64 |
onClose() |
frontend/src/features/TrackingTool/Upload/GeoJsonIo.ts | ||
---|---|---|
1 |
import { isMapPointDisplayable, PathVariant } from '../pathUtils'
|
|
1 |
import { isMapPointDisplayable, MapPoint, MapPointType, PathVariant } from '../Map/pathUtils'
|
|
2 | 2 |
import * as yup from 'yup' |
3 |
import generateUuid from '../../../utils/id/uuidGenerator' |
|
3 | 4 |
|
4 | 5 |
export const exportAsGeoJsonString = (path: PathVariant) => JSON.stringify({ |
5 | 6 |
type: 'FeatureCollection', |
... | ... | |
28 | 29 |
}) |
29 | 30 |
|
30 | 31 |
const catalogItemValidationSchema = yup.object({ |
31 |
id: yup.string().required(),
|
|
32 |
name: yup.string().required(),
|
|
33 |
allNames: yup.array().of(yup.string()).required(),
|
|
34 |
description: yup.string(), |
|
32 |
id: yup.string().optional(),
|
|
33 |
name: yup.string().optional(),
|
|
34 |
allNames: yup.array().of(yup.string()).optional(),
|
|
35 |
description: yup.string().optional(),
|
|
35 | 36 |
latitude: yup.number().required(), |
36 | 37 |
longitude: yup.number().required(), |
37 | 38 |
}) |
... | ... | |
52 | 53 |
} |
53 | 54 |
const path: PathVariant = features.map((feature: any) => { |
54 | 55 |
const catalogItemDto = feature.properties.catalogItem |
55 |
|
|
56 |
|
|
56 | 57 |
if (!catalogItemDto) { |
57 | 58 |
throw new Error('GeoJson file does not have a valid structure') |
58 | 59 |
} |
... | ... | |
60 | 61 |
const catalogItem = catalogItemValidationSchema.validateSync(catalogItemDto) |
61 | 62 |
|
62 | 63 |
return { |
64 |
id: generateUuid(), |
|
63 | 65 |
idx: feature.properties.idx, |
64 | 66 |
active: true, |
65 | 67 |
catalogItem: { |
... | ... | |
69 | 71 |
latitude: catalogItem.latitude, |
70 | 72 |
longitude: catalogItem.longitude, |
71 | 73 |
}, |
72 |
} |
|
74 |
type: MapPointType.GeoJson, |
|
75 |
} as MapPoint |
|
73 | 76 |
}) |
74 | 77 |
return path |
75 | 78 |
} |
frontend/src/features/TrackingTool/pathUtils.ts | ||
---|---|---|
1 |
// Business logic for tracking tool |
|
2 |
|
|
3 |
import { CatalogItemDto, PathDto } from '../../swagger/data-contracts' |
|
4 |
|
|
5 |
// For more comprehensive code alias CatalogItemDto[] as path variant |
|
6 |
export type PathVariant = MapPoint[] |
|
7 |
|
|
8 |
export interface MapPoint { |
|
9 |
idx: number, |
|
10 |
active: boolean, |
|
11 |
catalogItem: CatalogItemDto, |
|
12 |
} |
|
13 |
|
|
14 |
export const isMapPointDisplayable = (mapPoint: MapPoint): boolean => |
|
15 |
!!mapPoint.catalogItem.latitude && !!mapPoint.catalogItem.longitude |
|
16 |
|
|
17 |
/** |
|
18 |
* Cartesian product of two arrays |
|
19 |
* @param sets |
|
20 |
* @returns |
|
21 |
*/ |
|
22 |
const cartesianProduct = (sets: CatalogItemDto[][]): CatalogItemDto[][] => |
|
23 |
sets.reduce<CatalogItemDto[][]>( |
|
24 |
(results, ids) => |
|
25 |
results |
|
26 |
.map((result) => ids.map((id) => [...result, id])) |
|
27 |
.reduce((nested, result) => [...nested, ...result]), |
|
28 |
[[]] |
|
29 |
) |
|
30 |
|
|
31 |
/** |
|
32 |
* Builds a list of all possible path variants from pathDto |
|
33 |
* @param pathDto |
|
34 |
* @returns |
|
35 |
*/ |
|
36 |
export const buildPathVariants = (pathDto: PathDto): PathVariant[] => { |
|
37 |
if (!pathDto.foundCatalogItems) { |
|
38 |
return [] |
|
39 |
} |
|
40 |
|
|
41 |
return ( |
|
42 |
pathDto.foundCatalogItems.length === 1 |
|
43 |
? pathDto.foundCatalogItems |
|
44 |
: cartesianProduct(pathDto.foundCatalogItems) |
|
45 |
).map((variant, _) => |
|
46 |
variant.map( |
|
47 |
(catalogItem, idx) => ( |
|
48 |
{ |
|
49 |
idx, |
|
50 |
active: !!catalogItem.latitude && !!catalogItem.longitude, |
|
51 |
catalogItem |
|
52 |
}) |
|
53 |
) |
|
54 |
) |
|
55 |
} |
|
56 |
|
|
57 |
export default buildPathVariants |
frontend/src/features/TrackingTool/trackingToolSlice.ts | ||
---|---|---|
2 | 2 |
import { LatLngTuple } from "leaflet" |
3 | 3 |
import mapConfig from "../../config/mapConfig" |
4 | 4 |
import { PathDto } from "../../swagger/data-contracts" |
5 |
import buildPathVariants, { isMapPointDisplayable, MapPoint, PathVariant } from "./pathUtils" |
|
5 |
import buildPathVariants, { isMapPointDisplayable, MapPoint, PathVariant } from "./Map/pathUtils"
|
|
6 | 6 |
import { sendTextForProcessing } from "./trackingToolThunks" |
7 | 7 |
import storage from "redux-persist/lib/storage" |
8 | 8 |
import TrackingToolState from './TrackingToolState' |
... | ... | |
60 | 60 |
...state, |
61 | 61 |
currentPage: action.payload, |
62 | 62 |
}), |
63 |
updateMapMarker: (state: TrackingToolState, action: { payload: { idx: number, item: MapPoint } }) => { |
|
64 |
const { idx, item } = action.payload |
|
63 |
// Updates map marker while ignoring its idx property |
|
64 |
updateMapMarkerWithId: (state: TrackingToolState, action: { payload: { id: string, item: MapPoint } }) => { |
|
65 |
const { item } = action.payload |
|
66 |
const idx = state.primaryPathIdx |
|
67 |
if (!state.pathVariants || state.pathVariants.length <= idx) { |
|
68 |
return state |
|
69 |
} |
|
70 |
|
|
71 |
const mapMarkerIdx = state.pathVariants[idx].findIndex((item) => item.id === action.payload.id) |
|
72 |
if (mapMarkerIdx === -1) { |
|
73 |
return state |
|
74 |
} |
|
75 |
|
|
76 |
const newPathVariant = [...state.pathVariants[idx]] |
|
77 |
newPathVariant[mapMarkerIdx] = item |
|
78 |
return { |
|
79 |
...state, |
|
80 |
pathVariants: [...state.pathVariants.slice(0, idx), newPathVariant, ...state.pathVariants.slice(idx + 1)], |
|
81 |
} |
|
82 |
}, |
|
83 |
// Updates map marker based on its idx property |
|
84 |
updateMapMarker: (state: TrackingToolState, action: { payload: { item: MapPoint } }) => { |
|
85 |
const { item } = action.payload |
|
86 |
const idx = state.primaryPathIdx |
|
65 | 87 |
if (!state.pathVariants || state.pathVariants.length <= idx) { |
66 | 88 |
return state |
67 | 89 |
} |
... | ... | |
81 | 103 |
}) |
82 | 104 |
} |
83 | 105 |
}, |
106 |
swapMapMarkerIndices: (state: TrackingToolState, action: { payload: { destination: number, source: number } }) => { |
|
107 |
const { destination, source } = action.payload |
|
108 |
if (!state.pathVariants || state.pathVariants.length === 0) { |
|
109 |
return state |
|
110 |
} |
|
111 |
|
|
112 |
return { |
|
113 |
...state, |
|
114 |
pathVariants: state.pathVariants.map((pathVariant, i) => { |
|
115 |
if (state.primaryPathIdx !== i) { |
|
116 |
return [...pathVariant] |
|
117 |
} |
|
118 |
|
|
119 |
if (pathVariant.length <= destination || pathVariant.length <= source) { |
|
120 |
return [...pathVariant] |
|
121 |
} |
|
122 |
|
|
123 |
// JS dark magic splice |
|
124 |
const result = [...pathVariant] |
|
125 |
const [removed] = result.splice(source, 1) |
|
126 |
result.splice(destination, 0, removed) |
|
127 |
return result |
|
128 |
}) |
|
129 |
} |
|
130 |
}, |
|
84 | 131 |
clear: () => ({ ...initialState }), |
85 | 132 |
mergeWithCurrentPath: (state: TrackingToolState, action: { payload: PathVariant }) => { |
86 | 133 |
const { payload: jsonPath } = action |
... | ... | |
104 | 151 |
const pathMap = new Map(path.map((item) => [item.catalogItem.id as string, item])) |
105 | 152 |
|
106 | 153 |
// Create an array of items to be replaced and items to be added to the end |
107 |
const itemsToReplace: MapPoint[] = [] |
|
154 |
// const itemsToReplace: MapPoint[] = []
|
|
108 | 155 |
const itemsToAdd: MapPoint[] = [] |
109 | 156 |
jsonPath.forEach((item) => { |
110 | 157 |
if (!pathMap.has(item.catalogItem.id as string)) { |
111 | 158 |
itemsToAdd.push(item) |
112 | 159 |
return |
113 | 160 |
} |
114 |
|
|
115 |
const idx = pathMap.get(item.catalogItem.id as string)!.idx |
|
116 |
item.idx = idx |
|
117 |
itemsToReplace.push(item) |
|
161 |
|
|
162 |
// const idx = pathMap.get(item.catalogItem.id as string)!.idx
|
|
163 |
// item.idx = idx
|
|
164 |
// itemsToReplace.push(item)
|
|
118 | 165 |
}) |
119 | 166 |
|
120 | 167 |
// Iterate over items to replace and replace them |
121 | 168 |
const newPath = [...path] |
122 |
itemsToReplace.forEach((item) => { |
|
123 |
newPath[item.idx] = item |
|
124 |
}) |
|
169 |
// itemsToReplace.forEach((item) => {
|
|
170 |
// newPath[item.idx] = item
|
|
171 |
// })
|
|
125 | 172 |
|
126 | 173 |
// Add items to the end |
127 | 174 |
itemsToAdd.forEach((item) => { |
128 |
item.active = false
|
|
175 |
item.active = !state.pathVariants || state.pathVariants.length === 0
|
|
129 | 176 |
item.idx = newPath.length |
130 | 177 |
newPath.push(item) |
131 | 178 |
}) |
... | ... | |
176 | 223 |
}, |
177 | 224 |
}) |
178 | 225 |
|
179 |
export const { consumeErr, setPrimaryIdx, resetDialogApiCallSuccess, clear, updateMapMarker, mergeWithCurrentPath } = |
|
180 |
trackingToolSlice.actions |
|
226 |
export const { |
|
227 |
consumeErr, |
|
228 |
setPrimaryIdx, |
|
229 |
resetDialogApiCallSuccess, |
|
230 |
clear, |
|
231 |
updateMapMarker, |
|
232 |
mergeWithCurrentPath, |
|
233 |
swapMapMarkerIndices, |
|
234 |
updateMapMarkerWithId |
|
235 |
} = trackingToolSlice.actions |
|
181 | 236 |
const trackingToolReducer = trackingToolSlice.reducer |
182 | 237 |
export default trackingToolReducer |
frontend/src/features/TrackingTool/trackingToolThunks.ts | ||
---|---|---|
1 | 1 |
import { createAsyncThunk } from '@reduxjs/toolkit' |
2 | 2 |
import axiosInstance from '../../api/api' |
3 | 3 |
import { RootState } from '../redux/store' |
4 |
import { MapPoint, PathVariant } from './pathUtils' |
|
4 |
import { MapPoint, PathVariant } from './Map/pathUtils'
|
|
5 | 5 |
|
6 | 6 |
export const sendTextForProcessing = createAsyncThunk( |
7 | 7 |
'trackingTool/sendTextForProcessing', |
Také k dispozici: Unified diff
draggable list start
re #9741