Revize c0b66eaf
Přidáno uživatelem Václav Honzík před téměř 3 roky(ů)
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
map icon colors start
re #9741