Projekt

Obecné

Profil

« Předchozí | Další » 

Revize 11fca75a

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

autozoom + file upload

re #9741

Zobrazit rozdíly:

frontend/src/config/mapConfig.ts
11 11
    attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
12 12
    url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
13 13
    defaultCoordinates: [33.5138, 36.2765], // Damascus, Syria
14
    defaultZoom: 8
14
    defaultZoom: 6
15 15
}
16 16

  
17 17
export default mapConfig
frontend/src/features/TrackingTool/FileUpload.tsx
5 5
    Link,
6 6
    Stack,
7 7
    Typography,
8
} from '@mui/material'
9
import { useFormik } from 'formik'
10
import { Fragment, useState } from 'react'
11
import * as yup from 'yup'
12
import axiosInstance from '../../api/api'
13
import ButtonOpenableDialog from '../Reusables/ButtonOpenableDialog'
14
import AttachmentIcon from '@mui/icons-material/Attachment'
15
import DeleteIcon from '@mui/icons-material/Delete'
16
import SendIcon from '@mui/icons-material/Send'
8
} from "@mui/material"
9
import { useFormik } from "formik"
10
import { Fragment, useState } from "react"
11
import * as yup from "yup"
12
import axiosInstance from "../../api/api"
13
import ButtonOpenableDialog from "../Reusables/ButtonOpenableDialog"
14
import AttachmentIcon from "@mui/icons-material/Attachment"
15
import DeleteIcon from "@mui/icons-material/Delete"
16
import SendIcon from "@mui/icons-material/Send"
17
import { useDispatch } from "react-redux"
18
import { sendTextForProcessing } from "./trackingToolThunks"
19

  
20
interface UploadValues {
21
    file?: File
22
}
23

  
24
const initialValues: UploadValues = {}
17 25

  
18 26
const FileUpload = () => {
27
    const dispatch = useDispatch()
28

  
19 29
    const [filename, setFilename] = useState<string | undefined>(undefined)
30
    const [fileProcessing, setFileProcessing] = useState(false)
20 31

  
21 32
    const validationSchema = yup.object().shape({
22
        file: yup.mixed().required('File is required'),
33
        file: yup.mixed().required("File is required"),
23 34
    })
24 35

  
25 36
    const formik = useFormik({
26
        initialValues: {
27
            file: undefined,
28
        },
37
        initialValues,
29 38
        validationSchema,
30 39
        onSubmit: async (values) => {
31
            
40
            setFileProcessing(true)
41
            const reader = new FileReader()
42
            reader.readAsText(values.file as File)
43
            reader.onload = async () => {
44
                dispatch(sendTextForProcessing(reader.result as string))
45
                setFileProcessing(false)
46
            }
32 47
        },
33 48
    })
34 49

  
......
37 52
        const file = event.currentTarget.files[0]
38 53
        if (file) {
39 54
            setFilename(file.name)
40
            formik.setFieldValue('file', file)
55
            formik.setFieldValue("file", file)
41 56
        }
42 57
    }
43 58

  
44 59
    const onClose = () => {
60
        if (fileProcessing) {
61
            return
62
        }
45 63
        setFilename(undefined)
46 64
        formik.resetForm()
47 65
    }
48 66

  
49 67
    const onClearSelectedFile = () => {
50 68
        setFilename(undefined)
51
        formik.setFieldValue('file', undefined)
69
        formik.setFieldValue("file", undefined)
52 70
    }
53 71

  
54 72
    return (
......
91 109
                        <Fragment>
92 110
                            <Stack direction="row" spacing={1}>
93 111
                                <Typography
94
                                    sx={{
95
                                        // textOverflow: 'ellipsis',
96
                                        // overflow: 'hidden',
97
                                    }}
112
                                    sx={
113
                                        {
114
                                            // textOverflow: 'ellipsis',
115
                                            // overflow: 'hidden',
116
                                        }
117
                                    }
98 118
                                    variant="body1"
99 119
                                >
100
                                    Selected File:{' '}
120
                                    Selected File:{" "}
101 121
                                </Typography>
102 122
                                <Typography
103 123
                                    sx={{
104
                                        textOverflow: 'ellipsis',
105
                                        overflow: 'hidden',
124
                                        textOverflow: "ellipsis",
125
                                        overflow: "hidden",
106 126
                                    }}
107 127
                                    // color="text.secondary"
108 128
                                    component={Link}
......
117 137
                                justifyContent="flex-end"
118 138
                                alignItems="center"
119 139
                                spacing={2}
120
                                sx={{mt: 2}}
140
                                sx={{ mt: 2 }}
121 141
                            >
122 142
                                <Button
123 143
                                    // sx={{ mb: 2, mt: 1 }}
......
128 148
                                >
129 149
                                    Remove Selection
130 150
                                </Button>
131
                                <Button size="small" type="submit" variant="contained" startIcon={<SendIcon />}>
132
                                Submit
133
                            </Button>
151
                                <Button
152
                                    size="small"
153
                                    type="submit"
154
                                    variant="contained"
155
                                    startIcon={<SendIcon />}
156
                                >
157
                                    Submit
158
                                </Button>
134 159
                            </Stack>
135
                            
136
                            
137 160
                        </Fragment>
138 161
                    )}
139 162
                </form>
frontend/src/features/TrackingTool/MapPath.tsx
6 6
import { setPrimaryIdx, updateMapMarker } from "./trackingToolSlice"
7 7
import MapMarker from "./MapMarker"
8 8
import { LatLngTuple } from "leaflet"
9
import { Popup } from "react-leaflet"
9
import { Popup, Tooltip } from "react-leaflet"
10 10
import { Checkbox, FormControlLabel, Stack, Typography } from "@mui/material"
11 11
import { formatHtmlStringToReactDom } from "../../utils/formatting/HtmlUtils"
12 12
import { DialogCatalogItemDetail as CatalogItemDetailDialog } from "../Catalog/CatalogItemDetail"
......
123 123
                        )
124 124
                    }}
125 125
                >
126
                    <Popup>
127
                        <Fragment>
128
                            <Stack direction="column" sx={{ m: 0 }}>
129
                                <Typography
130
                                    variant="h6"
131
                                    fontWeight="bold"
132
                                    fontSize={16}
133
                                >
134
                                    {formatHtmlStringToReactDom(
135
                                        item.catalogItem.name as string
136
                                    )}
137
                                </Typography>
138
                                <FormControlLabel
139
                                    control={
140
                                        <Checkbox
141
                                            checked={item.active}
142
                                            onChange={() => {
143
                                                dispatch(
144
                                                    updateMapMarker({
145
                                                        idx,
146
                                                        item: new MapPoint(
147
                                                            item.idx,
148
                                                            !item.active,
149
                                                            item.catalogItem
150
                                                        ),
151
                                                    })
152
                                                )
153
                                            }}
154
                                        />
155
                                    }
156
                                    labelPlacement="end"
157
                                    label="Active"
158
                                />
159
                                <CatalogItemDetailDialog
160
                                    itemId={item.catalogItem.id ?? ""}
161
                                />
162
                            </Stack>
163
                        </Fragment>
164
                    </Popup>
126
                    <Fragment>
127
                        <Tooltip>
128
                            {/* <Typography> */}
129
                                {item.catalogItem.name ?? ""}
130
                            {/* </Typography> */}
131
                        </Tooltip>
132
                        <Popup>
133
                            <Fragment>
134
                                <Stack direction="column" sx={{ m: 0 }}>
135
                                    <Typography
136
                                        variant="h6"
137
                                        fontWeight="bold"
138
                                        fontSize={16}
139
                                    >
140
                                        {formatHtmlStringToReactDom(
141
                                            item.catalogItem.name as string
142
                                        )}
143
                                    </Typography>
144
                                    <FormControlLabel
145
                                        control={
146
                                            <Checkbox
147
                                                checked={item.active}
148
                                                onChange={() => {
149
                                                    dispatch(
150
                                                        updateMapMarker({
151
                                                            idx,
152
                                                            item: new MapPoint(
153
                                                                item.idx,
154
                                                                !item.active,
155
                                                                item.catalogItem
156
                                                            ),
157
                                                        })
158
                                                    )
159
                                                }}
160
                                            />
161
                                        }
162
                                        labelPlacement="end"
163
                                        label="Active"
164
                                    />
165
                                    <CatalogItemDetailDialog
166
                                        itemId={item.catalogItem.id ?? ""}
167
                                    />
168
                                </Stack>
169
                            </Fragment>
170
                        </Popup>
171
                    </Fragment>
165 172
                </MapMarker>
166 173
            ))
167 174
        )
frontend/src/features/TrackingTool/PlaintextUpload.tsx
10 10
import { Fragment, FunctionComponent, useEffect, useState } from 'react'
11 11
import SendIcon from '@mui/icons-material/Send'
12 12
import ClearIcon from '@mui/icons-material/Clear'
13
import { PathDto } from '../../swagger/data-contracts'
14
import axiosInstance from '../../api/api'
15 13
import { useDispatch, useSelector } from 'react-redux'
16
import { showNotification } from '../Notification/notificationSlice'
17 14
import { RootState } from '../redux/store'
18 15
import { sendTextForProcessing } from './trackingToolThunks'
19 16
import * as yup from 'yup'
frontend/src/features/TrackingTool/TrackingTool.tsx
1 1
import {
2
  Button,
3
  Card,
4
  CardContent,
5
  Grid,
6
  Stack,
7
  Typography,
2
    Button,
3
    Card,
4
    CardContent,
5
    Grid,
6
    Stack,
7
    Typography,
8 8
} from "@mui/material"
9
import { Fragment, useEffect, useState } from "react"
9
import { Fragment, useEffect, useRef, useState } from "react"
10 10
import { MapContainer, TileLayer, useMap } from "react-leaflet"
11 11
import mapConfig from "../../config/mapConfig"
12 12
import TextPath from "react-leaflet-textpath"
13 13
import PlaintextUpload from "./PlaintextUpload"
14 14
import FileUpload from "./FileUpload"
15
import L from "leaflet"
15
import L, { Map } from "leaflet"
16 16
import DeleteIcon from "@mui/icons-material/Delete"
17 17
import { PathDto } from "../../swagger/data-contracts"
18 18
import { formatHtmlStringToReactDom } from "../../utils/formatting/HtmlUtils"
......
26 26

  
27 27
// Page with tracking tool
28 28
const TrackingTool = () => {
29
  // Path response from the API
30
  const pathDto = useSelector((state: RootState) => state.trackingTool.pathDto)
31
  const pathVariants = useSelector(
32
    (state: RootState) => state.trackingTool.pathVariants
33
  )
34
  const mapCenter = useSelector(
35
    (state: RootState) => state.trackingTool.mapCenter
36
  )
29
    const dispatch = useDispatch()
37 30

  
38
  // Consume any error
39
  const err = useSelector((state: RootState) => state.trackingTool.lastError)
40
  const dispatch = useDispatch()
41

  
42
  useEffect(() => {
43
    if (!err) {
44
      return
45
    }
46
    const error = `${err}`
47
    dispatch(consumeError())
48
    dispatch(
49
      showNotification({
50
        message: error,
51
        severity: "error",
52
      })
31
    // Path response from the API
32
    const pathDto = useSelector(
33
        (state: RootState) => state.trackingTool.pathDto
34
    )
35
    const pathVariants = useSelector(
36
        (state: RootState) => state.trackingTool.pathVariants
53 37
    )
54
  }, [err, dispatch])
38
    const mapCenter = useSelector(
39
        (state: RootState) => state.trackingTool.mapCenter
40
    )
41

  
42
    // Consume any error
43
    const err = useSelector((state: RootState) => state.trackingTool.lastError)
44
    useEffect(() => {
45
        if (!err) {
46
            return
47
        }
48
        const error = `${err}`
49
        dispatch(consumeError())
50
        dispatch(
51
            showNotification({
52
                message: error,
53
                severity: "error",
54
            })
55
        )
56
    }, [err, dispatch])
55 57

  
56
  return (
57
    <Fragment>
58
      <Typography variant="h3" sx={{ mb: 2 }} fontWeight="bold">
59
        Tracking Tool
60
      </Typography>
58
    const mapRef = useRef<Map | undefined>(undefined)
59
    useEffect(() => {
60
        if (!mapRef || !mapRef.current) {
61
            console.log("No map ref")
62
            return
63
        }
61 64

  
62
      <Grid container>
63
        <Grid item xs={12}>
64
          {pathDto && pathDto?.foundCatalogItems?.length === 0 && (
65
            <Typography
66
              variant="body1"
67
              sx={{ mb: 2 }}
68
              fontWeight="500"
69
              align="center"
70
              color="error.main"
71
            >
72
              Looks like no path / catalog items match this query.
65
        const map = mapRef.current
66
        map.setView(mapCenter, mapConfig.defaultZoom, {
67
            animate: true,
68
        })
69
    }, [mapCenter, mapRef])
70

  
71
    return (
72
        <Fragment>
73
            <Typography variant="h3" sx={{ mb: 2 }} fontWeight="bold">
74
                Tracking Tool
73 75
            </Typography>
74
          )}
75
          {!pathDto && (
76
            <Stack
77
              direction="row"
78
              alignItems="flex-start"
79
              spacing={2}
80
              sx={{ mt: 1 }}
81
            >
82
              <Typography variant="h5" sx={{ mb: 2 }} fontWeight="500">
83
                Upload:
84
              </Typography>
85
              <PlaintextUpload />
86
              <FileUpload />
87
            </Stack>
88
          )}
89 76

  
90
          {pathDto && (
91
            <Stack alignItems="flex-end">
92
              <Button
93
                startIcon={<ClearIcon />}
94
                sx={{ mb: 1 }}
95
                variant="contained"
96
                onClick={() => dispatch(clear())}
97
              >
98
                Clear Map
99
              </Button>
100
            </Stack>
101
          )}
102
        </Grid>
77
            <Grid container>
78
                <Grid item xs={12}>
79
                    {pathDto && pathDto?.foundCatalogItems?.length === 0 && (
80
                        <Typography
81
                            variant="body1"
82
                            sx={{ mb: 2 }}
83
                            fontWeight="500"
84
                            align="center"
85
                            color="error.main"
86
                        >
87
                            Looks like no path / catalog items match this query.
88
                        </Typography>
89
                    )}
90
                    {!pathDto && (
91
                        <Stack
92
                            direction="row"
93
                            alignItems="flex-start"
94
                            spacing={2}
95
                            sx={{ mt: 1 }}
96
                        >
97
                            <Typography
98
                                variant="h5"
99
                                sx={{ mb: 2 }}
100
                                fontWeight="500"
101
                            >
102
                                Upload:
103
                            </Typography>
104
                            <PlaintextUpload />
105
                            <FileUpload />
106
                        </Stack>
107
                    )}
108

  
109
                    {pathDto && (
110
                        <Stack alignItems="flex-end">
111
                            <Button
112
                                startIcon={<ClearIcon />}
113
                                sx={{ mb: 1 }}
114
                                variant="contained"
115
                                onClick={() => dispatch(clear())}
116
                            >
117
                                Clear Map
118
                            </Button>
119
                        </Stack>
120
                    )}
121
                </Grid>
103 122

  
104
        <Grid
105
          item
106
          xs={12}
107
          md={12}
108
          style={{
109
            minHeight: "60vh",
110
            maxHeight: "100vh",
111
            width: "100%",
112
          }}
113
        >
114
          <MapContainer
115
            center={[mapCenter[0], mapCenter[1]]}
116
            zoom={mapConfig.defaultZoom}
117
            style={{ height: "100%", minHeight: "100%" }}
118
          >
119
            <TileLayer
120
              attribution={mapConfig.attribution}
121
              url={mapConfig.url}
122
            />
123
            {pathVariants?.map((pathVariant, idx) => (
124
              <MapPath idx={idx} />
125
            ))}
126
          </MapContainer>
127
          {pathDto && (
128
            <Fragment>
129
              <Card variant="outlined" sx={{ mt: 2 }}>
130
                <CardContent>
131
                  <Stack direction="column">
132
                    <Typography variant="h5" sx={{ mb: 1 }} fontWeight="600">
133
                      Processed Text
134
                    </Typography>
135
                    <Typography variant="body2">
136
                      {formatHtmlStringToReactDom(pathDto.text ?? "")}
137
                    </Typography>
138
                  </Stack>
139
                </CardContent>
140
              </Card>
141
            </Fragment>
142
          )}
143
        </Grid>
144
      </Grid>
145
    </Fragment>
146
  )
123
                <Grid
124
                    item
125
                    xs={12}
126
                    md={12}
127
                    style={{
128
                        minHeight: "60vh",
129
                        maxHeight: "100vh",
130
                        width: "100%",
131
                    }}
132
                >
133
                    <MapContainer
134
                        center={[mapCenter[0], mapCenter[1]]}
135
                        zoom={mapConfig.defaultZoom}
136
                        style={{ height: "100%", minHeight: "100%" }}
137
                        whenCreated={(map) => { mapRef.current = map }}
138
                    >
139
                        <TileLayer
140
                            attribution={mapConfig.attribution}
141
                            url={mapConfig.url}
142
                        />
143
                        {pathVariants?.map((pathVariant, idx) => (
144
                            <MapPath idx={idx} />
145
                        ))}
146
                    </MapContainer>
147
                    {pathDto && (
148
                        <Fragment>
149
                            <Card variant="outlined" sx={{ mt: 2 }}>
150
                                <CardContent>
151
                                    <Stack direction="column">
152
                                        <Typography
153
                                            variant="h5"
154
                                            sx={{ mb: 1 }}
155
                                            fontWeight="600"
156
                                        >
157
                                            Processed Text
158
                                        </Typography>
159
                                        <Typography variant="body2">
160
                                            {formatHtmlStringToReactDom(
161
                                                pathDto.text ?? ""
162
                                            )}
163
                                        </Typography>
164
                                    </Stack>
165
                                </CardContent>
166
                            </Card>
167
                        </Fragment>
168
                    )}
169
                </Grid>
170
            </Grid>
171
        </Fragment>
172
    )
147 173
}
148 174

  
149 175
export default TrackingTool
frontend/src/features/TrackingTool/trackingToolSlice.ts
31 31
    currentPage: 0,
32 32
}
33 33

  
34
// Returns tuple of average latitude and longitude
35
const calculateMapCenter = (pathVariant: PathVariant): LatLngTuple => [
36
    pathVariant
37
        .map((item) => item.catalogItem.latitude ?? 0)
38
        .reduce((a, b) => a + b, 0) / pathVariant.length,
39
    pathVariant
40
        .map((item) => item.catalogItem.longitude ?? 0)
41
        .reduce((a, b) => a + b, 0) / pathVariant.length,
42
]
34
const calculateMapCenter = (pathVariant: PathVariant): LatLngTuple | undefined => {
35
    const displayableItems = pathVariant.filter((item) => item.displayable)
36
    if (displayableItems.length === 0) {
37
        return undefined
38
    }
39

  
40
    return [
41
        displayableItems
42
            .map((item) => item.catalogItem.latitude ?? 0)
43
            .reduce((a, b) => a + b, 0) / displayableItems.length,
44
        displayableItems
45
            .map((item) => item.catalogItem.longitude ?? 0)
46
            .reduce((a, b) => a + b, 0) / displayableItems.length,
47
    ]
48
}
43 49

  
44 50
const persistConfig = {
45 51
    key: "auth",
......
93 99
        builder.addCase(sendTextForProcessing.fulfilled, (state, action) => {
94 100
            const pathDto: PathDto = action.payload
95 101
            const pathVariants = buildPathVariants(pathDto)
102

  
103
            const mapCenter = calculateMapCenter(pathVariants[state.primaryPathIdx])
96 104
            return {
97 105
                ...state,
98 106
                pathVariants,
99 107
                pathDto,
100
                // TODO calculate correctly
101
                mapCenter:
102
                    pathVariants.length > 0
103
                        ? calculateMapCenter(pathVariants[0])
104
                        : (state.mapCenter as LatLngTuple),
108
                mapCenter: mapCenter ?? state.mapCenter,
105 109
                isLoading: false,
106 110
                dialogApiCallSuccess: true,
107 111
                currentPage: 0,

Také k dispozici: Unified diff