Projekt

Obecné

Profil

« Předchozí | Další » 

Revize 48690561

Přidáno uživatelem Václav Honzík před asi 2 roky(ů)

smol refactor

re #9741

Zobrazit rozdíly:

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'
... Rozdílový soubor je zkrácen, protože jeho délka přesahuje max. limit.

Také k dispozici: Unified diff