Revize 48690561
Přidáno uživatelem Václav Honzík před asi 2 roky(ů)
frontend/src/features/TrackingTool/Controls/DragDropCtxWrapper.tsx | ||
---|---|---|
1 |
import { memo } from 'react' |
|
2 |
import { |
|
3 |
DragDropContext, |
|
4 |
Droppable, |
|
5 |
OnDragEndResponder, |
|
6 |
} from 'react-beautiful-dnd' |
|
7 |
import { MapPoint } from '../trackingToolUtils' |
|
8 |
import MapPointDraggableListItem from './MapPointDraggableListItem' |
|
9 |
|
|
10 |
export interface DraggableListProps { |
|
11 |
items: MapPoint[] |
|
12 |
onDragEnd: OnDragEndResponder |
|
13 |
} |
|
14 |
|
|
15 |
window.addEventListener('error', (e) => { |
|
16 |
if ( |
|
17 |
e.message === |
|
18 |
'ResizeObserver loop completed with undelivered notifications.' || |
|
19 |
e.message === 'ResizeObserver loop limit exceeded' |
|
20 |
) { |
|
21 |
e.stopImmediatePropagation() |
|
22 |
} |
|
23 |
}) |
|
24 |
|
|
25 |
const DragDropCtxWrapper = memo(({ items, onDragEnd }: DraggableListProps) => { |
|
26 |
return ( |
|
27 |
<DragDropContext onDragEnd={onDragEnd}> |
|
28 |
<Droppable droppableId="droppable-list"> |
|
29 |
{(provided) => ( |
|
30 |
<div ref={provided.innerRef} {...provided.droppableProps}> |
|
31 |
{items.map((item, index) => ( |
|
32 |
<MapPointDraggableListItem |
|
33 |
list={items} |
|
34 |
idx={index} |
|
35 |
key={item.id} |
|
36 |
/> |
|
37 |
))} |
|
38 |
{provided.placeholder} |
|
39 |
</div> |
|
40 |
)} |
|
41 |
</Droppable> |
|
42 |
</DragDropContext> |
|
43 |
) |
|
44 |
}) |
|
45 |
|
|
46 |
export default DragDropCtxWrapper |
frontend/src/features/TrackingTool/Controls/MapPointDraggableList.tsx | ||
---|---|---|
1 |
import { Paper } from '@mui/material' |
|
2 |
import { useCallback } from 'react' |
|
3 |
import { DropResult } from 'react-beautiful-dnd' |
|
4 |
import { useDispatch, useSelector } from 'react-redux' |
|
5 |
import { RootState } from '../../redux/store' |
|
6 |
import { moveMarkerToDestination } from '../trackingToolSlice' |
|
7 |
import DragDropCtxWrapper from './DragDropCtxWrapper' |
|
8 |
|
|
9 |
const MapPointDraggableList = () => { |
|
10 |
const dispatch = useDispatch() |
|
11 |
const path = useSelector( |
|
12 |
(state: RootState) => state.trackingTool.displayedPath |
|
13 |
) |
|
14 |
const onDragEnd = useCallback( |
|
15 |
({ destination, source }: DropResult) => { |
|
16 |
if (!destination || !source || destination.index === source.index) { |
|
17 |
return |
|
18 |
} |
|
19 |
|
|
20 |
dispatch( |
|
21 |
moveMarkerToDestination({ |
|
22 |
source: source.index, |
|
23 |
destination: destination.index, |
|
24 |
}) |
|
25 |
) |
|
26 |
}, |
|
27 |
[dispatch] |
|
28 |
) |
|
29 |
|
|
30 |
return ( |
|
31 |
<Paper variant="outlined"> |
|
32 |
<DragDropCtxWrapper items={path ?? []} onDragEnd={onDragEnd} /> |
|
33 |
</Paper> |
|
34 |
) |
|
35 |
} |
|
36 |
|
|
37 |
export default MapPointDraggableList |
frontend/src/features/TrackingTool/Controls/MapPointDraggableListItem.tsx | ||
---|---|---|
1 |
import { |
|
2 |
IconButton, |
|
3 |
ListItem, |
|
4 |
ListItemAvatar, |
|
5 |
ListItemText, |
|
6 |
Typography, |
|
7 |
} from '@mui/material' |
|
8 |
import { Draggable } from 'react-beautiful-dnd' |
|
9 |
import { |
|
10 |
getMapPointSemanticColor, |
|
11 |
MapPointType, |
|
12 |
PathVariant, |
|
13 |
} from '../trackingToolUtils' |
|
14 |
import { CatalogItemDto } from '../../../swagger/data-contracts' |
|
15 |
import { useDispatch } from 'react-redux' |
|
16 |
import { removeMapMarker, updateMapMarker } from '../trackingToolSlice' |
|
17 |
import { useMemo } from 'react' |
|
18 |
import DragHandleIcon from '@mui/icons-material/DragHandle' |
|
19 |
import VisibilityIcon from '@mui/icons-material/Visibility' |
|
20 |
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff' |
|
21 |
import AddRoadIcon from '@mui/icons-material/AddRoad' |
|
22 |
import RemoveRoadIcon from '@mui/icons-material/RemoveRoad' |
|
23 |
import DeleteForeverIcon from '@mui/icons-material/DeleteForever' |
|
24 |
|
|
25 |
export type DraggableListItemProps = { |
|
26 |
list: PathVariant |
|
27 |
idx: number |
|
28 |
} |
|
29 |
|
|
30 |
const getFormattedLocationOrEmpty = (catalogItem: CatalogItemDto) => { |
|
31 |
if (!catalogItem || !catalogItem.latitude || !catalogItem.longitude) { |
|
32 |
return 'Location unavailable' |
|
33 |
} |
|
34 |
|
|
35 |
return `${catalogItem.latitude.toFixed( |
|
36 |
3 |
|
37 |
)}°, ${catalogItem.longitude.toFixed(3)}°` |
|
38 |
} |
|
39 |
|
|
40 |
const MapPointDraggableListItem = ({ list, idx }: DraggableListItemProps) => { |
|
41 |
const item = list[idx] |
|
42 |
const dispatch = useDispatch() |
|
43 |
|
|
44 |
// useMemo to prevent unnecessary re-renders which will make the list jumpy |
|
45 |
return useMemo(() => { |
|
46 |
const toggleAddToPath = () => { |
|
47 |
dispatch( |
|
48 |
// updateMapMarkerWithId({ |
|
49 |
// item: { |
|
50 |
// ...item, |
|
51 |
// addToPath: !item?.addToPath, |
|
52 |
// } as MapPoint, |
|
53 |
// id: item.id, |
|
54 |
// }) |
|
55 |
updateMapMarker({ |
|
56 |
...item, |
|
57 |
addToPath: !item?.addToPath, |
|
58 |
idx, |
|
59 |
}) |
|
60 |
) |
|
61 |
} |
|
62 |
|
|
63 |
const toggleHidden = () => { |
|
64 |
dispatch( |
|
65 |
// updateMapMarkerWithId({ |
|
66 |
// item: { |
|
67 |
// ...item, |
|
68 |
// hidden: !item?.hidden, |
|
69 |
// } as MapPoint, |
|
70 |
// id: item.id, |
|
71 |
// }) |
|
72 |
updateMapMarker({ |
|
73 |
...item, |
|
74 |
hidden: !item?.hidden, |
|
75 |
idx, |
|
76 |
}) |
|
77 |
) |
|
78 |
} |
|
79 |
|
|
80 |
const deleteItem = () => { |
|
81 |
dispatch( |
|
82 |
removeMapMarker({ |
|
83 |
...item, |
|
84 |
idx, |
|
85 |
}) |
|
86 |
) |
|
87 |
} |
|
88 |
|
|
89 |
return ( |
|
90 |
item && ( |
|
91 |
<Draggable |
|
92 |
key={`${item.id}`} |
|
93 |
draggableId={`${item.id}`} |
|
94 |
index={idx} |
|
95 |
> |
|
96 |
{(provided, snapshot) => ( |
|
97 |
<ListItem |
|
98 |
ref={provided.innerRef} |
|
99 |
{...provided.draggableProps} |
|
100 |
{...provided.dragHandleProps} |
|
101 |
> |
|
102 |
<ListItemAvatar> |
|
103 |
<DragHandleIcon /> |
|
104 |
</ListItemAvatar> |
|
105 |
<ListItemText |
|
106 |
primary={ |
|
107 |
<Typography |
|
108 |
style={{ |
|
109 |
color: getMapPointSemanticColor( |
|
110 |
item |
|
111 |
), |
|
112 |
}} |
|
113 |
> |
|
114 |
{item.catalogItem.name ?? |
|
115 |
'Unknown name'} |
|
116 |
</Typography> |
|
117 |
} |
|
118 |
secondary={getFormattedLocationOrEmpty( |
|
119 |
item.catalogItem |
|
120 |
)} |
|
121 |
/> |
|
122 |
{item.type !== MapPointType.LocalCatalog && ( |
|
123 |
<IconButton sx={{ mr: 1 }} onClick={deleteItem}> |
|
124 |
<DeleteForeverIcon /> |
|
125 |
</IconButton> |
|
126 |
)} |
|
127 |
<IconButton sx={{ mr: 1 }} onClick={toggleHidden}> |
|
128 |
{item.hidden ? ( |
|
129 |
<VisibilityOffIcon /> |
|
130 |
) : ( |
|
131 |
<VisibilityIcon /> |
|
132 |
)} |
|
133 |
</IconButton> |
|
134 |
<IconButton |
|
135 |
sx={{ mr: 1 }} |
|
136 |
onClick={toggleAddToPath} |
|
137 |
> |
|
138 |
{item.addToPath ? ( |
|
139 |
<AddRoadIcon /> |
|
140 |
) : ( |
|
141 |
<RemoveRoadIcon /> |
|
142 |
)} |
|
143 |
</IconButton> |
|
144 |
{/* <FormControlLabel |
|
145 |
control={ |
|
146 |
<Checkbox |
|
147 |
checked={item.addToPath} |
|
148 |
onChange={toggleAddToPath} |
|
149 |
/> |
|
150 |
} |
|
151 |
label="Add to path" |
|
152 |
/> */} |
|
153 |
</ListItem> |
|
154 |
)} |
|
155 |
</Draggable> |
|
156 |
) |
|
157 |
) |
|
158 |
}, [item, idx, dispatch]) |
|
159 |
} |
|
160 |
|
|
161 |
export default MapPointDraggableListItem |
frontend/src/features/TrackingTool/Controls/MapPointToggleables.tsx | ||
---|---|---|
1 |
import { Checkbox, FormControlLabel, Stack, Typography } from '@mui/material' |
|
2 |
import { useState } from 'react' |
|
3 |
import { useDispatch, useSelector } from 'react-redux' |
|
4 |
import { RootState } from '../../redux/store' |
|
5 |
import { updateDisplayedPath } from '../trackingToolSlice' |
|
6 |
import { MapPointType } from '../trackingToolUtils' |
|
7 |
|
|
8 |
// Component which controls what type of map points are enabled and disabled |
|
9 |
const MapPointToggleables = () => { |
|
10 |
const dispatch = useDispatch() |
|
11 |
const path = useSelector((state: RootState) => state.trackingTool.displayedPath) |
|
12 |
|
|
13 |
// keep track of the state of the checkboxes |
|
14 |
const [enableMap, setEnableMap] = useState({ |
|
15 |
[MapPointType.LocalCatalog]: true, |
|
16 |
[MapPointType.FromCoordinates]: true, |
|
17 |
[MapPointType.GeoJson]: true, |
|
18 |
[MapPointType.ExternalCatalog]: true, |
|
19 |
}) |
|
20 |
|
|
21 |
// Disables specific feature |
|
22 |
const toggleFeature = (feature: MapPointType) => { |
|
23 |
if (!path) { |
|
24 |
return |
|
25 |
} |
|
26 |
setEnableMap({ ...enableMap, [feature]: !enableMap[feature] }) |
|
27 |
const newPath = path.map((point) => { |
|
28 |
// if the point is the feature we are disabling, set it to as so, skip otherwise |
|
29 |
if (point.type === feature) { |
|
30 |
return { |
|
31 |
...point, |
|
32 |
hidden: enableMap[point.type], |
|
33 |
addToPath: !enableMap[point.type] ? true : point.addToPath, |
|
34 |
} |
|
35 |
} |
|
36 |
|
|
37 |
return point |
|
38 |
}) |
|
39 |
dispatch(updateDisplayedPath(newPath)) |
|
40 |
} |
|
41 |
|
|
42 |
return ( |
|
43 |
<Stack direction="column"> |
|
44 |
<Stack direction="row" justifyItems="space-between"> |
|
45 |
<Typography align="left" variant="h6" sx={{ mr: 1 }}> |
|
46 |
From file |
|
47 |
</Typography> |
|
48 |
<FormControlLabel |
|
49 |
control={ |
|
50 |
<Checkbox |
|
51 |
checked={enableMap[MapPointType.GeoJson]} |
|
52 |
onChange={() => toggleFeature(MapPointType.GeoJson)} |
|
53 |
/> |
|
54 |
} |
|
55 |
label="Show All" |
|
56 |
/> |
|
57 |
</Stack> |
|
58 |
<Stack direction="row" justifyItems="space-between"> |
|
59 |
<Typography align="left" variant="h6" sx={{ mr: 1 }}> |
|
60 |
From local catalog |
|
61 |
</Typography> |
|
62 |
<FormControlLabel |
|
63 |
control={ |
|
64 |
<Checkbox |
|
65 |
checked={enableMap[MapPointType.LocalCatalog]} |
|
66 |
onChange={() => |
|
67 |
toggleFeature(MapPointType.LocalCatalog) |
|
68 |
} |
|
69 |
/> |
|
70 |
} |
|
71 |
label="Show All" |
|
72 |
/> |
|
73 |
</Stack> |
|
74 |
<Stack direction="row" justifyItems="space-between"> |
|
75 |
<Typography align="left" variant="h6" sx={{ mr: 1 }}> |
|
76 |
From coordinates |
|
77 |
</Typography> |
|
78 |
<FormControlLabel |
|
79 |
control={ |
|
80 |
<Checkbox |
|
81 |
checked={enableMap[MapPointType.FromCoordinates]} |
|
82 |
onChange={() => |
|
83 |
toggleFeature(MapPointType.FromCoordinates) |
|
84 |
} |
|
85 |
/> |
|
86 |
} |
|
87 |
label="Show All" |
|
88 |
/> |
|
89 |
</Stack> |
|
90 |
<Stack direction="row" justifyItems="space-between"> |
|
91 |
<Typography align="left" variant="h6" sx={{ mr: 1 }}> |
|
92 |
From external catalog |
|
93 |
</Typography> |
|
94 |
<FormControlLabel |
|
95 |
control={ |
|
96 |
<Checkbox |
|
97 |
checked={enableMap[MapPointType.ExternalCatalog]} |
|
98 |
onChange={() => |
|
99 |
toggleFeature(MapPointType.ExternalCatalog) |
|
100 |
} |
|
101 |
/> |
|
102 |
} |
|
103 |
label="Show All" |
|
104 |
/> |
|
105 |
</Stack> |
|
106 |
</Stack> |
|
107 |
) |
|
108 |
} |
|
109 |
|
|
110 |
export default MapPointToggleables |
frontend/src/features/TrackingTool/Controls/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/Import/GeoJsonExportButton.tsx | ||
---|---|---|
1 |
import { Button } from '@mui/material' |
|
2 |
import { useSelector } from 'react-redux' |
|
3 |
import { RootState } from '../../redux/store' |
|
4 |
import { isMapPointDisplayable } from '../trackingToolUtils' |
|
5 |
import { exportAsGeoJsonString } from '../Map/geoJsonMapping' |
|
6 |
|
|
7 |
const GeoJsonExportButton = () => { |
|
8 |
const path = useSelector((state: RootState) => state.trackingTool.displayedPath) |
|
9 |
const exportPath = () => { |
|
10 |
if (!path) { |
|
11 |
return |
|
12 |
} |
|
13 |
|
|
14 |
const exportPath = path.filter( |
|
15 |
(vertex) => isMapPointDisplayable(vertex) && vertex.addToPath |
|
16 |
) |
|
17 |
const exportPathString = exportAsGeoJsonString(exportPath) |
|
18 |
const blob = new Blob([exportPathString], { type: 'application/json' }) |
|
19 |
const url = window.URL.createObjectURL(blob) |
|
20 |
const link = document.createElement('a') |
|
21 |
link.href = url |
|
22 |
link.setAttribute('download', 'path.json') |
|
23 |
document.body.appendChild(link) |
|
24 |
link.click() |
|
25 |
document.body.removeChild(link) |
|
26 |
} |
|
27 |
|
|
28 |
return ( |
|
29 |
<Button variant="contained" onClick={exportPath}> |
|
30 |
Export |
|
31 |
</Button> |
|
32 |
) |
|
33 |
} |
|
34 |
|
|
35 |
export default GeoJsonExportButton |
frontend/src/features/TrackingTool/Import/GeoJsonImportDialog.tsx | ||
---|---|---|
1 |
import { DialogContent, DialogTitle } from '@mui/material' |
|
2 |
import { useFormik } from 'formik' |
|
3 |
import { useState } from 'react' |
|
4 |
import { useDispatch } from 'react-redux' |
|
5 |
import ButtonOpenableDialog from '../../Reusables/ButtonOpenableDialog' |
|
6 |
import * as yup from 'yup' |
|
7 |
import { showNotification } from '../../Notification/notificationSlice' |
|
8 |
import SingleFileSelectionForm from '../../Reusables/SingleFileSelectionForm' |
|
9 |
import { mergeWithCurrentPath } from '../trackingToolSlice' |
|
10 |
import { parseGeoJsonToPathVariant } from '../Map/geoJsonMapping' |
|
11 |
|
|
12 |
const GeoJsonImportDialog = () => { |
|
13 |
const dispatch = useDispatch() |
|
14 |
|
|
15 |
const [filename, setFilename] = useState<string | undefined>(undefined) |
|
16 |
const [fileProcessing, setFileProcessing] = useState(false) |
|
17 |
const [open, setOpen] = useState(false) |
|
18 |
|
|
19 |
const validationSchema = yup.object().shape({ |
|
20 |
file: yup.mixed().required('File is required'), |
|
21 |
}) |
|
22 |
|
|
23 |
const initialValues: { file?: File } = { |
|
24 |
file: undefined, |
|
25 |
} |
|
26 |
|
|
27 |
// Callback when user selects the file |
|
28 |
const onFileSelected = (event: any) => { |
|
29 |
const file = event.currentTarget.files[0] |
|
30 |
if (file) { |
|
31 |
setFilename(file.name) |
|
32 |
formik.setFieldValue('file', file) |
|
33 |
} |
|
34 |
} |
|
35 |
|
|
36 |
const onClose = () => { |
|
37 |
if (fileProcessing) { |
|
38 |
return |
|
39 |
} |
|
40 |
setFilename(undefined) |
|
41 |
formik.resetForm() |
|
42 |
setOpen(false) |
|
43 |
} |
|
44 |
|
|
45 |
const onClearSelectedFile = () => { |
|
46 |
setFilename(undefined) |
|
47 |
formik.setFieldValue('file', undefined) |
|
48 |
} |
|
49 |
|
|
50 |
const formik = useFormik({ |
|
51 |
initialValues, |
|
52 |
validationSchema, |
|
53 |
onSubmit: async (values) => { |
|
54 |
setFileProcessing(true) |
|
55 |
const reader = new FileReader() |
|
56 |
reader.readAsText(values.file as File) |
|
57 |
reader.onload = async () => { |
|
58 |
try { |
|
59 |
const pathVariant = parseGeoJsonToPathVariant( |
|
60 |
reader.result as string |
|
61 |
) |
|
62 |
// Merge current path variant with the new one |
|
63 |
dispatch(mergeWithCurrentPath(pathVariant)) |
|
64 |
onClose() |
|
65 |
} catch (e: any) { |
|
66 |
dispatch( |
|
67 |
showNotification({ |
|
68 |
message: e.message, |
|
69 |
// message: 'Error importing GeoJson, the file has invalid format', |
|
70 |
severity: 'error', |
|
71 |
autohideSecs: 5, |
|
72 |
}) |
|
73 |
) |
|
74 |
} |
|
75 |
setFileProcessing(false) |
|
76 |
} |
|
77 |
}, |
|
78 |
}) |
|
79 |
|
|
80 |
return ( |
|
81 |
<ButtonOpenableDialog |
|
82 |
buttonText="Import" |
|
83 |
buttonColor="primary" |
|
84 |
buttonVariant="contained" |
|
85 |
onCloseCallback={onClose} |
|
86 |
maxWidth="xs" |
|
87 |
open={open} |
|
88 |
setOpen={setOpen} |
|
89 |
> |
|
90 |
<DialogTitle>Import Path</DialogTitle> |
|
91 |
<DialogContent> |
|
92 |
<SingleFileSelectionForm |
|
93 |
onFileSelected={onFileSelected} |
|
94 |
onClearSelectedFile={onClearSelectedFile} |
|
95 |
filename={filename} |
|
96 |
formik={formik} |
|
97 |
/> |
|
98 |
</DialogContent> |
|
99 |
</ButtonOpenableDialog> |
|
100 |
) |
|
101 |
} |
|
102 |
|
|
103 |
export default GeoJsonImportDialog |
frontend/src/features/TrackingTool/Import/ImportContextMenu.tsx | ||
---|---|---|
2 | 2 |
import { Fragment, useCallback, useState } from 'react' |
3 | 3 |
import { Popup, useMapEvents } from 'react-leaflet' |
4 | 4 |
import Typography from '@mui/material/Typography' |
5 |
import { Stack } from '@mui/material'
|
|
5 |
import { Stack } from '@mui/material' |
|
6 | 6 |
import AddFromCoordinatesDialog from './AddFromCoordinatesDialog' |
7 | 7 |
import { useSelector } from 'react-redux' |
8 | 8 |
import { RootState } from '../../redux/store' |
9 | 9 |
import ImportLocationDialog from './ImportLocationDialog' |
10 |
import { PathDto } from '../../../swagger/data-contracts' |
|
11 | 10 |
|
12 | 11 |
const RightClickPopupMenu = () => { |
13 | 12 |
const [open, setOpen] = useState(false) |
14 | 13 |
const [latLng, setLatLng] = useState<[number, number]>([0, 0]) |
15 |
const pathDto = useSelector((state: RootState) => state.trackingTool.pathDto) |
|
14 |
const path = useSelector( |
|
15 |
(state: RootState) => state.trackingTool.displayedPath |
|
16 |
) |
|
16 | 17 |
|
17 | 18 |
useMapEvents({ |
18 | 19 |
contextmenu: (e: LeafletMouseEvent) => { |
19 |
if (!pathDto) {
|
|
20 |
if (!path) { |
|
20 | 21 |
return |
21 | 22 |
} |
22 | 23 |
setLatLng([e.latlng.lat, e.latlng.lng]) |
... | ... | |
38 | 39 |
justifyItems="center" |
39 | 40 |
justifyContent="center" |
40 | 41 |
> |
41 |
<Typography style={{margin: 0}} sx={{ mb: 0.5 }} align="center"> |
|
42 |
<Typography |
|
43 |
style={{ margin: 0 }} |
|
44 |
sx={{ mb: 0.5 }} |
|
45 |
align="center" |
|
46 |
> |
|
42 | 47 |
{latLng[0].toFixed(5)}°{latLng[1].toFixed(5)}° |
43 | 48 |
</Typography> |
44 | 49 |
<AddFromCoordinatesDialog |
frontend/src/features/TrackingTool/Import/MapPointToggleables.tsx | ||
---|---|---|
1 |
import { Checkbox, FormControlLabel, Stack, Typography } from '@mui/material' |
|
2 |
import { useEffect, useState } from 'react' |
|
3 |
import { useDispatch, useSelector } from 'react-redux' |
|
4 |
import { RootState } from '../../redux/store' |
|
5 |
import { updatePrimaryPath } from '../trackingToolSlice' |
|
6 |
import { MapPointType, PathVariant } from '../trackingToolUtils' |
|
7 |
|
|
8 |
const MapPointToggleables = () => { |
|
9 |
const dispatch = useDispatch() |
|
10 |
const paths = useSelector( |
|
11 |
(state: RootState) => state.trackingTool.pathVariants |
|
12 |
) |
|
13 |
const [path, setPath] = useState<PathVariant>([]) |
|
14 |
const primaryPathIdx = useSelector( |
|
15 |
(state: RootState) => state.trackingTool.primaryPathIdx |
|
16 |
) |
|
17 |
useEffect(() => { |
|
18 |
setPath( |
|
19 |
paths && paths.length > primaryPathIdx ? paths[primaryPathIdx] : [] |
|
20 |
) |
|
21 |
}, [paths, primaryPathIdx]) |
|
22 |
|
|
23 |
const [enableMap, setEnableMap] = useState({ |
|
24 |
[MapPointType.LocalCatalog]: true, |
|
25 |
[MapPointType.FromCoordinates]: true, |
|
26 |
[MapPointType.GeoJson]: true, |
|
27 |
[MapPointType.ExternalCatalog]: true, |
|
28 |
}) |
|
29 |
|
|
30 |
// Disables specific feature |
|
31 |
const toggleFeature = (feature: MapPointType) => { |
|
32 |
setEnableMap({ ...enableMap, [feature]: !enableMap[feature] }) |
|
33 |
const newPath = path.map((point) => { |
|
34 |
if (point.type === feature) { |
|
35 |
return { |
|
36 |
...point, |
|
37 |
hidden: enableMap[point.type], |
|
38 |
addToPath: !enableMap[point.type] ? true : point.addToPath, |
|
39 |
} |
|
40 |
} |
|
41 |
|
|
42 |
return point |
|
43 |
}) |
|
44 |
dispatch(updatePrimaryPath(newPath)) |
|
45 |
} |
|
46 |
|
|
47 |
return ( |
|
48 |
<Stack direction="column"> |
|
49 |
<Stack direction="row" justifyItems="space-between"> |
|
50 |
<Typography align="left" variant="h6" sx={{ mr: 1 }}> |
|
51 |
From file |
|
52 |
</Typography> |
|
53 |
<FormControlLabel |
|
54 |
control={ |
|
55 |
<Checkbox |
|
56 |
checked={enableMap[MapPointType.GeoJson]} |
|
57 |
onChange={() => toggleFeature(MapPointType.GeoJson)} |
|
58 |
/> |
|
59 |
} |
|
60 |
label="Show All" |
|
61 |
/> |
|
62 |
</Stack> |
|
63 |
<Stack direction="row" justifyItems="space-between"> |
|
64 |
<Typography align="left" variant="h6" sx={{ mr: 1 }}> |
|
65 |
From local catalog |
|
66 |
</Typography> |
|
67 |
<FormControlLabel |
|
68 |
control={ |
|
69 |
<Checkbox |
|
70 |
checked={enableMap[MapPointType.LocalCatalog]} |
|
71 |
onChange={() => |
|
72 |
toggleFeature(MapPointType.LocalCatalog) |
|
73 |
} |
|
74 |
/> |
|
75 |
} |
|
76 |
label="Show All" |
|
77 |
/> |
|
78 |
</Stack> |
|
79 |
<Stack direction="row" justifyItems="space-between"> |
|
80 |
<Typography align="left" variant="h6" sx={{ mr: 1 }}> |
|
81 |
From coordinates |
|
82 |
</Typography> |
|
83 |
<FormControlLabel |
|
84 |
control={ |
|
85 |
<Checkbox |
|
86 |
checked={enableMap[MapPointType.FromCoordinates]} |
|
87 |
onChange={() => |
|
88 |
toggleFeature(MapPointType.FromCoordinates) |
|
89 |
} |
|
90 |
/> |
|
91 |
} |
|
92 |
label="Show All" |
|
93 |
/> |
|
94 |
</Stack> |
|
95 |
<Stack direction="row" justifyItems="space-between"> |
|
96 |
<Typography align="left" variant="h6" sx={{ mr: 1 }}> |
|
97 |
From external catalog |
|
98 |
</Typography> |
|
99 |
<FormControlLabel |
|
100 |
control={ |
|
101 |
<Checkbox |
|
102 |
checked={enableMap[MapPointType.ExternalCatalog]} |
|
103 |
onChange={() => |
|
104 |
toggleFeature(MapPointType.ExternalCatalog) |
|
105 |
} |
|
106 |
/> |
|
107 |
} |
|
108 |
label="Show All" |
|
109 |
/> |
|
110 |
</Stack> |
|
111 |
</Stack> |
|
112 |
) |
|
113 |
} |
|
114 |
|
|
115 |
export default MapPointToggleables |
frontend/src/features/TrackingTool/Map/Map.tsx | ||
---|---|---|
1 |
import { ThemeProvider } from '@mui/material' |
|
2 |
import { useRef, useEffect } from 'react' |
|
3 |
import { MapContainer, TileLayer } from 'react-leaflet' |
|
4 |
import { useSelector } from 'react-redux' |
|
5 |
import mapConfig from '../../../config/mapConfig' |
|
6 |
import { RootState } from '../../redux/store' |
|
7 |
import { buildTheme } from '../../Theme/ThemeWrapper' |
|
8 |
import RightClickPopupMenu from '../Import/ImportContextMenu' |
|
9 |
import MapPath from './MapPath' |
|
10 |
import { Map as LeafletMap } from 'leaflet' |
|
11 |
|
|
12 |
const mapTheme = buildTheme('light') |
|
13 |
|
|
14 |
const Map = () => { |
|
15 |
|
|
16 |
const mapCenter = useSelector((state: RootState) => state.trackingTool.mapCenter) |
|
17 |
|
|
18 |
const mapRef = useRef<LeafletMap | undefined>(undefined) |
|
19 |
useEffect(() => { |
|
20 |
if (!mapRef || !mapRef.current) { |
|
21 |
console.log('No map ref') |
|
22 |
return |
|
23 |
} |
|
24 |
|
|
25 |
const map = mapRef.current |
|
26 |
map.setView(mapCenter, mapConfig.defaultZoom, { |
|
27 |
animate: true, |
|
28 |
}) |
|
29 |
}, [mapCenter, mapRef]) |
|
30 |
|
|
31 |
return ( |
|
32 |
<ThemeProvider theme={mapTheme}> |
|
33 |
<MapContainer |
|
34 |
center={[mapCenter[0], mapCenter[1]]} |
|
35 |
zoom={mapConfig.defaultZoom} |
|
36 |
style={{ height: '100%', minHeight: '100%' }} |
|
37 |
whenCreated={(map) => { |
|
38 |
mapRef.current = map |
|
39 |
}} |
|
40 |
> |
|
41 |
<TileLayer |
|
42 |
attribution={mapConfig.attribution} |
|
43 |
url={mapConfig.url} |
|
44 |
/> |
|
45 |
<MapPath /> |
|
46 |
<RightClickPopupMenu /> |
|
47 |
</MapContainer> |
|
48 |
</ThemeProvider> |
|
49 |
) |
|
50 |
} |
|
51 |
|
|
52 |
export default Map |
frontend/src/features/TrackingTool/Map/MapPath.tsx | ||
---|---|---|
1 |
import { Fragment, FunctionComponent, useEffect, useState } from 'react'
|
|
1 |
import { Fragment, useEffect, useState } from 'react' |
|
2 | 2 |
import { useDispatch, useSelector } from 'react-redux' |
3 | 3 |
import { RootState } from '../../redux/store' |
4 |
import { PathVariant, MapPoint, isMapPointDisplayable } from '../trackingToolUtils'
|
|
4 |
import { MapPoint, isMapPointDisplayable } from '../trackingToolUtils' |
|
5 | 5 |
import TextPath from 'react-leaflet-textpath' |
6 |
import { |
|
7 |
setPrimaryIdx, |
|
8 |
updateMapMarker, |
|
9 |
updateMapMarkerWithId, |
|
10 |
} from '../trackingToolSlice' |
|
6 |
import { updateMapMarker } from '../trackingToolSlice' |
|
11 | 7 |
import MapMarker from './MapMarker' |
12 | 8 |
import { LatLngTuple } from 'leaflet' |
13 | 9 |
import { Popup, Tooltip } from 'react-leaflet' |
... | ... | |
15 | 11 |
import { formatHtmlStringToReactDom } from '../../../utils/formatting/HtmlUtils' |
16 | 12 |
import { DialogCatalogItemDetail as CatalogItemDetailDialog } from '../../Catalog/CatalogItemDetail' |
17 | 13 |
|
18 |
export interface MapPathProps { |
|
19 |
idx: number // index of the path in the list |
|
20 |
} |
|
21 |
|
|
22 | 14 |
type EdgeElement = any |
23 |
|
|
24 |
// Blue |
|
25 |
export const primaryPathColor = '#346eeb' |
|
26 |
|
|
27 |
// Grey |
|
28 |
export const secondaryPathColor = '#878e9c' |
|
29 |
|
|
30 |
const MapPath: FunctionComponent<MapPathProps> = ({ idx }) => { |
|
15 |
const MapPath = () => { |
|
31 | 16 |
const dispatch = useDispatch() |
32 |
|
|
33 |
// Get list of all paths from the store |
|
34 |
// And extract path from them |
|
35 |
const paths = useSelector( |
|
36 |
(state: RootState) => state.trackingTool.pathVariants |
|
37 |
) |
|
38 |
const [path, setPath] = useState<PathVariant>([]) |
|
39 |
useEffect(() => { |
|
40 |
// Either set the path if it exists or set it to an empty array |
|
41 |
setPath(paths && paths.length > idx ? paths[idx] : []) |
|
42 |
}, [idx, paths]) |
|
43 |
|
|
44 |
// Primary path index to set the correct color |
|
45 |
const primaryPathIdx = useSelector( |
|
46 |
(state: RootState) => state.trackingTool.primaryPathIdx |
|
17 |
const path = useSelector( |
|
18 |
(state: RootState) => state.trackingTool.displayedPath |
|
47 | 19 |
) |
20 |
// Color of the path |
|
21 |
const pathColor = '#346eeb' |
|
48 | 22 |
|
49 | 23 |
// List of all active map points |
50 | 24 |
const [displayableMapPoints, setDisplayableMapPoints] = useState< |
51 | 25 |
MapPoint[] |
52 | 26 |
>([]) |
53 | 27 |
useEffect(() => { |
28 |
if (!path) { |
|
29 |
setDisplayableMapPoints([]) |
|
30 |
return |
|
31 |
} |
|
32 |
|
|
54 | 33 |
// Set all displayable vertices |
55 | 34 |
setDisplayableMapPoints( |
56 | 35 |
path.filter((mapPoint) => isMapPointDisplayable(mapPoint)) |
... | ... | |
78 | 57 |
] |
79 | 58 |
edges.push( |
80 | 59 |
<TextPath |
81 |
// Somehow this refuses to work so let it rerender everything ... |
|
82 | 60 |
key={`${activeMapPoints[i].id}-${ |
83 | 61 |
activeMapPoints[i + 1].id |
84 | 62 |
}`} |
... | ... | |
87 | 65 |
[end.latitude, end.longitude], |
88 | 66 |
]} |
89 | 67 |
text="►" |
90 |
// text=" > > > > " |
|
91 | 68 |
attributes={{ |
92 | 69 |
'font-size': 19, |
93 |
// Set to primaryPathColor if primary index in the tracking tool is equal to this index |
|
94 |
fill: |
|
95 |
primaryPathIdx === idx |
|
96 |
? primaryPathColor |
|
97 |
: secondaryPathColor, |
|
70 |
fill: pathColor, |
|
98 | 71 |
}} |
99 |
onClick={() => dispatch(setPrimaryIdx(idx))} |
|
100 | 72 |
repeat |
101 | 73 |
center |
102 | 74 |
weight={0} |
... | ... | |
104 | 76 |
) |
105 | 77 |
} |
106 | 78 |
setEdges(edges) |
107 |
}, [dispatch, displayableMapPoints, idx, primaryPathIdx])
|
|
79 |
}, [dispatch, displayableMapPoints]) |
|
108 | 80 |
|
109 | 81 |
// List of vertices to display |
110 | 82 |
const [vertices, setVertices] = useState<JSX.Element[]>([]) |
... | ... | |
121 | 93 |
mapPoint={item} |
122 | 94 |
updatePositionCallbackFn={(position: LatLngTuple) => { |
123 | 95 |
dispatch( |
124 |
updateMapMarkerWithId({ |
|
125 |
item: { |
|
126 |
...item, |
|
127 |
catalogItem: { |
|
128 |
...item.catalogItem, |
|
129 |
latitude: position[0], |
|
130 |
longitude: position[1], |
|
131 |
}, |
|
96 |
updateMapMarker({ |
|
97 |
...item, |
|
98 |
catalogItem: { |
|
99 |
...item.catalogItem, |
|
100 |
latitude: position[0], |
|
101 |
longitude: position[1], |
|
132 | 102 |
}, |
133 |
id: item.id, |
|
134 | 103 |
}) |
135 | 104 |
) |
136 | 105 |
}} |
... | ... | |
161 | 130 |
dispatch( |
162 | 131 |
updateMapMarker({ |
163 | 132 |
...item, |
164 |
addToPath: !item.addToPath, |
|
133 |
addToPath: |
|
134 |
!item.addToPath, |
|
165 | 135 |
}) |
166 | 136 |
) |
167 | 137 |
}} |
... | ... | |
180 | 150 |
</MapMarker> |
181 | 151 |
)) |
182 | 152 |
) |
183 |
}, [dispatch, displayableMapPoints, idx])
|
|
153 |
}, [dispatch, displayableMapPoints]) |
|
184 | 154 |
|
185 | 155 |
return ( |
186 | 156 |
<Fragment> |
frontend/src/features/TrackingTool/Map/geoJsonMapping.ts | ||
---|---|---|
1 |
import { isMapPointDisplayable, MapPoint, MapPointType, PathVariant } from '../trackingToolUtils' |
|
2 |
import * as yup from 'yup' |
|
3 |
import generateUuid from '../../../utils/id/uuidGenerator' |
|
4 |
|
|
5 |
export const exportAsGeoJsonString = (path: PathVariant) => JSON.stringify({ |
|
6 |
type: 'FeatureCollection', |
|
7 |
features: path.filter(item => item.addToPath && isMapPointDisplayable(item)).map((item) => { |
|
8 |
const catalogItem = item.catalogItem |
|
9 |
return { |
|
10 |
type: 'Feature', |
|
11 |
properties: { |
|
12 |
catalogItem: { |
|
13 |
id: catalogItem.id, |
|
14 |
name: catalogItem.name, |
|
15 |
allNames: catalogItem.allNames, |
|
16 |
description: catalogItem.description, |
|
17 |
latitude: catalogItem.latitude, |
|
18 |
longitude: catalogItem.longitude, |
|
19 |
}, |
|
20 |
idx: item.idx, |
|
21 |
displayable: isMapPointDisplayable(item), |
|
22 |
}, |
|
23 |
geometry: { |
|
24 |
type: 'Point', |
|
25 |
coordinates: [catalogItem.longitude, catalogItem.latitude], |
|
26 |
}, |
|
27 |
} |
|
28 |
}), |
|
29 |
}) |
|
30 |
|
|
31 |
const catalogItemValidationSchema = yup.object({ |
|
32 |
id: yup.string().optional(), |
|
33 |
name: yup.string().optional(), |
|
34 |
allNames: yup.array().of(yup.string()).optional(), |
|
35 |
description: yup.string().optional(), |
|
36 |
latitude: yup.number().required(), |
|
37 |
longitude: yup.number().required(), |
|
38 |
}) |
|
39 |
|
|
40 |
/** |
|
41 |
* Parses a GeoJson string and returns a list of MapPoints |
|
42 |
* @param geoJson loaded file |
|
43 |
* @returns |
|
44 |
*/ |
|
45 |
export const parseGeoJsonToPathVariant = (geoJson: string) => { |
|
46 |
const parsed = JSON.parse(geoJson) |
|
47 |
if (parsed.type !== 'FeatureCollection') { |
|
48 |
throw new Error('Invalid GeoJson') |
|
49 |
} |
|
50 |
const features = parsed.features |
|
51 |
if (!features) { |
|
52 |
throw new Error('Invalid GeoJson provided') |
|
53 |
} |
|
54 |
const path: PathVariant = features.map((feature: any) => { |
|
55 |
const catalogItemDto = feature.properties.catalogItem |
|
56 |
|
|
57 |
if (!catalogItemDto) { |
|
58 |
throw new Error('GeoJson file does not have a valid structure') |
|
59 |
} |
|
60 |
// validate catalog item |
|
61 |
const catalogItem = catalogItemValidationSchema.validateSync(catalogItemDto) |
|
62 |
|
|
63 |
return { |
|
64 |
id: generateUuid(), |
|
65 |
idx: feature.properties.idx, |
|
66 |
addToPath: true, |
|
67 |
catalogItem: { |
|
68 |
id: catalogItem.id, |
|
69 |
name: catalogItem.name, |
|
70 |
description: catalogItem.description, |
|
71 |
latitude: catalogItem.latitude, |
|
72 |
longitude: catalogItem.longitude, |
|
73 |
}, |
|
74 |
type: MapPointType.GeoJson, |
|
75 |
} as MapPoint |
|
76 |
}) |
|
77 |
return path |
|
78 |
} |
|
79 |
|
frontend/src/features/TrackingTool/Map/mapUtils.ts | ||
---|---|---|
1 |
import { CatalogItemDto, PathDto } from '../../../swagger/data-contracts' |
|
2 |
import generateUuid from '../../../utils/id/uuidGenerator' |
|
3 |
import { MapPoint, MapPointType, PathVariant } from '../trackingToolUtils' |
|
4 |
|
|
5 |
|
|
6 |
/** |
|
7 |
* Cartesian product of two arrays |
|
8 |
* @param sets |
|
9 |
* @returns |
|
10 |
*/ |
|
11 |
const cartesianProduct = (sets: CatalogItemDto[][]): CatalogItemDto[][] => |
|
12 |
sets.reduce<CatalogItemDto[][]>( |
|
13 |
(results, ids) => |
|
14 |
results |
|
15 |
.map((result) => ids.map((id) => [...result, id])) |
|
16 |
.reduce((nested, result) => [...nested, ...result]), |
|
17 |
[[]] |
|
18 |
) |
|
19 |
|
|
20 |
/** |
|
21 |
* Builds a list of all possible path variants from pathDto |
|
22 |
* @param pathDto |
|
23 |
* @returns |
|
24 |
*/ |
|
25 |
export const buildPathVariants = (pathDto: PathDto, mapPointType: MapPointType = MapPointType.LocalCatalog): PathVariant[] => { |
|
26 |
if (!pathDto.foundCatalogItems) { |
|
27 |
return [] |
|
28 |
} |
|
29 |
|
|
30 |
return ( |
|
31 |
pathDto.foundCatalogItems.length === 1 |
|
32 |
? pathDto.foundCatalogItems |
|
33 |
: cartesianProduct(pathDto.foundCatalogItems) |
|
34 |
).map((variant, _) => |
|
35 |
variant.map( |
|
36 |
(catalogItem, idx) => ( |
|
37 |
{ |
|
38 |
id: generateUuid(), |
|
39 |
idx, |
|
40 |
addToPath: !!catalogItem.latitude && !!catalogItem.longitude, |
|
41 |
catalogItem, |
|
42 |
type: mapPointType, |
|
43 |
} as MapPoint) |
|
44 |
) |
|
45 |
) |
|
46 |
} |
|
47 |
|
|
48 |
export default buildPathVariants |
frontend/src/features/TrackingTool/Map/pathUtils.ts | ||
---|---|---|
1 |
import { CatalogItemDto, PathDto } from '../../../swagger/data-contracts' |
|
2 |
import generateUuid from '../../../utils/id/uuidGenerator' |
|
3 |
import { MapPoint, MapPointType, PathVariant } from '../trackingToolUtils' |
|
4 |
|
|
5 |
|
|
6 |
/** |
|
7 |
* Cartesian product of two arrays |
|
8 |
* @param sets |
|
9 |
* @returns |
|
10 |
*/ |
|
11 |
const cartesianProduct = (sets: CatalogItemDto[][]): CatalogItemDto[][] => |
|
12 |
sets.reduce<CatalogItemDto[][]>( |
|
13 |
(results, ids) => |
|
14 |
results |
|
15 |
.map((result) => ids.map((id) => [...result, id])) |
|
16 |
.reduce((nested, result) => [...nested, ...result]), |
|
17 |
[[]] |
|
18 |
) |
|
19 |
|
|
20 |
/** |
|
21 |
* Builds a list of all possible path variants from pathDto |
|
22 |
* @param pathDto |
|
23 |
* @returns |
|
24 |
*/ |
|
25 |
export const buildPathVariants = (pathDto: PathDto, mapPointType: MapPointType = MapPointType.LocalCatalog): PathVariant[] => { |
|
26 |
if (!pathDto.foundCatalogItems) { |
|
27 |
return [] |
|
28 |
} |
|
29 |
|
|
30 |
return ( |
|
31 |
pathDto.foundCatalogItems.length === 1 |
|
32 |
? pathDto.foundCatalogItems |
|
33 |
: cartesianProduct(pathDto.foundCatalogItems) |
|
34 |
).map((variant, _) => |
|
35 |
variant.map( |
|
36 |
(catalogItem, idx) => ( |
|
37 |
{ |
|
38 |
id: generateUuid(), |
|
39 |
idx, |
|
40 |
addToPath: !!catalogItem.latitude && !!catalogItem.longitude, |
|
41 |
catalogItem, |
|
42 |
type: mapPointType, |
|
43 |
} as MapPoint) |
|
44 |
) |
|
45 |
) |
|
46 |
} |
|
47 |
|
|
48 |
export default buildPathVariants |
frontend/src/features/TrackingTool/MapPointDraggableList/DragDropCtxWrapper.tsx | ||
---|---|---|
1 |
import { memo } from 'react' |
|
2 |
import { |
|
3 |
DragDropContext, |
|
4 |
Droppable, |
|
5 |
OnDragEndResponder, |
|
6 |
} from 'react-beautiful-dnd' |
|
7 |
import { MapPoint } from '../trackingToolUtils' |
|
8 |
import MapPointDraggableListItem from './MapPointDraggableListItem' |
|
9 |
|
|
10 |
export interface DraggableListProps { |
|
11 |
items: MapPoint[] |
|
12 |
onDragEnd: OnDragEndResponder |
|
13 |
} |
|
14 |
|
|
15 |
window.addEventListener('error', (e) => { |
|
16 |
if ( |
|
17 |
e.message === |
|
18 |
'ResizeObserver loop completed with undelivered notifications.' || |
|
19 |
e.message === 'ResizeObserver loop limit exceeded' |
|
20 |
) { |
|
21 |
e.stopImmediatePropagation() |
|
22 |
} |
|
23 |
}) |
|
24 |
|
|
25 |
const DragDropCtxWrapper = memo(({ items, onDragEnd }: DraggableListProps) => { |
|
26 |
return ( |
|
27 |
<DragDropContext onDragEnd={onDragEnd}> |
|
28 |
<Droppable droppableId="droppable-list"> |
|
29 |
{(provided) => ( |
|
30 |
<div ref={provided.innerRef} {...provided.droppableProps}> |
|
31 |
{items.map((item, index) => ( |
|
32 |
<MapPointDraggableListItem |
|
33 |
list={items} |
|
34 |
idx={index} |
|
35 |
key={item.id} |
|
36 |
/> |
|
37 |
))} |
|
38 |
{provided.placeholder} |
|
39 |
</div> |
|
40 |
)} |
|
41 |
</Droppable> |
|
42 |
</DragDropContext> |
|
43 |
) |
|
44 |
}) |
|
45 |
|
|
46 |
export default DragDropCtxWrapper |
frontend/src/features/TrackingTool/MapPointDraggableList/MapPointDraggableList.tsx | ||
---|---|---|
1 |
import { Paper } from '@mui/material' |
|
2 |
import { useEffect, useState, useCallback } from 'react' |
|
3 |
import { DropResult } from 'react-beautiful-dnd' |
|
4 |
import { useDispatch, useSelector } from 'react-redux' |
|
5 |
import { RootState } from '../../redux/store' |
|
6 |
import { moveMarkerToDestination } from '../trackingToolSlice' |
|
7 |
import { PathVariant } from '../trackingToolUtils' |
|
8 |
import DragDropCtxWrapper from './DragDropCtxWrapper' |
|
9 |
|
|
10 |
const MapPointDraggableList = () => { |
|
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 = useCallback(({ destination, source }: DropResult) => { |
|
37 |
if (!destination || !source || destination.index === source.index) { |
|
38 |
return |
|
39 |
} |
|
40 |
|
|
41 |
dispatch( |
|
42 |
moveMarkerToDestination({ |
|
43 |
source: source.index, |
|
44 |
destination: destination.index, |
|
45 |
}) |
|
46 |
) |
|
47 |
}, [dispatch]) |
|
48 |
|
|
49 |
return ( |
|
50 |
<Paper variant="outlined"> |
|
51 |
<DragDropCtxWrapper items={path ?? []} onDragEnd={onDragEnd} /> |
|
52 |
</Paper> |
|
53 |
) |
|
54 |
} |
|
55 |
|
|
56 |
export default MapPointDraggableList |
frontend/src/features/TrackingTool/MapPointDraggableList/MapPointDraggableListItem.tsx | ||
---|---|---|
1 |
import { |
|
2 |
IconButton, |
|
3 |
ListItem, |
|
4 |
ListItemAvatar, |
|
5 |
ListItemText, |
|
6 |
Typography, |
|
7 |
} from '@mui/material' |
|
8 |
import { Draggable } from 'react-beautiful-dnd' |
|
9 |
import { |
|
10 |
getMapPointSemanticColor, |
|
11 |
MapPointType, |
|
12 |
PathVariant, |
|
13 |
} from '../trackingToolUtils' |
|
14 |
import { CatalogItemDto } from '../../../swagger/data-contracts' |
|
15 |
import { useDispatch } from 'react-redux' |
|
16 |
import { removeMapMarker, updateMapMarker } from '../trackingToolSlice' |
|
17 |
import { useMemo } from 'react' |
|
18 |
import DragHandleIcon from '@mui/icons-material/DragHandle' |
|
19 |
import VisibilityIcon from '@mui/icons-material/Visibility' |
|
20 |
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff' |
|
21 |
import AddRoadIcon from '@mui/icons-material/AddRoad' |
|
22 |
import RemoveRoadIcon from '@mui/icons-material/RemoveRoad' |
|
23 |
import DeleteForeverIcon from '@mui/icons-material/DeleteForever' |
|
24 |
|
|
25 |
export type DraggableListItemProps = { |
|
26 |
list: PathVariant |
|
27 |
idx: number |
|
28 |
} |
|
29 |
|
|
30 |
const getFormattedLocationOrEmpty = (catalogItem: CatalogItemDto) => { |
|
31 |
if (!catalogItem || !catalogItem.latitude || !catalogItem.longitude) { |
|
32 |
return 'Location unavailable' |
|
33 |
} |
|
34 |
|
|
35 |
return `${catalogItem.latitude.toFixed( |
|
36 |
3 |
|
37 |
)}°, ${catalogItem.longitude.toFixed(3)}°` |
|
38 |
} |
|
39 |
|
|
40 |
const MapPointDraggableListItem = ({ list, idx }: DraggableListItemProps) => { |
|
41 |
const item = list[idx] |
|
42 |
const dispatch = useDispatch() |
|
43 |
|
|
44 |
// useMemo to prevent unnecessary re-renders which will make the list jumpy |
|
45 |
return useMemo(() => { |
|
46 |
const toggleAddToPath = () => { |
|
47 |
dispatch( |
|
48 |
// updateMapMarkerWithId({ |
|
49 |
// item: { |
|
50 |
// ...item, |
|
51 |
// addToPath: !item?.addToPath, |
|
52 |
// } as MapPoint, |
|
53 |
// id: item.id, |
|
54 |
// }) |
|
55 |
updateMapMarker({ |
|
56 |
...item, |
|
57 |
addToPath: !item?.addToPath, |
|
58 |
idx, |
|
59 |
}) |
|
60 |
) |
|
61 |
} |
|
62 |
|
|
63 |
const toggleHidden = () => { |
|
64 |
dispatch( |
|
65 |
// updateMapMarkerWithId({ |
|
66 |
// item: { |
|
67 |
// ...item, |
|
68 |
// hidden: !item?.hidden, |
|
69 |
// } as MapPoint, |
|
70 |
// id: item.id, |
|
71 |
// }) |
|
72 |
updateMapMarker({ |
|
73 |
...item, |
|
74 |
hidden: !item?.hidden, |
|
75 |
idx, |
|
76 |
}) |
|
77 |
) |
|
78 |
} |
|
79 |
|
|
80 |
const deleteItem = () => { |
|
81 |
dispatch( |
|
82 |
removeMapMarker({ |
|
83 |
...item, |
|
84 |
idx, |
|
85 |
}) |
|
86 |
) |
|
87 |
} |
|
88 |
|
|
89 |
return ( |
|
90 |
item && ( |
|
91 |
<Draggable |
|
92 |
key={`${item.id}`} |
|
93 |
draggableId={`${item.id}`} |
|
94 |
index={idx} |
|
95 |
> |
|
96 |
{(provided, snapshot) => ( |
|
97 |
<ListItem |
|
98 |
ref={provided.innerRef} |
|
99 |
{...provided.draggableProps} |
|
100 |
{...provided.dragHandleProps} |
|
101 |
> |
|
102 |
<ListItemAvatar> |
|
103 |
<DragHandleIcon /> |
|
104 |
</ListItemAvatar> |
|
105 |
<ListItemText |
|
106 |
primary={ |
|
107 |
<Typography |
|
108 |
style={{ |
|
109 |
color: getMapPointSemanticColor( |
|
110 |
item |
|
111 |
), |
|
112 |
}} |
|
113 |
> |
|
114 |
{item.catalogItem.name ?? |
|
115 |
'Unknown name'} |
|
116 |
</Typography> |
|
117 |
} |
|
118 |
secondary={getFormattedLocationOrEmpty( |
|
119 |
item.catalogItem |
|
120 |
)} |
|
121 |
/> |
|
122 |
{item.type !== MapPointType.LocalCatalog && ( |
|
123 |
<IconButton sx={{ mr: 1 }} onClick={deleteItem}> |
|
124 |
<DeleteForeverIcon /> |
|
125 |
</IconButton> |
|
126 |
)} |
|
127 |
<IconButton sx={{ mr: 1 }} onClick={toggleHidden}> |
|
128 |
{item.hidden ? ( |
|
129 |
<VisibilityOffIcon /> |
|
130 |
) : ( |
|
131 |
<VisibilityIcon /> |
|
132 |
)} |
|
133 |
</IconButton> |
|
134 |
<IconButton |
|
135 |
sx={{ mr: 1 }} |
|
136 |
onClick={toggleAddToPath} |
|
137 |
> |
|
138 |
{item.addToPath ? ( |
|
139 |
<AddRoadIcon /> |
|
140 |
) : ( |
|
141 |
<RemoveRoadIcon /> |
|
142 |
)} |
|
143 |
</IconButton> |
|
144 |
{/* <FormControlLabel |
|
145 |
control={ |
|
146 |
<Checkbox |
|
147 |
checked={item.addToPath} |
|
148 |
onChange={toggleAddToPath} |
|
149 |
/> |
|
150 |
} |
|
151 |
label="Add to path" |
|
152 |
/> */} |
|
153 |
</ListItem> |
|
154 |
)} |
|
155 |
</Draggable> |
|
156 |
) |
|
157 |
) |
|
158 |
}, [item, idx, dispatch]) |
|
159 |
} |
|
160 |
|
|
161 |
export default MapPointDraggableListItem |
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/ProcessedTextCard.tsx | ||
---|---|---|
1 |
import { PathDto } from "../../swagger/data-contracts" |
|
2 |
|
|
3 |
export interface ProcessedtextCardProps { |
|
4 |
pathDto?: PathDto |
|
5 |
|
|
6 |
} |
|
7 |
|
|
8 |
const ProcessedTextCard = () => { |
|
9 |
|
|
10 |
} |
|
11 |
|
|
12 |
export default ProcessedTextCard |
frontend/src/features/TrackingTool/TrackingTool.tsx | ||
---|---|---|
1 |
import { |
|
2 |
Button, |
|
3 |
Card, |
|
4 |
CardContent, |
|
5 |
Grid, |
|
6 |
Stack, |
|
7 |
ThemeProvider, |
|
8 |
Typography, |
|
9 |
} from '@mui/material' |
|
10 |
import { Fragment, useEffect, useRef } from 'react' |
Také k dispozici: Unified diff
smol refactor
re #9741