Projekt

Obecné

Profil

« Předchozí | Další » 

Revize c0b66eaf

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

map icon colors start

re #9741

Zobrazit rozdíly:

frontend/src/features/Reusables/ButtonOpenableDialog.tsx
10 10
    buttonVariant: 'text' | 'outlined' | 'contained' // the variant of the button that opens the dialog
11 11
    children: ReactNode // the content of the dialog
12 12
    maxWidth?: 'xs' | 'sm' | 'md' | 'lg' // the max width of the dialog
13
    open: boolean,
13
    open: boolean
14 14
    setOpen: (open: boolean) => void
15 15
    size?: 'small' | 'medium' | 'large'
16
    startIcon?: ReactNode // the icon to the left of the button
17
    endIcon?: ReactNode // the icon to the right of the button
16 18
}
17 19

  
18

  
19 20
// Generic dialog that can be opened by a button and closed by clicking on the backdrop.
20 21
const ButtonOpenableDialog: FunctionComponent<ButtonOpenableDialogProps> = ({
21 22
    onOpenCallback,
......
27 28
    maxWidth,
28 29
    open,
29 30
    setOpen,
30
    size
31
    size,
32
    startIcon,
33
    endIcon,
31 34
}) => {
32

  
33 35
    // Change maxWidth to large if its undefined
34 36
    maxWidth = maxWidth ?? 'lg'
35 37

  
......
55 57
                color={buttonColor}
56 58
                variant={buttonVariant}
57 59
                size={size ?? 'medium'}
60
                startIcon={startIcon}
61
                endIcon={endIcon}
58 62
            >
59 63
                {buttonText}
60 64
            </Button>
61
            <Dialog
62
                fullWidth
63
                open={open}
64
                onClose={onClose}
65
                maxWidth={maxWidth}
66
            >
65
            <Dialog fullWidth open={open} onClose={onClose} maxWidth={maxWidth}>
67 66
                {children}
68 67
            </Dialog>
69 68
        </Fragment>
frontend/src/features/TrackingTool/DraggableList/DraggableList.tsx
1
import { memo } from 'react'
2
import {
3
    DragDropContext,
4
    Droppable,
5
    OnDragEndResponder,
6
} from 'react-beautiful-dnd'
7
import { MapPoint } from '../Map/pathUtils'
8
import DraggableListItem from './DraggableListItem'
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 DraggableList = 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
                            <DraggableListItem
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 DraggableList
frontend/src/features/TrackingTool/DraggableList/DraggableListItem.tsx
1
import {
2
    Checkbox,
3
    FormControlLabel,
4
    IconButton,
5
    ListItem,
6
    ListItemAvatar,
7
    ListItemText,
8
} from '@mui/material'
9
import { Draggable } from 'react-beautiful-dnd'
10
import { MapPoint, PathVariant } from '../Map/pathUtils'
11
import { CatalogItemDto } from '../../../swagger/data-contracts'
12
import { useDispatch } from 'react-redux'
13
import { updateMapMarkerWithId } from '../trackingToolSlice'
14
import { useMemo } from 'react'
15
import DragHandleIcon from '@mui/icons-material/DragHandle'
16
import VisibilityIcon from '@mui/icons-material/Visibility'
17
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'
18
import AddRoadIcon from '@mui/icons-material/AddRoad'
19
import RemoveRoadIcon from '@mui/icons-material/RemoveRoad'
20

  
21
export type DraggableListItemProps = {
22
    list: PathVariant
23
    idx: number
24
}
25

  
26
const getFormattedLocationOrEmpty = (catalogItem: CatalogItemDto) => {
27
    if (!catalogItem || !catalogItem.latitude || !catalogItem.longitude) {
28
        return 'Location unavailable'
29
    }
30

  
31
    return `${catalogItem.latitude.toFixed(
32
        3
33
    )}°, ${catalogItem.longitude.toFixed(3)}°`
34
}
35

  
36
const DraggableListItem = ({ list, idx }: DraggableListItemProps) => {
37
    const item = list[idx]
38
    const dispatch = useDispatch()
39

  
40
    // useMemo to prevent unnecessary re-renders which will make the list jumpy
41
    return useMemo(() => {
42
        const toggleAddToPath = () => {
43
            dispatch(
44
                updateMapMarkerWithId({
45
                    item: {
46
                        ...item,
47
                        addToPath: !item?.addToPath,
48
                    } as MapPoint,
49
                    id: item.id,
50
                })
51
            )
52
        }
53

  
54
        const toggleHidden = () => {
55
            dispatch(
56
                updateMapMarkerWithId({
57
                    item: {
58
                        ...item,
59
                        hidden: !item?.hidden,
60
                    } as MapPoint,
61
                    id: item.id,
62
                })
63
            )
64
        }
65

  
66
        return (
67
            <Draggable
68
                key={`${item.id}`}
69
                draggableId={`${item.id}`}
70
                index={idx}
71
            >
72
                {(provided, snapshot) => (
73
                    <ListItem
74
                        ref={provided.innerRef}
75
                        {...provided.draggableProps}
76
                        {...provided.dragHandleProps}
77
                    >
78
                        <ListItemAvatar>
79
                            <DragHandleIcon />
80
                        </ListItemAvatar>
81
                        <ListItemText
82
                            primary={item.catalogItem.name ?? 'Unknown name'}
83
                            secondary={getFormattedLocationOrEmpty(
84
                                item.catalogItem
85
                            )}
86
                        />
87
                        <IconButton sx={{ mr: 1 }} onClick={toggleHidden}>
88
                            {item.hidden ? (
89
                                <VisibilityOffIcon />
90
                            ) : (
91
                                <VisibilityIcon />
92
                            )}
93
                        </IconButton>
94
                        <IconButton sx={{ mr: 1 }} onClick={toggleAddToPath}>
95
                            {item.addToPath ? (
96
                                <AddRoadIcon />
97
                            ) : (
98
                                <RemoveRoadIcon />
99
                            )}
100
                        </IconButton>
101
                        {/* <FormControlLabel
102
                            control={
103
                                <Checkbox
104
                                    checked={item.addToPath}
105
                                    onChange={toggleAddToPath}
106
                                />
107
                            }
108
                            label="Add to path"
109
                        /> */}
110
                    </ListItem>
111
                )}
112
            </Draggable>
113
        )
114
    }, [item, idx, dispatch])
115
}
116

  
117
export default DraggableListItem
frontend/src/features/TrackingTool/DraggableList/DraggableMarkerList.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 '../Map/pathUtils'
8
import DraggableList from './DraggableList'
9

  
10
const DraggableMarkerList = () => {
11
    const dispatch = useDispatch()
12

  
13
    // List of all paths
14
    const paths = useSelector(
15
        (state: RootState) => state.trackingTool.pathVariants
16
    )
17

  
18
    // Primary path index - i.e. the selected path
19
    const primaryPathIdx = useSelector(
20
        (state: RootState) => state.trackingTool.primaryPathIdx
21
    )
22

  
23
    // Selected path as local state
24
    const [path, setPath] = useState<PathVariant | undefined>()
25

  
26
    // Set localstate path whenever it changes in the store
27
    useEffect(() => {
28
        if (!paths || paths.length < primaryPathIdx) {
29
            setPath(undefined)
30
            return
31
        }
32

  
33
        setPath(paths[primaryPathIdx])
34
    }, [paths, primaryPathIdx])
35

  
36
    const onDragEnd = 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
            <DraggableList items={path ?? []} onDragEnd={onDragEnd} />
52
        </Paper>
53
    )
54
}
55

  
56
export default DraggableMarkerList
frontend/src/features/TrackingTool/Import/AddFromCoordinatesDialog.tsx
14 14
import * as yup from 'yup'
15 15
import { useDispatch } from 'react-redux'
16 16
import { mergeWithCurrentPath } from '../trackingToolSlice'
17
import { MapPointType, PathVariant } from '../Map/pathUtils'
17
import { MapPointType, PathVariant } from '../trackingToolUtils'
18 18
import generateUuid from '../../../utils/id/uuidGenerator'
19 19
import { useFormik } from 'formik'
20 20
import ContextMenuDialogProps from './contextMenuDialogProps'
......
46 46
            name: yup.string().required('Name is required'),
47 47
        }),
48 48
        onSubmit: (values: AddCatalogItemFromCoords) => {
49
            dispatch(
50
                mergeWithCurrentPath([
51
                    {
52
                        id: generateUuid(),
53
                        idx: -1,
54
                        addToPath: false,
55
                        type: MapPointType.FromCoordinates,
56
                        catalogItem: {
57
                            name: values.name,
58
                            latitude: values.latitude,
59
                            longitude: values.longitude,
60
                        },
61
                    },
62
                ] as PathVariant)
63
            )
49
            const item = {
50
                id: generateUuid(),
51
                idx: -1,
52
                addToPath: false,
53
                type: MapPointType.FromCoordinates,
54
                catalogItem: {
55
                    name: values.name,
56
                    latitude: values.latitude,
57
                    longitude: values.longitude,
58
                },
59
            }
60
            console.log(`Creating item: ${JSON.stringify(item)}`)
61
            dispatch(mergeWithCurrentPath([item] as PathVariant))
62
            onClose()
64 63
        },
65 64
    })
66 65

  
......
133 132
                                    md={3}
134 133
                                    justifyContent="flex-end"
135 134
                                >
136
                                    <Button variant="contained" type="submit" color="primary">
135
                                    <Button
136
                                        variant="contained"
137
                                        type="submit"
138
                                        color="primary"
139
                                    >
137 140
                                        Import
138 141
                                    </Button>
139 142
                                </Grid>
frontend/src/features/TrackingTool/Import/ImportContextMenu.tsx
1 1
import { LeafletMouseEvent } from 'leaflet'
2 2
import { Fragment, useCallback, useState } from 'react'
3
import { Popup, useMap, useMapEvents } from 'react-leaflet'
4

  
5
import * as React from 'react'
6
import Divider from '@mui/material/Divider'
7
import Paper from '@mui/material/Paper'
8
import MenuList from '@mui/material/MenuList'
9
import MenuItem from '@mui/material/MenuItem'
10
import ListItemText from '@mui/material/ListItemText'
11
import ListItemIcon from '@mui/material/ListItemIcon'
3
import { Popup, useMapEvents } from 'react-leaflet'
12 4
import Typography from '@mui/material/Typography'
13
import ContentCut from '@mui/icons-material/ContentCut'
14
import ContentCopy from '@mui/icons-material/ContentCopy'
15
import ContentPaste from '@mui/icons-material/ContentPaste'
16
import Cloud from '@mui/icons-material/Cloud'
17
import { Button, Stack, ThemeProvider } from '@mui/material'
5
import {  Stack } from '@mui/material'
18 6
import AddFromCoordinatesDialog from './AddFromCoordinatesDialog'
19 7
import { useSelector } from 'react-redux'
20 8
import { RootState } from '../../redux/store'
21
import { buildTheme } from '../../Theme/ThemeWrapper'
22 9
import ImportLocationDialog from './ImportLocationDialog'
10
import { PathDto } from '../../../swagger/data-contracts'
23 11

  
24 12
const RightClickPopupMenu = () => {
25 13
    const [open, setOpen] = useState(false)
26 14
    const [latLng, setLatLng] = useState<[number, number]>([0, 0])
27

  
28
    const paletteMode = useSelector(
29
        (state: RootState) => state.theme.paletteMode
30
    )
15
    const pathDto = useSelector((state: RootState) => state.trackingTool.pathDto)
31 16

  
32 17
    useMapEvents({
33 18
        contextmenu: (e: LeafletMouseEvent) => {
19
            if (!pathDto) {
20
                return
21
            }
34 22
            setLatLng([e.latlng.lat, e.latlng.lng])
35 23
            setOpen(true)
36 24
        },
frontend/src/features/TrackingTool/Import/ImportLocationDialog.tsx
25 25
    websiteCatalogs: 'Website Catalogs',
26 26
}
27 27

  
28

  
29 28
const externalCatalogs = {
30
    'PLEIADES': 'Pleiades',
31
    'GEONAMES': 'Geonames',
32
    'CIGS': 'CIGS',
33
    'ANE': 'ANE'
29
    PLEIADES: 'Pleiades',
30
    GEONAMES: 'Geonames',
31
    CIGS: 'CIGS',
32
    ANE: 'ANE',
34 33
}
35 34

  
36 35
interface ImportData {
......
56 55
            if (importType === 'localCatalog') {
57 56
                values.externalCatalogType = undefined
58 57
            }
59

  
60

  
61 58
        },
62 59
    })
63 60

  
......
98 95
            <ThemeWrapper>
99 96
                <Paper>
100 97
                    <DialogTitle>
101
                    <Stack
98
                        <Stack
102 99
                            direction="row"
103 100
                            justifyContent="space-between"
104 101
                            alignItems="center"
105 102
                            spacing={1}
106 103
                        >
107
                            <Fragment>
108
                                Import Locations
109
                            </Fragment>
104
                            <Fragment>Import Locations</Fragment>
110 105
                            <IconButton onClick={onClose}>
111 106
                                <CloseIcon />
112 107
                            </IconButton>
......
153 148
                                md={3}
154 149
                                justifyContent="flex-end"
155 150
                            >
156
                                <Button variant="contained" type="submit" color="primary">
151
                                <Button
152
                                    variant="contained"
153
                                    type="submit"
154
                                    color="primary"
155
                                >
157 156
                                    Import
158 157
                                </Button>
159 158
                            </Grid>
......
174 173
                                            name="externalCatalogType"
175 174
                                            value={
176 175
                                                formik.values
177
                                                    .externalCatalogType || "PLEIADES"
176
                                                    .externalCatalogType ||
177
                                                'PLEIADES'
178 178
                                            }
179 179
                                            onChange={formik.handleChange}
180 180
                                            label="Catalog Type"
181 181
                                            size="small"
182 182
                                        >
183
                                            {Object.entries(externalCatalogs).map(
184
                                                ([key, value], idx) => (
185
                                                    <MenuItem
186
                                                        key={key}
187
                                                        value={key}
188
                                                    >
189
                                                        {value}
190
                                                    </MenuItem>
191
                                                )
192
                                            )}
183
                                            {Object.entries(
184
                                                externalCatalogs
185
                                            ).map(([key, value], idx) => (
186
                                                <MenuItem key={key} value={key}>
187
                                                    {value}
188
                                                </MenuItem>
189
                                            ))}
193 190
                                        </Select>
194 191
                                    </FormControl>
195 192
                                    <NameTextField />
frontend/src/features/TrackingTool/Map/MapMarker.tsx
1
import { LatLngTuple, Marker as MarkerPOJO } from 'leaflet'
1
import L, { LatLngTuple, Marker as MarkerPOJO } from 'leaflet'
2 2
import { FunctionComponent, ReactNode, useMemo, useRef, useState } from 'react'
3 3
import { Marker } from 'react-leaflet'
4
import {
5
    getMapPointIcon,
6
    MapPoint,
7
} from '../trackingToolUtils'
4 8

  
5 9
export interface MapMarkerProps {
6 10
    position: LatLngTuple
7 11
    children?: ReactNode
12
    mapPoint: MapPoint
8 13
    color?: 'external' | 'disabled' | 'localCatalog'
9 14
    updatePositionCallbackFn: (position: LatLngTuple) => void // Callback function to notify MapPath to rerender the path
10 15
}
......
14 19
    position,
15 20
    children,
16 21
    updatePositionCallbackFn,
22
    mapPoint,
17 23
}) => {
18 24
    const [currentPosition, setCurrentPosition] = useState(position)
19 25
    const markerRef = useRef<MarkerPOJO | null>(null)
......
37 43
        <Marker
38 44
            draggable={true}
39 45
            position={currentPosition}
46
            icon={getMapPointIcon(mapPoint)}
40 47
            eventHandlers={eventHandlers}
41 48
            ref={markerRef}
42 49
        >
frontend/src/features/TrackingTool/Map/MapPath.tsx
1 1
import { Fragment, FunctionComponent, 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 './pathUtils'
4
import { PathVariant, MapPoint, isMapPointDisplayable } from '../trackingToolUtils'
5 5
import TextPath from 'react-leaflet-textpath'
6 6
import {
7 7
    setPrimaryIdx,
......
113 113
        setVertices(
114 114
            displayableMapPoints.map((item) => (
115 115
                <MapMarker
116
                    key={`${item.catalogItem.id}`}
116
                    key={item.id}
117 117
                    position={[
118 118
                        item.catalogItem.latitude as number,
119 119
                        item.catalogItem.longitude as number,
120 120
                    ]}
121
                    mapPoint={item}
121 122
                    updatePositionCallbackFn={(position: LatLngTuple) => {
122 123
                        dispatch(
123 124
                            updateMapMarkerWithId({
frontend/src/features/TrackingTool/Map/pathUtils.ts
1
// Business logic for tracking tool
2

  
3 1
import { CatalogItemDto, PathDto } from '../../../swagger/data-contracts'
4 2
import generateUuid from '../../../utils/id/uuidGenerator'
3
import { MapPoint, MapPointType, PathVariant } from '../trackingToolUtils'
5 4

  
6
// For more comprehensive code alias CatalogItemDto[] as path variant
7
export type PathVariant = MapPoint[]
8

  
9
export enum MapPointType {
10
    LocalCatalog, // Fetched from local catalog
11
    ExternalCatalog, // Fetched from external catalog
12
    GeoJson, // From GeoJSON file
13
    FromCoordinates, // From coordinates
14
}
15

  
16
// Represents a point on the map - wrapper for CatalogItemDto to make it easier to work with
17
export interface MapPoint {
18
    id: string // unique id for react
19
    idx: number,
20
    addToPath: boolean, // whether to add the point to the path
21
    catalogItem: CatalogItemDto,
22
    type: MapPointType
23
    hidden?: boolean // if true the point will not be displayed on the map
24
}
25

  
26
export const isMapPointDisplayable = (mapPoint: MapPoint): boolean =>
27
    !!mapPoint.catalogItem.latitude && !!mapPoint.catalogItem.longitude && !mapPoint.hidden
28 5

  
29 6
/**
30 7
 * Cartesian product of two arrays
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 { getMapPointSemanticColor as getMapPointSemanticColor, MapPoint, MapPointType, PathVariant } from '../trackingToolUtils'
10
import { CatalogItemDto } from '../../../swagger/data-contracts'
11
import { useDispatch } from 'react-redux'
12
import { removeMapMarker, updateMapMarker } from '../trackingToolSlice'
13
import { useMemo } from 'react'
14
import DragHandleIcon from '@mui/icons-material/DragHandle'
15
import VisibilityIcon from '@mui/icons-material/Visibility'
16
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'
17
import AddRoadIcon from '@mui/icons-material/AddRoad'
18
import RemoveRoadIcon from '@mui/icons-material/RemoveRoad'
19
import DeleteForeverIcon from '@mui/icons-material/DeleteForever'
20

  
21
export type DraggableListItemProps = {
22
    list: PathVariant
23
    idx: number
24
}
25

  
26
const getFormattedLocationOrEmpty = (catalogItem: CatalogItemDto) => {
27
    if (!catalogItem || !catalogItem.latitude || !catalogItem.longitude) {
28
        return 'Location unavailable'
29
    }
30

  
31
    return `${catalogItem.latitude.toFixed(
32
        3
33
    )}°, ${catalogItem.longitude.toFixed(3)}°`
34
}
35

  
36
const MapPointDraggableListItem = ({ list, idx }: DraggableListItemProps) => {
37
    const item = list[idx]
38
    const dispatch = useDispatch()
39

  
40
    // useMemo to prevent unnecessary re-renders which will make the list jumpy
41
    return useMemo(() => {
42
        const toggleAddToPath = () => {
43
            dispatch(
44
                // updateMapMarkerWithId({
45
                //     item: {
46
                //         ...item,
47
                //         addToPath: !item?.addToPath,
48
                //     } as MapPoint,
49
                //     id: item.id,
50
                // })
51
                updateMapMarker({
52
                    ...item,
53
                    addToPath: !item?.addToPath,
54
                    idx,
55
                })
56
            )
57
        }
58

  
59
        const toggleHidden = () => {
60
            dispatch(
61
                // updateMapMarkerWithId({
62
                //     item: {
63
                //         ...item,
64
                //         hidden: !item?.hidden,
65
                //     } as MapPoint,
66
                //     id: item.id,
67
                // })
68
                updateMapMarker({
69
                    ...item,
70
                    hidden: !item?.hidden,
71
                    idx,
72
                })
73
            )
74
        }
75

  
76
        const deleteItem = () => {
77
            dispatch(
78
                removeMapMarker({
79
                    ...item,
80
                    idx,
81
                })
82
            )
83
        }
84

  
85
        return (
86
            item && (
87
                <Draggable
88
                    key={`${item.id}`}
89
                    draggableId={`${item.id}`}
90
                    index={idx}
91
                >
92
                    {(provided, snapshot) => (
93
                        <ListItem
94
                            ref={provided.innerRef}
95
                            {...provided.draggableProps}
96
                            {...provided.dragHandleProps}
97
                        >
98
                            <ListItemAvatar>
99
                                <DragHandleIcon />
100
                            </ListItemAvatar>
101
                            <ListItemText
102
                                primary={
103
                                    <Typography
104
                                        style={{ color: getMapPointSemanticColor(item) }}
105
                                    >
106
                                        {item.catalogItem.name ??
107
                                            'Unknown name'}
108
                                    </Typography>
109
                                }
110
                                secondary={getFormattedLocationOrEmpty(
111
                                    item.catalogItem
112
                                )}
113
                            />
114
                            {item.type !== MapPointType.LocalCatalog && (
115
                                <IconButton sx={{ mr: 1 }} onClick={deleteItem}>
116
                                    <DeleteForeverIcon />
117
                                </IconButton>
118
                            )}
119
                            <IconButton sx={{ mr: 1 }} onClick={toggleHidden}>
120
                                {item.hidden ? (
121
                                    <VisibilityOffIcon />
122
                                ) : (
123
                                    <VisibilityIcon />
124
                                )}
125
                            </IconButton>
126
                            <IconButton
127
                                sx={{ mr: 1 }}
128
                                onClick={toggleAddToPath}
129
                            >
130
                                {item.addToPath ? (
131
                                    <AddRoadIcon />
132
                                ) : (
133
                                    <RemoveRoadIcon />
134
                                )}
135
                            </IconButton>
136
                            {/* <FormControlLabel
137
                            control={
138
                                <Checkbox
139
                                    checked={item.addToPath}
140
                                    onChange={toggleAddToPath}
141
                                />
142
                            }
143
                            label="Add to path"
144
                        /> */}
145
                        </ListItem>
146
                    )}
147
                </Draggable>
148
            )
149
        )
150
    }, [item, idx, dispatch])
151
}
152

  
153
export default MapPointDraggableListItem
frontend/src/features/TrackingTool/TrackingTool.tsx
23 23
import GeoJsonExportButton from './Upload/GeoJsonExportButton'
24 24
import GeoJsonImportDialog from './Upload/GeoJsonImportDialog'
25 25
import ProcessedTextDisplay from './ProcessedText/ProcessedTextDisplay'
26
import DraggableMarkerList from './DraggableList/DraggableMarkerList'
26
import MapPointDraggableList from './MapPointDraggableList/MapPointDraggableList'
27 27
import RightClickPopupMenu from './Import/ImportContextMenu'
28 28
import { buildTheme, getPalette } from '../Theme/ThemeWrapper'
29
import AttachFileIcon from '@mui/icons-material/AttachFile'
29 30

  
30 31
const mapTheme = buildTheme('light')
31 32

  
......
111 112
                                <FileUpload />
112 113
                            </Fragment>
113 114
                        )}
114
                        <GeoJsonImportDialog />
115 115
                        {pathVariants && pathVariants.length > 0 && (
116
                            <GeoJsonExportButton />
116
                            <Fragment>
117
                                <GeoJsonImportDialog />
118
                                <GeoJsonExportButton />
119
                            </Fragment>
117 120
                        )}
118 121
                    </Stack>
119 122

  
......
163 166
                </Grid>
164 167
                <Grid container sx={{ mt: 1, mb: 20 }} spacing={1}>
165 168
                    <Grid item xs={12} md={6}>
166
                        <DraggableMarkerList />
169
                        <MapPointDraggableList />
167 170
                    </Grid>
168 171
                    <Grid item xs={12} md={6}>
169 172
                        <ProcessedTextDisplay />
frontend/src/features/TrackingTool/TrackingToolState.ts
1 1
import { LatLngTuple } from 'leaflet'
2 2
import { PathDto } from '../../swagger/data-contracts'
3
import { PathVariant } from './Map/pathUtils'
3
import { PathVariant } from './trackingToolUtils'
4 4

  
5 5
export default interface TrackingToolState {
6 6
    isLoading: boolean // whether the data is being loaded
frontend/src/features/TrackingTool/Upload/FileUpload.tsx
14 14
import { useDispatch } from 'react-redux'
15 15
import { sendTextForProcessing } from '../trackingToolThunks'
16 16
import SingleFileSelectionForm from '../../Reusables/SingleFileSelectionForm'
17
import AttachFileIcon from '@mui/icons-material/AttachFile'
18
import TextSnippetIcon from '@mui/icons-material/TextSnippet'
17 19

  
18 20
interface UploadValues {
19 21
    file?: File
......
77 79
            maxWidth="xs"
78 80
            open={open}
79 81
            setOpen={setOpen}
82
            startIcon={<AttachFileIcon />}
80 83
        >
81 84
            <DialogTitle>Upload New File</DialogTitle>
82 85
            <DialogContent>
frontend/src/features/TrackingTool/Upload/GeoJsonExportButton.tsx
2 2
import { useEffect, useState } from 'react'
3 3
import { useSelector } from 'react-redux'
4 4
import { RootState } from '../../redux/store'
5
import { isMapPointDisplayable, PathVariant } from '../Map/pathUtils'
6
import { exportAsGeoJsonString } from './GeoJsonIo'
5
import { isMapPointDisplayable, PathVariant } from '../trackingToolUtils'
6
import { exportAsGeoJsonString } from './geoJsonIo'
7 7

  
8 8
const GeoJsonExportButton = () => {
9 9
    const [path, setPath] = useState<PathVariant | undefined>(undefined)
frontend/src/features/TrackingTool/Upload/GeoJsonImportDialog.tsx
7 7
import { showNotification } from '../../Notification/notificationSlice'
8 8
import SingleFileSelectionForm from '../../Reusables/SingleFileSelectionForm'
9 9
import { mergeWithCurrentPath } from '../trackingToolSlice'
10
import { parseGeoJsonToPathVariant } from './GeoJsonIo'
10
import { parseGeoJsonToPathVariant } from './geoJsonIo'
11 11

  
12 12
const GeoJsonImportDialog = () => {
13 13
    const dispatch = useDispatch()
frontend/src/features/TrackingTool/Upload/GeoJsonIo.ts
1
import { isMapPointDisplayable, MapPoint, MapPointType, PathVariant } from '../Map/pathUtils'
1
import { isMapPointDisplayable, MapPoint, MapPointType, PathVariant } from '../trackingToolUtils'
2 2
import * as yup from 'yup'
3 3
import generateUuid from '../../../utils/id/uuidGenerator'
4 4

  
frontend/src/features/TrackingTool/Upload/PlaintextUpload.tsx
1 1
import {
2 2
    Button,
3
    Dialog,
4 3
    DialogContent,
5 4
    DialogTitle,
6 5
    Stack,
......
15 14
import { sendTextForProcessing } from '../trackingToolThunks'
16 15
import * as yup from 'yup'
17 16
import { resetDialogApiCallSuccess } from '../trackingToolSlice'
17
import ButtonOpenableDialog from '../../Reusables/ButtonOpenableDialog'
18
import TextSnippetIcon from '@mui/icons-material/TextSnippet'
18 19

  
19 20
const PlaintextUpload = () => {
20 21
    const loading = useSelector(
......
33 34
        text: yup.mixed().required('Text is required'),
34 35
    })
35 36

  
37
    // Form control
36 38
    const formik = useFormik({
37 39
        initialValues: {
38 40
            text: '',
......
63 65

  
64 66
    return (
65 67
        <Fragment>
66
            <Button variant="contained" onClick={() => setOpen(true)}>
67
                Text
68
            </Button>
69
            <Dialog
68
            <ButtonOpenableDialog
70 69
                open={open}
71
                fullWidth={true}
72
                onClose={onCloseDialog}
70
                setOpen={setOpen}
73 71
                maxWidth="lg"
72
                buttonColor="primary"
73
                buttonVariant="contained"
74
                buttonText="Text"
75
                startIcon={<TextSnippetIcon />}
76
                onCloseCallback={onCloseDialog}
74 77
            >
75 78
                <DialogTitle>Plaintext Input</DialogTitle>
76 79
                <DialogContent>
......
110 113
                        </Stack>
111 114
                    </form>
112 115
                </DialogContent>
113
            </Dialog>
116
            </ButtonOpenableDialog>
114 117
        </Fragment>
115 118
    )
116 119
}
frontend/src/features/TrackingTool/trackingToolSlice.ts
2 2
import { LatLngTuple } from "leaflet"
3 3
import mapConfig from "../../config/mapConfig"
4 4
import { PathDto } from "../../swagger/data-contracts"
5
import buildPathVariants, { isMapPointDisplayable, MapPoint, PathVariant } from "./Map/pathUtils"
5
import buildPathVariants from "./Map/pathUtils"
6
import  { isMapPointDisplayable, MapPoint, PathVariant } from "./trackingToolUtils"
6 7
import { sendTextForProcessing } from "./trackingToolThunks"
7 8
import storage from "redux-persist/lib/storage"
8
import TrackingToolState from './TrackingToolState'
9
import TrackingToolState from './trackingToolState'
9 10

  
10 11

  
11 12
const defaultPathsPerPage = 5
......
103 104
                })
104 105
            }
105 106
        },
107
        // Removes map marker based on its idx property
108
        removeMapMarker: (state: TrackingToolState, action: { payload: { id: string, idx: number } }) => {
109
            const item = action.payload
110
            const idx = state.primaryPathIdx
111
            if (!state.pathVariants || state.pathVariants.length <= idx) {
112
                return state
113
            }
114

  
115
            return {
116
                ...state,
117
                pathVariants: state.pathVariants.map((pathVariant, i) => {
118
                    if (i !== idx) {
119
                        return [...pathVariant]
120
                    }
121

  
122
                    return [
123
                        ...pathVariant.slice(0, item.idx),
124
                        ...pathVariant.slice(item.idx + 1),
125
                    ]
126
                })
127
            }
128
        },
106 129
        moveMarkerToDestination: (state: TrackingToolState, action: { payload: { destination: number, source: number } }) => {
107 130
            const { destination, source } = action.payload
108 131
            if (!state.pathVariants || state.pathVariants.length === 0) {
......
154 177
            // const itemsToReplace: MapPoint[] = []
155 178
            const itemsToAdd: MapPoint[] = []
156 179
            jsonPath.forEach((item) => {
157
                if (!pathMap.has(item.catalogItem.id as string)) {
180
                if (!pathMap.has(item.catalogItem.id ?? '')) {
158 181
                    itemsToAdd.push(item)
159 182
                    return
160 183
                }
......
231 254
    updateMapMarker,
232 255
    mergeWithCurrentPath,
233 256
    moveMarkerToDestination,
234
    updateMapMarkerWithId
257
    updateMapMarkerWithId,
258
    removeMapMarker,
235 259
} = trackingToolSlice.actions
236 260
const trackingToolReducer = trackingToolSlice.reducer
237 261
export default trackingToolReducer
frontend/src/features/TrackingTool/trackingToolThunks.ts
1 1
import { createAsyncThunk } from '@reduxjs/toolkit'
2 2
import axiosInstance from '../../api/api'
3 3
import { RootState } from '../redux/store'
4
import { MapPoint, PathVariant } from './Map/pathUtils'
4
import { MapPoint, PathVariant } from './trackingToolUtils'
5 5

  
6 6
export const sendTextForProcessing = createAsyncThunk(
7 7
    'trackingTool/sendTextForProcessing',
frontend/src/features/TrackingTool/trackingToolUtils.ts
1
import L, { PointExpression } from 'leaflet'
2
import { CatalogItemDto } from '../../swagger/data-contracts'
3
import { mdiMapMarker } from '@mdi/js'
4

  
5
// For more comprehensive code alias CatalogItemDto[] as path variant
6
export type PathVariant = MapPoint[]
7

  
8
export enum MapPointType {
9
    LocalCatalog, // Fetched from local catalog
10
    ExternalCatalog, // Fetched from external catalog
11
    GeoJson, // From GeoJSON file
12
    FromCoordinates, // From coordinates
13
}
14

  
15
// Represents a point on the map - wrapper for CatalogItemDto to make it easier to work with
16
export interface MapPoint {
17
    id: string // unique id for react
18
    idx: number,
19
    addToPath: boolean, // whether to add the point to the path
20
    catalogItem: CatalogItemDto,
21
    type: MapPointType
22
    hidden?: boolean // if true the point will not be displayed on the map
23
}
24

  
25
export const isMapPointDisplayable = (mapPoint: MapPoint): boolean =>
26
    !!mapPoint.catalogItem.latitude && !!mapPoint.catalogItem.longitude && !mapPoint.hidden
27

  
28
/**
29
 * Based on its type - either imported from local catalog, remote catalogs etc. each type has its own color to differentiate them
30
 * @param item item to get color for
31
 * @returns CSS color string
32
 */
33
export const getMapPointSemanticColor = (item: MapPoint) => {
34
    switch (item.type) {
35
        case MapPointType.LocalCatalog:
36
            return 'inherit'
37
        case MapPointType.FromCoordinates:
38
            return '#21972D'
39
        case MapPointType.ExternalCatalog:
40
            return '#A72020'
41
        case MapPointType.GeoJson:
42
            return '#967520'
43
    }
44
}
45

  
46
const createMapMarkerSvg = (color: string) => {
47
    return `data:image/svg+xml;utf8, ${encodeURIComponent(`
48
    <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="${color}">
49
     <path d="M0 0h24v24H0z" fill="none" /><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z" />
50
     </svg>`)}`
51
}
52

  
53
const mapMarkerSvgs = {
54
    [MapPointType.LocalCatalog]: createMapMarkerSvg('#285CAB'),
55
    [MapPointType.ExternalCatalog]: createMapMarkerSvg('#A72020'),
56
    [MapPointType.GeoJson]: createMapMarkerSvg('#967520'),
57
    [MapPointType.FromCoordinates]: createMapMarkerSvg('#21972D'),
58
}
59

  
60
const iconAnchor = [32, 32] as PointExpression
61
const iconSize = [42, 42] as PointExpression
62

  
63
const mapMarkers = {
64
    [MapPointType.LocalCatalog]: L.icon({
65
        iconAnchor, iconSize,
66
        iconUrl: mapMarkerSvgs[MapPointType.LocalCatalog],
67
    }),
68
    [MapPointType.ExternalCatalog]: L.icon({
69
        iconAnchor, iconSize,
70
        iconUrl: mapMarkerSvgs[MapPointType.ExternalCatalog],
71
    }),
72

  
73
    [MapPointType.GeoJson]: L.icon({
74
        iconAnchor, iconSize,
75
        iconUrl: mapMarkerSvgs[MapPointType.GeoJson],
76
    }),
77
    [MapPointType.FromCoordinates]: L.icon({
78
        iconAnchor, iconSize,
79
        iconUrl: mapMarkerSvgs[MapPointType.FromCoordinates],
80
    }),
81
}
82

  
83
export const getMapPointIcon = (item: MapPoint): L.Icon => mapMarkers[item.type]

Také k dispozici: Unified diff