Projekt

Obecné

Profil

« Předchozí | Další » 

Revize 812b9f90

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

draggable list start

re #9741

Zobrazit rozdíly:

frontend/package.json
17 17
    "jwt-decode": "^3.1.2",
18 18
    "leaflet": "^1.8.0",
19 19
    "react": "^17.0.2",
20
    "react-beautiful-dnd": "^13.1.0",
20 21
    "react-dom": "^17.0.2",
21 22
    "react-html-parser": "^2.0.2",
22 23
    "react-leaflet": "3.2.5",
......
31 32
    "swagger-typescript-api": "^9.3.1",
32 33
    "ts-node": "^10.7.0",
33 34
    "typescript": "^4.4.2",
35
    "uuid": "^8.3.2",
34 36
    "web-vitals": "^2.1.0",
35 37
    "yup": "^0.32.11"
36 38
  },
......
68 70
    "@types/leaflet": "^1.7.9",
69 71
    "@types/node": "^16.7.13",
70 72
    "@types/react": "^17.0.43",
73
    "@types/react-beautiful-dnd": "^13.1.2",
71 74
    "@types/react-dom": "^17.0.9",
72 75
    "@types/react-redux": "^7.1.23",
73 76
    "@types/redux-persist": "^4.3.1",
77
    "@types/uuid": "^8.3.4",
74 78
    "@types/yup": "^0.29.13",
75 79
    "redux-devtools-extension": "^2.13.9"
76 80
  }
frontend/src/features/Reusables/DraggableMarker.tsx
1
import { EventedProps } from '@react-leaflet/core'
2
import { LatLngExpression, Marker, MarkerOptions } from 'leaflet'
3
import { FunctionComponent, ReactNode, useCallback, useMemo, useRef, useState } from 'react'
4

  
5
export interface DraggableMarkerProps extends MarkerOptions, EventedProps {
6
    children?: ReactNode
7
    position: LatLngExpression
8
}
9

  
10
const DraggableMarker: FunctionComponent<DraggableMarkerProps> = ({ children, position }) => {
11
    const [draggable, setDraggable] = useState(false)
12
    const [pos, setPos] = useState(position)
13
    const markerRef = useRef(null)
14
    const eventHandlers = useMemo(
15
        () => ({
16
            dragend() {
17
                const marker: any = markerRef.current
18
                if (marker != null) {
19
                    setPos(marker.getLatLng())
20
                }
21
            },
22
        }),
23
        []
24
    )
25
    const toggleDraggable = useCallback(() => {
26
        setDraggable((d) => !d)
27
    }, [])
28

  
29
    return (
30
        // @ts-ignore
31
        <Marker
32
            draggable={draggable}
33
            eventHandlers={eventHandlers}
34
            position={pos}
35
            ref={markerRef}
36
        >
37
            {children}
38
        </Marker>
39
    )
40
}
41

  
42
export default DraggableMarker
frontend/src/features/TrackingTool/DraggableList/DraggableList.tsx
1
import { memo } from 'react'
2
import { DragDropContext, Droppable, OnDragEndResponder } from 'react-beautiful-dnd'
3
import { MapPoint } from '../Map/pathUtils'
4
import DraggableListItem from './DraggableListItem'
5

  
6
export interface DraggableListProps {
7
    items: MapPoint[]
8
    onDragEnd: OnDragEndResponder
9
}
10

  
11
const DraggableList = memo(({ items, onDragEnd }: DraggableListProps) => {
12
    return (
13
        <DragDropContext onDragEnd={onDragEnd}>
14
            <Droppable droppableId="droppable-list">
15
                {(provided) => (
16
                    <div ref={provided.innerRef} {...provided.droppableProps}>
17
                        {items.map((item, index) => (
18
                            <DraggableListItem
19
                                list={items}
20
                                index={index}
21
                                key={item.id}
22
                            />
23
                        ))}
24
                        {provided.placeholder}
25
                    </div>
26
                )}
27
            </Droppable>
28
        </DragDropContext>
29
    )
30
})
31

  
32
export default DraggableList
frontend/src/features/TrackingTool/DraggableList/DraggableListItem.tsx
1
import {
2
    Avatar,
3
    Checkbox,
4
    ListItem,
5
    ListItemAvatar,
6
    ListItemText,
7
} from '@mui/material'
8
import makeStyles from '@mui/material/styles/makeStyles'
9
import { Draggable } from 'react-beautiful-dnd'
10
import { MapPoint, PathVariant } from '../Map/pathUtils'
11
import LocationOnIcon from '@mui/icons-material/LocationOn'
12
import { CatalogItemDto } from '../../../swagger/data-contracts'
13
import { useDispatch } from 'react-redux'
14
import { updateMapMarker, updateMapMarkerWithId } from '../trackingToolSlice'
15
import { Fragment, useEffect, useState } from 'react'
16

  
17
export type DraggableListItemProps = {
18
    list: PathVariant
19
    index: number
20
}
21

  
22
const getFormattedLocationOrEmpty = (catalogItem: CatalogItemDto) => {
23
    if (!catalogItem || !catalogItem.latitude || !catalogItem.longitude) {
24
        return 'Location Unknown'
25
    }
26

  
27
    return `${catalogItem.latitude}°, ${catalogItem.longitude}°`
28
}
29

  
30
const DraggableListItem = ({ list, index }: DraggableListItemProps) => {
31
    const item = list[index]
32
    const dispatch = useDispatch()
33
    const toggleHidden = () => {
34
        dispatch(
35
            updateMapMarkerWithId({
36
                item: {
37
                    ...item,
38
                    active: !item?.active,
39
                } as MapPoint,
40
                id: item.id,
41
            })
42
        )
43
    }
44

  
45
    return (
46
        <Draggable key={`${item.id}`} draggableId={`${item.id}`} index={index}>
47
            {(provided, snapshot) => (
48
                <ListItem
49
                    ref={provided.innerRef}
50
                    {...provided.draggableProps}
51
                    {...provided.dragHandleProps}
52
                    sx={{
53
                        background: snapshot.isDragging ? 'rgb(235,235,235)' : 'inherit',
54
                    }}
55
                >
56
                    <ListItemAvatar>
57
                        <Avatar>
58
                            <LocationOnIcon />
59
                        </Avatar>
60
                    </ListItemAvatar>
61
                    <ListItemText
62
                        primary={item.catalogItem.name ?? 'Unknown name'}
63
                        secondary={getFormattedLocationOrEmpty(
64
                            item.catalogItem
65
                        )}
66
                    />
67
                    <Checkbox checked={item.active} onChange={toggleHidden} />
68
                </ListItem>
69
            )}
70
        </Draggable>
71
    )
72
}
73

  
74
export default DraggableListItem
frontend/src/features/TrackingTool/DraggableList/DraggableMarkerList.tsx
1
import { Paper } from '@mui/material'
2
import { useEffect, useState } from 'react'
3
import { DropResult } from 'react-beautiful-dnd'
4
import { useDispatch, useSelector } from 'react-redux'
5
import { RootState } from '../../redux/store'
6
import { swapMapMarkerIndices } from '../trackingToolSlice'
7
import { PathVariant } from '../Map/pathUtils'
8
import DraggableList from './DraggableList'
9

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

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

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

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

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

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

  
36
    const onDragEnd = ({ destination, source }: DropResult) => {
37
        if (!destination || !source || destination.index === source.index) {
38
            return
39
        }
40

  
41
        dispatch(
42
            swapMapMarkerIndices({
43
                source: source.index,
44
                destination: destination.index,
45
            })
46
        )
47
    }
48

  
49
    return (
50
        <Paper variant="outlined">
51
            <DraggableList items={path ?? []} onDragEnd={onDragEnd} />
52
        </Paper>
53
    )
54
}
55

  
56
export default DraggableMarkerList
frontend/src/features/TrackingTool/Map/MapMarker.tsx
1
import { LatLngTuple, Marker as MarkerPOJO } from 'leaflet'
2
import { FunctionComponent, ReactNode, useMemo, useRef, useState } from 'react'
3
import { Marker } from 'react-leaflet'
4

  
5
export interface MapMarkerProps {
6
    position: LatLngTuple
7
    children?: ReactNode
8
    color?: 'external' | 'disabled' | 'localCatalog'
9
    updatePositionCallbackFn: (position: LatLngTuple) => void // Callback function to notify MapPath to rerender the path
10
}
11

  
12
// Custom Map Marker component
13
const MapMarker: FunctionComponent<MapMarkerProps> = ({
14
    position,
15
    children,
16
    updatePositionCallbackFn,
17
}) => {
18
    const [currentPosition, setCurrentPosition] = useState(position)
19
    const markerRef = useRef<MarkerPOJO | null>(null)
20
    const eventHandlers = useMemo(
21
        () => ({
22
            dragend: () => {
23
                const marker = markerRef.current
24
                console.log(markerRef)
25
                if (!marker) {
26
                    return
27
                }
28
                const latlng = marker.getLatLng()
29
                setCurrentPosition([latlng.lat, latlng.lng])
30
                updatePositionCallbackFn([latlng.lat, latlng.lng])
31
            },
32
        }),
33
        [updatePositionCallbackFn]
34
    )
35

  
36
    return (
37
        <Marker
38
            draggable={true}
39
            position={currentPosition}
40
            eventHandlers={eventHandlers}
41
            ref={markerRef}
42
        >
43
            {children}
44
        </Marker>
45
    )
46
}
47

  
48
export default MapMarker
frontend/src/features/TrackingTool/Map/MapPath.tsx
1
import { Fragment, FunctionComponent, useEffect, useState } from 'react'
2
import { useDispatch, useSelector } from 'react-redux'
3
import { RootState } from '../../redux/store'
4
import { PathVariant, MapPoint, isMapPointDisplayable } from './pathUtils'
5
import TextPath from 'react-leaflet-textpath'
6
import { setPrimaryIdx, updateMapMarker } from '../trackingToolSlice'
7
import MapMarker from './MapMarker'
8
import { LatLngTuple } from 'leaflet'
9
import { Popup, Tooltip } from 'react-leaflet'
10
import { Checkbox, FormControlLabel, Stack, Typography } from '@mui/material'
11
import { formatHtmlStringToReactDom } from '../../../utils/formatting/HtmlUtils'
12
import { DialogCatalogItemDetail as CatalogItemDetailDialog } from '../../Catalog/CatalogItemDetail'
13

  
14
export interface MapPathProps {
15
    idx: number // index of the path in the list
16
}
17

  
18
type EdgeElement = any
19

  
20
// Blue
21
export const primaryPathColor = '#346eeb'
22

  
23
// Grey
24
export const secondaryPathColor = '#878e9c'
25

  
26
const MapPath: FunctionComponent<MapPathProps> = ({ idx }) => {
27
    const dispatch = useDispatch()
28

  
29
    // Get list of all paths from the store
30
    // And extract path from them
31
    const paths = useSelector(
32
        (state: RootState) => state.trackingTool.pathVariants
33
    )
34
    const [path, setPath] = useState<PathVariant>([])
35
    useEffect(() => {
36
        // Either set the path if it exists or set it to an empty array
37
        setPath(paths && paths.length > idx ? paths[idx] : [])
38
    }, [idx, paths])
39

  
40
    // Primary path index to set the correct color
41
    const primaryPathIdx = useSelector(
42
        (state: RootState) => state.trackingTool.primaryPathIdx
43
    )
44

  
45
    // List of all active map points
46
    const [displayableMapPoints, setDisplayableMapPoints] = useState<
47
        MapPoint[]
48
    >([])
49
    useEffect(() => {
50
        // Set all displayable vertices
51
        setDisplayableMapPoints(
52
            path.filter((mapPoint) => isMapPointDisplayable(mapPoint))
53
        )
54
    }, [path])
55

  
56
    // List of all edges in the path
57
    const [edges, setEdges] = useState<EdgeElement[]>([])
58
    useEffect(() => {
59
        // Get all active map points
60
        const activeMapPoints = displayableMapPoints.filter(
61
            (item) => item.active
62
        )
63
        if (activeMapPoints.length < 2) {
64
            setEdges([])
65
            return
66
        }
67

  
68
        // Build edges
69
        const edges = []
70
        for (let i = 0; i < activeMapPoints.length - 1; i += 1) {
71
            const [start, end] = [
72
                activeMapPoints[i].catalogItem,
73
                activeMapPoints[i + 1].catalogItem,
74
            ]
75
            edges.push(
76
                <TextPath
77
                    // Somehow this refuses to work so let it rerender everything ...
78
                    key={`${start.id}-${end.id}:${start.latitude},${start.longitude}-${end.latitude},${end.longitude}`}
79
                    positions={[
80
                        [start.latitude, start.longitude],
81
                        [end.latitude, end.longitude],
82
                    ]}
83
                    text="►"
84
                    // text=" > > > > "
85
                    attributes={{
86
                        'font-size': 19,
87
                        // Set to primaryPathColor if primary index in the tracking tool is equal to this index
88
                        fill:
89
                            primaryPathIdx === idx
90
                                ? primaryPathColor
91
                                : secondaryPathColor,
92
                    }}
93
                    onClick={() => dispatch(setPrimaryIdx(idx))}
94
                    repeat
95
                    center
96
                    weight={0}
97
                />
98
            )
99
        }
100
        setEdges(edges)
101
    }, [dispatch, displayableMapPoints, idx, primaryPathIdx])
102

  
103
    // List of vertices to display
104
    const [vertices, setVertices] = useState<JSX.Element[]>([])
105
    useEffect(() => {
106
        // Iterate over all displayable map points and map them to MapMarker
107
        setVertices(
108
            displayableMapPoints.map((item) => (
109
                <MapMarker
110
                    key={`${item.catalogItem.latitude}${item.catalogItem.longitude}`}
111
                    position={[
112
                        item.catalogItem.latitude as number,
113
                        item.catalogItem.longitude as number,
114
                    ]}
115
                    updatePositionCallbackFn={(position: LatLngTuple) => {
116
                        dispatch(
117
                            updateMapMarker({
118
                                item: {
119
                                    ...item,
120
                                    catalogItem: {
121
                                        ...item.catalogItem,
122
                                        latitude: position[0],
123
                                        longitude: position[1],
124
                                    },
125
                                },
126
                            })
127
                        )
128
                    }}
129
                >
130
                    <Fragment>
131
                        <Tooltip>
132
                            {/* <Typography> */}
133
                            {item.catalogItem.name ?? ''}
134
                            {/* </Typography> */}
135
                        </Tooltip>
136
                        <Popup>
137
                            <Fragment>
138
                                <Stack direction="column" sx={{ m: 0 }}>
139
                                    <Typography
140
                                        variant="h6"
141
                                        fontWeight="bold"
142
                                        fontSize={16}
143
                                    >
144
                                        {formatHtmlStringToReactDom(
145
                                            item.catalogItem.name as string
146
                                        )}
147
                                    </Typography>
148
                                    <FormControlLabel
149
                                        control={
150
                                            <Checkbox
151
                                                checked={item.active}
152
                                                onChange={() => {
153
                                                    dispatch(
154
                                                        updateMapMarker({
155
                                                            item: {
156
                                                                ...item,
157
                                                                active: !item.active,
158
                                                            },
159
                                                        })
160
                                                    )
161
                                                }}
162
                                            />
163
                                        }
164
                                        labelPlacement="end"
165
                                        label="Active"
166
                                    />
167
                                    <CatalogItemDetailDialog
168
                                        itemId={item.catalogItem.id ?? ''}
169
                                    />
170
                                </Stack>
171
                            </Fragment>
172
                        </Popup>
173
                    </Fragment>
174
                </MapMarker>
175
            ))
176
        )
177
    }, [dispatch, displayableMapPoints, idx])
178

  
179
    return (
180
        <Fragment>
181
            {vertices}
182
            {edges}
183
        </Fragment>
184
    )
185
}
186

  
187
export default MapPath
frontend/src/features/TrackingTool/Map/pathUtils.ts
1
// Business logic for tracking tool
2

  
3
import { CatalogItemDto, PathDto } from '../../../swagger/data-contracts'
4
import generateUuid from '../../../utils/id/uuidGenerator'
5

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

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

  
16
// Represents a point on the map - wrapper for CatalogItemDto to make it easier to work with
17
export interface MapPoint {
18
    id: string // unique id for react
19
    idx: number,
20
    active: boolean,
21
    catalogItem: CatalogItemDto,
22
    type: MapPointType
23
}
24

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

  
28
/**
29
 * Cartesian product of two arrays
30
 * @param sets
31
 * @returns
32
 */
33
const cartesianProduct = (sets: CatalogItemDto[][]): CatalogItemDto[][] =>
34
    sets.reduce<CatalogItemDto[][]>(
35
        (results, ids) =>
36
            results
37
                .map((result) => ids.map((id) => [...result, id]))
38
                .reduce((nested, result) => [...nested, ...result]),
39
        [[]]
40
    )
41

  
42
/**
43
 * Builds a list of all possible path variants from pathDto
44
 * @param pathDto
45
 * @returns
46
 */
47
export const buildPathVariants = (pathDto: PathDto, mapPointType: MapPointType = MapPointType.LocalCatalog): PathVariant[] => {
48
    if (!pathDto.foundCatalogItems) {
49
        return []
50
    }
51

  
52
    return (
53
        pathDto.foundCatalogItems.length === 1
54
            ? pathDto.foundCatalogItems
55
            : cartesianProduct(pathDto.foundCatalogItems)
56
    ).map((variant, _) =>
57
        variant.map(
58
            (catalogItem, idx) => (
59
                {
60
                    id: generateUuid(),
61
                    idx,
62
                    active: !!catalogItem.latitude && !!catalogItem.longitude,
63
                    catalogItem,
64
                    type: mapPointType,
65
                })
66
        )
67
    )
68
}
69

  
70
export default buildPathVariants
frontend/src/features/TrackingTool/MapMarker.tsx
1
import { LatLngTuple, Marker as MarkerPOJO } from "leaflet"
2
import { FunctionComponent, ReactNode, useMemo, useRef, useState } from "react"
3
import { Marker } from "react-leaflet"
4

  
5
export interface MapMarkerProps {
6
    position: LatLngTuple
7
    children?: ReactNode
8
    color?: "external" | "disabled" | "localCatalog"
9
    updatePositionCallbackFn: (position: LatLngTuple) => void // Callback function to notify MapPath to rerender the path
10
}
11

  
12
// Custom Map Marker component
13
const MapMarker: FunctionComponent<MapMarkerProps> = ({
14
    position,
15
    children,
16
    updatePositionCallbackFn,
17
}) => {
18
    const [currentPosition, setCurrentPosition] = useState(position)
19
    const markerRef = useRef<MarkerPOJO | null>(null)
20
    const eventHandlers = useMemo(
21
        () => ({
22
            dragend: () => {
23
                const marker = markerRef.current
24
                console.log(markerRef)
25
                if (!marker) {
26
                    return
27
                }
28
                const latlng = marker.getLatLng()
29
                setCurrentPosition([latlng.lat, latlng.lng])
30
                updatePositionCallbackFn([latlng.lat, latlng.lng])
31
            },
32
        }),
33
        [updatePositionCallbackFn]
34
    )
35

  
36
    return (
37
        <Marker
38
            draggable={true}
39
            position={currentPosition}
40
            eventHandlers={eventHandlers}
41
            ref={markerRef}
42
        >
43
            {children}
44
        </Marker>
45
    )
46
}
47

  
48
export default MapMarker
frontend/src/features/TrackingTool/MapPath.tsx
1
import { Fragment, FunctionComponent, useEffect, useState } from 'react'
2
import { useDispatch, useSelector } from 'react-redux'
3
import { RootState } from '../redux/store'
4
import { PathVariant, MapPoint, isMapPointDisplayable } from './pathUtils'
5
import TextPath from 'react-leaflet-textpath'
6
import { setPrimaryIdx, updateMapMarker } from './trackingToolSlice'
7
import MapMarker from './MapMarker'
8
import { LatLngTuple } from 'leaflet'
9
import { Popup, Tooltip } from 'react-leaflet'
10
import { Checkbox, FormControlLabel, Stack, Typography } from '@mui/material'
11
import { formatHtmlStringToReactDom } from '../../utils/formatting/HtmlUtils'
12
import { DialogCatalogItemDetail as CatalogItemDetailDialog } from '../Catalog/CatalogItemDetail'
13

  
14
export interface MapPathProps {
15
    idx: number // index of the path in the list
16
}
17

  
18
type EdgeElement = any
19

  
20
// Blue
21
export const primaryPathColor = '#346eeb'
22

  
23
// Grey
24
export const secondaryPathColor = '#878e9c'
25

  
26
const MapPath: FunctionComponent<MapPathProps> = ({ idx }) => {
27
    const dispatch = useDispatch()
28

  
29
    // Get list of all paths from the store
30
    // And extract path from them
31
    const paths = useSelector(
32
        (state: RootState) => state.trackingTool.pathVariants
33
    )
34
    const [path, setPath] = useState<PathVariant>([])
35
    useEffect(() => {
36
        // Either set the path if it exists or set it to an empty array
37
        setPath(paths && paths.length > idx ? paths[idx] : [])
38
    }, [idx, paths])
39

  
40
    // Primary path index to set the correct color
41
    const primaryPathIdx = useSelector(
42
        (state: RootState) => state.trackingTool.primaryPathIdx
43
    )
44

  
45
    // List of all active map points
46
    const [displayableMapPoints, setDisplayableMapPoints] = useState<
47
        MapPoint[]
48
    >([])
49
    useEffect(() => {
50
        // Set all displayable vertices
51
        setDisplayableMapPoints(
52
            path.filter((mapPoint) => isMapPointDisplayable(mapPoint))
53
        )
54
    }, [path])
55

  
56
    // List of all edges in the path
57
    const [edges, setEdges] = useState<EdgeElement[]>([])
58
    useEffect(() => {
59
        // Get all active map points
60
        const activeMapPoints = displayableMapPoints.filter(
61
            (item) => item.active
62
        )
63
        if (activeMapPoints.length < 2) {
64
            setEdges([])
65
            return
66
        }
67

  
68
        // Build edges
69
        const edges = []
70
        for (let i = 0; i < activeMapPoints.length - 1; i += 1) {
71
            const [start, end] = [
72
                activeMapPoints[i].catalogItem,
73
                activeMapPoints[i + 1].catalogItem,
74
            ]
75
            edges.push(
76
                <TextPath
77
                    // Somehow this refuses to work so let it rerender everything ...
78
                    key={`${start.id}-${end.id}:${start.latitude},${start.longitude}-${end.latitude},${end.longitude}`}
79
                    positions={[
80
                        [start.latitude, start.longitude],
81
                        [end.latitude, end.longitude],
82
                    ]}
83
                    text="►"
84
                    // text=" > > > > "
85
                    attributes={{
86
                        'font-size': 19,
87
                        // Set to primaryPathColor if primary index in the tracking tool is equal to this index
88
                        fill:
89
                            primaryPathIdx === idx
90
                                ? primaryPathColor
91
                                : secondaryPathColor,
92
                    }}
93
                    onClick={() => dispatch(setPrimaryIdx(idx))}
94
                    repeat
95
                    center
96
                    weight={0}
97
                />
98
            )
99
        }
100
        setEdges(edges)
101
    }, [dispatch, displayableMapPoints, idx, primaryPathIdx])
102

  
103
    // List of vertices to display
104
    const [vertices, setVertices] = useState<JSX.Element[]>([])
105
    useEffect(() => {
106
        // Iterate over all displayable map points and map them to MapMarker
107
        setVertices(
108
            displayableMapPoints.map((item) => (
109
                <MapMarker
110
                    key={`${item.catalogItem.latitude}${item.catalogItem.longitude}`}
111
                    position={[
112
                        item.catalogItem.latitude as number,
113
                        item.catalogItem.longitude as number,
114
                    ]}
115
                    updatePositionCallbackFn={(position: LatLngTuple) => {
116
                        dispatch(
117
                            updateMapMarker({
118
                                idx,
119
                                item: {
120
                                    idx: item.idx,
121
                                    active: item.active,
122
                                    catalogItem: {
123
                                        ...item.catalogItem,
124
                                        latitude: position[0],
125
                                        longitude: position[1],
126
                                    },
127
                                },
128
                            })
129
                        )
130
                    }}
131
                >
132
                    <Fragment>
133
                        <Tooltip>
134
                            {/* <Typography> */}
135
                            {item.catalogItem.name ?? ''}
136
                            {/* </Typography> */}
137
                        </Tooltip>
138
                        <Popup>
139
                            <Fragment>
140
                                <Stack direction="column" sx={{ m: 0 }}>
141
                                    <Typography
142
                                        variant="h6"
143
                                        fontWeight="bold"
144
                                        fontSize={16}
145
                                    >
146
                                        {formatHtmlStringToReactDom(
147
                                            item.catalogItem.name as string
148
                                        )}
149
                                    </Typography>
150
                                    <FormControlLabel
151
                                        control={
152
                                            <Checkbox
153
                                                checked={item.active}
154
                                                onChange={() => {
155
                                                    dispatch(
156
                                                        updateMapMarker({
157
                                                            idx,
158
                                                            item: {
159
                                                                ...item,
160
                                                                active: !item.active,
161
                                                            },
162
                                                        })
163
                                                    )
164
                                                }}
165
                                            />
166
                                        }
167
                                        labelPlacement="end"
168
                                        label="Active"
169
                                    />
170
                                    <CatalogItemDetailDialog
171
                                        itemId={item.catalogItem.id ?? ''}
172
                                    />
173
                                </Stack>
174
                            </Fragment>
175
                        </Popup>
176
                    </Fragment>
177
                </MapMarker>
178
            ))
179
        )
180
    }, [dispatch, displayableMapPoints, idx])
181

  
182
    return (
183
        <Fragment>
184
            {vertices}
185
            {edges}
186
        </Fragment>
187
    )
188
}
189

  
190
export default MapPath
frontend/src/features/TrackingTool/ProcessedText/ProcessedTextDisplay.tsx
1
import { Card, CardContent, Stack, Typography } from '@mui/material'
2
import { Fragment } from 'react'
3
import { useSelector } from 'react-redux'
4
import { formatHtmlStringToReactDom } from '../../../utils/formatting/HtmlUtils'
5
import { RootState } from '../../redux/store'
6

  
7
const ProcessedTextDisplay = () => {
8
    const pathDto = useSelector(
9
        (state: RootState) => state.trackingTool.pathDto
10
    )
11

  
12
    return (
13
        <Fragment>
14
            {pathDto && (
15
                <Card variant="outlined" sx={{maxHeight: '50vh'}}>
16
                    <CardContent>
17
                        <Stack direction="column">
18
                            <Typography
19
                                variant="h5"
20
                                sx={{ mb: 1 }}
21
                                fontWeight="600"
22
                            >
23
                                Processed Text
24
                            </Typography>
25
                            <Typography variant="body2">
26
                                {formatHtmlStringToReactDom(pathDto.text ?? '')}
27
                            </Typography>
28
                        </Stack>
29
                    </CardContent>
30
                </Card>
31
            )}
32
        </Fragment>
33
    )
34
}
35

  
36
export default ProcessedTextDisplay
frontend/src/features/TrackingTool/TrackingTool.tsx
13 13
import FileUpload from './Upload/FileUpload'
14 14
import { Map } from 'leaflet'
15 15
import { formatHtmlStringToReactDom } from '../../utils/formatting/HtmlUtils'
16
import MapPath from './MapPath'
16
import MapPath from './Map/MapPath'
17 17
import { useDispatch, useSelector } from 'react-redux'
18 18
import { RootState } from '../redux/store'
19 19
import { clear, consumeErr as consumeError } from './trackingToolSlice'
......
21 21
import ClearIcon from '@mui/icons-material/Clear'
22 22
import GeoJsonExportButton from './Upload/GeoJsonExportButton'
23 23
import GeoJsonImportDialog from './Upload/GeoJsonImportDialog'
24
import ProcessedTextDisplay from './ProcessedText/ProcessedTextDisplay'
25
import DraggableMarkerList from './DraggableList/DraggableMarkerList'
24 26

  
25 27
// Page with tracking tool
26 28
const TrackingTool = () => {
......
150 152
                            <MapPath idx={idx} />
151 153
                        ))}
152 154
                    </MapContainer>
153
                    {pathDto && (
154
                        <Fragment>
155
                            <Card variant="outlined" sx={{ mt: 2 }}>
156
                                <CardContent>
157
                                    <Stack direction="column">
158
                                        <Typography
159
                                            variant="h5"
160
                                            sx={{ mb: 1 }}
161
                                            fontWeight="600"
162
                                        >
163
                                            Processed Text
164
                                        </Typography>
165
                                        <Typography variant="body2">
166
                                            {formatHtmlStringToReactDom(
167
                                                pathDto.text ?? ''
168
                                            )}
169
                                        </Typography>
170
                                    </Stack>
171
                                </CardContent>
172
                            </Card>
173
                        </Fragment>
174
                    )}
155
                </Grid>
156
                <Grid container sx={{ mt: 1, mb: 20 }} spacing={1}>
157
                    <Grid item xs={12} md={6}>
158
                        <DraggableMarkerList />
159
                    </Grid>
160
                    <Grid item xs={12} md={6}>
161
                        <ProcessedTextDisplay />
162
                    </Grid>
175 163
                </Grid>
176 164
            </Grid>
177 165
        </Fragment>
frontend/src/features/TrackingTool/TrackingToolState.ts
1 1
import { LatLngTuple } from 'leaflet'
2 2
import { PathDto } from '../../swagger/data-contracts'
3
import { PathVariant } from './pathUtils'
3
import { PathVariant } from './Map/pathUtils'
4 4

  
5 5
export default interface TrackingToolState {
6 6
    isLoading: boolean // whether the data is being loaded
frontend/src/features/TrackingTool/Upload/GeoJsonExportButton.tsx
2 2
import { useEffect, useState } from 'react'
3 3
import { useSelector } from 'react-redux'
4 4
import { RootState } from '../../redux/store'
5
import { isMapPointDisplayable, PathVariant } from '../pathUtils'
5
import { isMapPointDisplayable, PathVariant } from '../Map/pathUtils'
6 6
import { exportAsGeoJsonString } from './GeoJsonIo'
7 7

  
8 8
const GeoJsonExportButton = () => {
frontend/src/features/TrackingTool/Upload/GeoJsonImportDialog.tsx
1
import { Button, Dialog, DialogContent, DialogTitle } from '@mui/material'
1
import { DialogContent, DialogTitle } from '@mui/material'
2 2
import { useFormik } from 'formik'
3
import { Fragment, useState } from 'react'
4
import { useDispatch, useSelector } from 'react-redux'
5
import { RootState } from '../../redux/store'
3
import { useState } from 'react'
4
import { useDispatch } from 'react-redux'
6 5
import ButtonOpenableDialog from '../../Reusables/ButtonOpenableDialog'
7
import { PathVariant } from '../pathUtils'
8 6
import * as yup from 'yup'
9 7
import { showNotification } from '../../Notification/notificationSlice'
10 8
import SingleFileSelectionForm from '../../Reusables/SingleFileSelectionForm'
......
58 56
            reader.readAsText(values.file as File)
59 57
            reader.onload = async () => {
60 58
                try {
61
                    const pathVariant = parseGeoJsonToPathVariant(reader.result as string)
62
                    console.log(pathVariant)
59
                    const pathVariant = parseGeoJsonToPathVariant(
60
                        reader.result as string
61
                    )
63 62
                    // Merge current path variant with the new one
64 63
                    dispatch(mergeWithCurrentPath(pathVariant))
65 64
                    onClose()
frontend/src/features/TrackingTool/Upload/GeoJsonIo.ts
1
import { isMapPointDisplayable, PathVariant } from '../pathUtils'
1
import { isMapPointDisplayable, MapPoint, MapPointType, PathVariant } from '../Map/pathUtils'
2 2
import * as yup from 'yup'
3
import generateUuid from '../../../utils/id/uuidGenerator'
3 4

  
4 5
export const exportAsGeoJsonString = (path: PathVariant) => JSON.stringify({
5 6
    type: 'FeatureCollection',
......
28 29
})
29 30

  
30 31
const catalogItemValidationSchema = yup.object({
31
    id: yup.string().required(),
32
    name: yup.string().required(),
33
    allNames: yup.array().of(yup.string()).required(),
34
    description: yup.string(),
32
    id: yup.string().optional(),
33
    name: yup.string().optional(),
34
    allNames: yup.array().of(yup.string()).optional(),
35
    description: yup.string().optional(),
35 36
    latitude: yup.number().required(),
36 37
    longitude: yup.number().required(),
37 38
})
......
52 53
    }
53 54
    const path: PathVariant = features.map((feature: any) => {
54 55
        const catalogItemDto = feature.properties.catalogItem
55
        
56

  
56 57
        if (!catalogItemDto) {
57 58
            throw new Error('GeoJson file does not have a valid structure')
58 59
        }
......
60 61
        const catalogItem = catalogItemValidationSchema.validateSync(catalogItemDto)
61 62

  
62 63
        return {
64
            id: generateUuid(),
63 65
            idx: feature.properties.idx,
64 66
            active: true,
65 67
            catalogItem: {
......
69 71
                latitude: catalogItem.latitude,
70 72
                longitude: catalogItem.longitude,
71 73
            },
72
        }
74
            type: MapPointType.GeoJson,
75
        } as MapPoint
73 76
    })
74 77
    return path
75 78
}
frontend/src/features/TrackingTool/pathUtils.ts
1
// Business logic for tracking tool
2

  
3
import { CatalogItemDto, PathDto } from '../../swagger/data-contracts'
4

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

  
8
export interface MapPoint {
9
    idx: number,
10
    active: boolean,
11
    catalogItem: CatalogItemDto,
12
}
13

  
14
export const isMapPointDisplayable = (mapPoint: MapPoint): boolean =>
15
    !!mapPoint.catalogItem.latitude && !!mapPoint.catalogItem.longitude
16

  
17
/**
18
 * Cartesian product of two arrays
19
 * @param sets
20
 * @returns
21
 */
22
const cartesianProduct = (sets: CatalogItemDto[][]): CatalogItemDto[][] =>
23
    sets.reduce<CatalogItemDto[][]>(
24
        (results, ids) =>
25
            results
26
                .map((result) => ids.map((id) => [...result, id]))
27
                .reduce((nested, result) => [...nested, ...result]),
28
        [[]]
29
    )
30

  
31
/**
32
 * Builds a list of all possible path variants from pathDto
33
 * @param pathDto
34
 * @returns
35
 */
36
export const buildPathVariants = (pathDto: PathDto): PathVariant[] => {
37
    if (!pathDto.foundCatalogItems) {
38
        return []
39
    }
40

  
41
    return (
42
        pathDto.foundCatalogItems.length === 1
43
            ? pathDto.foundCatalogItems
44
            : cartesianProduct(pathDto.foundCatalogItems)
45
    ).map((variant, _) =>
46
        variant.map(
47
            (catalogItem, idx) => (
48
                {
49
                    idx,
50
                    active: !!catalogItem.latitude && !!catalogItem.longitude,
51
                    catalogItem
52
                })
53
        )
54
    )
55
}
56

  
57
export default buildPathVariants
frontend/src/features/TrackingTool/trackingToolSlice.ts
2 2
import { LatLngTuple } from "leaflet"
3 3
import mapConfig from "../../config/mapConfig"
4 4
import { PathDto } from "../../swagger/data-contracts"
5
import buildPathVariants, { isMapPointDisplayable, MapPoint, PathVariant } from "./pathUtils"
5
import buildPathVariants, { isMapPointDisplayable, MapPoint, PathVariant } from "./Map/pathUtils"
6 6
import { sendTextForProcessing } from "./trackingToolThunks"
7 7
import storage from "redux-persist/lib/storage"
8 8
import TrackingToolState from './TrackingToolState'
......
60 60
            ...state,
61 61
            currentPage: action.payload,
62 62
        }),
63
        updateMapMarker: (state: TrackingToolState, action: { payload: { idx: number, item: MapPoint } }) => {
64
            const { idx, item } = action.payload
63
        // Updates map marker while ignoring its idx property
64
        updateMapMarkerWithId: (state: TrackingToolState, action: { payload: { id: string, item: MapPoint } }) => {
65
            const { item } = action.payload
66
            const idx = state.primaryPathIdx
67
            if (!state.pathVariants || state.pathVariants.length <= idx) {
68
                return state
69
            }
70

  
71
            const mapMarkerIdx = state.pathVariants[idx].findIndex((item) => item.id === action.payload.id)
72
            if (mapMarkerIdx === -1) {
73
                return state
74
            }
75

  
76
            const newPathVariant = [...state.pathVariants[idx]]
77
            newPathVariant[mapMarkerIdx] = item
78
            return {
79
                ...state,
80
                pathVariants: [...state.pathVariants.slice(0, idx), newPathVariant, ...state.pathVariants.slice(idx + 1)],
81
            }
82
        },
83
        // Updates map marker based on its idx property
84
        updateMapMarker: (state: TrackingToolState, action: { payload: { item: MapPoint } }) => {
85
            const { item } = action.payload
86
            const idx = state.primaryPathIdx
65 87
            if (!state.pathVariants || state.pathVariants.length <= idx) {
66 88
                return state
67 89
            }
......
81 103
                })
82 104
            }
83 105
        },
106
        swapMapMarkerIndices: (state: TrackingToolState, action: { payload: { destination: number, source: number } }) => {
107
            const { destination, source } = action.payload
108
            if (!state.pathVariants || state.pathVariants.length === 0) {
109
                return state
110
            }
111

  
112
            return {
113
                ...state,
114
                pathVariants: state.pathVariants.map((pathVariant, i) => {
115
                    if (state.primaryPathIdx !== i) {
116
                        return [...pathVariant]
117
                    }
118

  
119
                    if (pathVariant.length <= destination || pathVariant.length <= source) {
120
                        return [...pathVariant]
121
                    }
122

  
123
                    // JS dark magic splice
124
                    const result = [...pathVariant]
125
                    const [removed] = result.splice(source, 1)
126
                    result.splice(destination, 0, removed)
127
                    return result
128
                })
129
            }
130
        },
84 131
        clear: () => ({ ...initialState }),
85 132
        mergeWithCurrentPath: (state: TrackingToolState, action: { payload: PathVariant }) => {
86 133
            const { payload: jsonPath } = action
......
104 151
            const pathMap = new Map(path.map((item) => [item.catalogItem.id as string, item]))
105 152

  
106 153
            // Create an array of items to be replaced and items to be added to the end
107
            const itemsToReplace: MapPoint[] = []
154
            // const itemsToReplace: MapPoint[] = []
108 155
            const itemsToAdd: MapPoint[] = []
109 156
            jsonPath.forEach((item) => {
110 157
                if (!pathMap.has(item.catalogItem.id as string)) {
111 158
                    itemsToAdd.push(item)
112 159
                    return
113 160
                }
114
                
115
                const idx = pathMap.get(item.catalogItem.id as string)!.idx
116
                item.idx = idx
117
                itemsToReplace.push(item)
161

  
162
                // const idx = pathMap.get(item.catalogItem.id as string)!.idx
163
                // item.idx = idx
164
                // itemsToReplace.push(item)
118 165
            })
119 166

  
120 167
            // Iterate over items to replace and replace them
121 168
            const newPath = [...path]
122
            itemsToReplace.forEach((item) => {
123
                newPath[item.idx] = item
124
            })
169
            // itemsToReplace.forEach((item) => {
170
            //     newPath[item.idx] = item
171
            // })
125 172

  
126 173
            // Add items to the end
127 174
            itemsToAdd.forEach((item) => {
128
                item.active = false
175
                item.active = !state.pathVariants || state.pathVariants.length === 0
129 176
                item.idx = newPath.length
130 177
                newPath.push(item)
131 178
            })
......
176 223
    },
177 224
})
178 225

  
179
export const { consumeErr, setPrimaryIdx, resetDialogApiCallSuccess, clear, updateMapMarker, mergeWithCurrentPath } =
180
    trackingToolSlice.actions
226
export const {
227
    consumeErr,
228
    setPrimaryIdx,
229
    resetDialogApiCallSuccess,
230
    clear,
231
    updateMapMarker,
232
    mergeWithCurrentPath,
233
    swapMapMarkerIndices,
234
    updateMapMarkerWithId
235
} = trackingToolSlice.actions
181 236
const trackingToolReducer = trackingToolSlice.reducer
182 237
export default trackingToolReducer
frontend/src/features/TrackingTool/trackingToolThunks.ts
1 1
import { createAsyncThunk } from '@reduxjs/toolkit'
2 2
import axiosInstance from '../../api/api'
3 3
import { RootState } from '../redux/store'
4
import { MapPoint, PathVariant } from './pathUtils'
4
import { MapPoint, PathVariant } from './Map/pathUtils'
5 5

  
6 6
export const sendTextForProcessing = createAsyncThunk(
7 7
    'trackingTool/sendTextForProcessing',

Také k dispozici: Unified diff