commit 11fca75adb663b47df834765038a9f5c196387da
Author: Vaclav Honzik <vaclavhonzik98@gmail.com>
Date:   Mon May 16 17:25:29 2022 +0200

    autozoom + file upload
    
    re #9741

diff --git a/frontend/src/config/mapConfig.ts b/frontend/src/config/mapConfig.ts
index 844bde6..af4123d 100644
--- a/frontend/src/config/mapConfig.ts
+++ b/frontend/src/config/mapConfig.ts
@@ -11,7 +11,7 @@ const mapConfig: MapConfig = {
     attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
     url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
     defaultCoordinates: [33.5138, 36.2765], // Damascus, Syria
-    defaultZoom: 8
+    defaultZoom: 6
 }
 
 export default mapConfig
diff --git a/frontend/src/features/TrackingTool/FileUpload.tsx b/frontend/src/features/TrackingTool/FileUpload.tsx
index 4e3ef2c..385a848 100644
--- a/frontend/src/features/TrackingTool/FileUpload.tsx
+++ b/frontend/src/features/TrackingTool/FileUpload.tsx
@@ -5,30 +5,45 @@ import {
     Link,
     Stack,
     Typography,
-} from '@mui/material'
-import { useFormik } from 'formik'
-import { Fragment, useState } from 'react'
-import * as yup from 'yup'
-import axiosInstance from '../../api/api'
-import ButtonOpenableDialog from '../Reusables/ButtonOpenableDialog'
-import AttachmentIcon from '@mui/icons-material/Attachment'
-import DeleteIcon from '@mui/icons-material/Delete'
-import SendIcon from '@mui/icons-material/Send'
+} from "@mui/material"
+import { useFormik } from "formik"
+import { Fragment, useState } from "react"
+import * as yup from "yup"
+import axiosInstance from "../../api/api"
+import ButtonOpenableDialog from "../Reusables/ButtonOpenableDialog"
+import AttachmentIcon from "@mui/icons-material/Attachment"
+import DeleteIcon from "@mui/icons-material/Delete"
+import SendIcon from "@mui/icons-material/Send"
+import { useDispatch } from "react-redux"
+import { sendTextForProcessing } from "./trackingToolThunks"
+
+interface UploadValues {
+    file?: File
+}
+
+const initialValues: UploadValues = {}
 
 const FileUpload = () => {
+    const dispatch = useDispatch()
+
     const [filename, setFilename] = useState<string | undefined>(undefined)
+    const [fileProcessing, setFileProcessing] = useState(false)
 
     const validationSchema = yup.object().shape({
-        file: yup.mixed().required('File is required'),
+        file: yup.mixed().required("File is required"),
     })
 
     const formik = useFormik({
-        initialValues: {
-            file: undefined,
-        },
+        initialValues,
         validationSchema,
         onSubmit: async (values) => {
-            
+            setFileProcessing(true)
+            const reader = new FileReader()
+            reader.readAsText(values.file as File)
+            reader.onload = async () => {
+                dispatch(sendTextForProcessing(reader.result as string))
+                setFileProcessing(false)
+            }
         },
     })
 
@@ -37,18 +52,21 @@ const FileUpload = () => {
         const file = event.currentTarget.files[0]
         if (file) {
             setFilename(file.name)
-            formik.setFieldValue('file', file)
+            formik.setFieldValue("file", file)
         }
     }
 
     const onClose = () => {
+        if (fileProcessing) {
+            return
+        }
         setFilename(undefined)
         formik.resetForm()
     }
 
     const onClearSelectedFile = () => {
         setFilename(undefined)
-        formik.setFieldValue('file', undefined)
+        formik.setFieldValue("file", undefined)
     }
 
     return (
@@ -91,18 +109,20 @@ const FileUpload = () => {
                         <Fragment>
                             <Stack direction="row" spacing={1}>
                                 <Typography
-                                    sx={{
-                                        // textOverflow: 'ellipsis',
-                                        // overflow: 'hidden',
-                                    }}
+                                    sx={
+                                        {
+                                            // textOverflow: 'ellipsis',
+                                            // overflow: 'hidden',
+                                        }
+                                    }
                                     variant="body1"
                                 >
-                                    Selected File:{' '}
+                                    Selected File:{" "}
                                 </Typography>
                                 <Typography
                                     sx={{
-                                        textOverflow: 'ellipsis',
-                                        overflow: 'hidden',
+                                        textOverflow: "ellipsis",
+                                        overflow: "hidden",
                                     }}
                                     // color="text.secondary"
                                     component={Link}
@@ -117,7 +137,7 @@ const FileUpload = () => {
                                 justifyContent="flex-end"
                                 alignItems="center"
                                 spacing={2}
-                                sx={{mt: 2}}
+                                sx={{ mt: 2 }}
                             >
                                 <Button
                                     // sx={{ mb: 2, mt: 1 }}
@@ -128,12 +148,15 @@ const FileUpload = () => {
                                 >
                                     Remove Selection
                                 </Button>
-                                <Button size="small" type="submit" variant="contained" startIcon={<SendIcon />}>
-                                Submit
-                            </Button>
+                                <Button
+                                    size="small"
+                                    type="submit"
+                                    variant="contained"
+                                    startIcon={<SendIcon />}
+                                >
+                                    Submit
+                                </Button>
                             </Stack>
-                            
-                            
                         </Fragment>
                     )}
                 </form>
diff --git a/frontend/src/features/TrackingTool/MapPath.tsx b/frontend/src/features/TrackingTool/MapPath.tsx
index e1f3bf8..354da85 100644
--- a/frontend/src/features/TrackingTool/MapPath.tsx
+++ b/frontend/src/features/TrackingTool/MapPath.tsx
@@ -6,7 +6,7 @@ import TextPath from "react-leaflet-textpath"
 import { setPrimaryIdx, updateMapMarker } from "./trackingToolSlice"
 import MapMarker from "./MapMarker"
 import { LatLngTuple } from "leaflet"
-import { Popup } from "react-leaflet"
+import { Popup, Tooltip } from "react-leaflet"
 import { Checkbox, FormControlLabel, Stack, Typography } from "@mui/material"
 import { formatHtmlStringToReactDom } from "../../utils/formatting/HtmlUtils"
 import { DialogCatalogItemDetail as CatalogItemDetailDialog } from "../Catalog/CatalogItemDetail"
@@ -123,45 +123,52 @@ const MapPath: FunctionComponent<MapPathProps> = ({ idx }) => {
                         )
                     }}
                 >
-                    <Popup>
-                        <Fragment>
-                            <Stack direction="column" sx={{ m: 0 }}>
-                                <Typography
-                                    variant="h6"
-                                    fontWeight="bold"
-                                    fontSize={16}
-                                >
-                                    {formatHtmlStringToReactDom(
-                                        item.catalogItem.name as string
-                                    )}
-                                </Typography>
-                                <FormControlLabel
-                                    control={
-                                        <Checkbox
-                                            checked={item.active}
-                                            onChange={() => {
-                                                dispatch(
-                                                    updateMapMarker({
-                                                        idx,
-                                                        item: new MapPoint(
-                                                            item.idx,
-                                                            !item.active,
-                                                            item.catalogItem
-                                                        ),
-                                                    })
-                                                )
-                                            }}
-                                        />
-                                    }
-                                    labelPlacement="end"
-                                    label="Active"
-                                />
-                                <CatalogItemDetailDialog
-                                    itemId={item.catalogItem.id ?? ""}
-                                />
-                            </Stack>
-                        </Fragment>
-                    </Popup>
+                    <Fragment>
+                        <Tooltip>
+                            {/* <Typography> */}
+                                {item.catalogItem.name ?? ""}
+                            {/* </Typography> */}
+                        </Tooltip>
+                        <Popup>
+                            <Fragment>
+                                <Stack direction="column" sx={{ m: 0 }}>
+                                    <Typography
+                                        variant="h6"
+                                        fontWeight="bold"
+                                        fontSize={16}
+                                    >
+                                        {formatHtmlStringToReactDom(
+                                            item.catalogItem.name as string
+                                        )}
+                                    </Typography>
+                                    <FormControlLabel
+                                        control={
+                                            <Checkbox
+                                                checked={item.active}
+                                                onChange={() => {
+                                                    dispatch(
+                                                        updateMapMarker({
+                                                            idx,
+                                                            item: new MapPoint(
+                                                                item.idx,
+                                                                !item.active,
+                                                                item.catalogItem
+                                                            ),
+                                                        })
+                                                    )
+                                                }}
+                                            />
+                                        }
+                                        labelPlacement="end"
+                                        label="Active"
+                                    />
+                                    <CatalogItemDetailDialog
+                                        itemId={item.catalogItem.id ?? ""}
+                                    />
+                                </Stack>
+                            </Fragment>
+                        </Popup>
+                    </Fragment>
                 </MapMarker>
             ))
         )
diff --git a/frontend/src/features/TrackingTool/PlaintextUpload.tsx b/frontend/src/features/TrackingTool/PlaintextUpload.tsx
index 1e05016..ae19978 100644
--- a/frontend/src/features/TrackingTool/PlaintextUpload.tsx
+++ b/frontend/src/features/TrackingTool/PlaintextUpload.tsx
@@ -10,10 +10,7 @@ import { useFormik } from 'formik'
 import { Fragment, FunctionComponent, useEffect, useState } from 'react'
 import SendIcon from '@mui/icons-material/Send'
 import ClearIcon from '@mui/icons-material/Clear'
-import { PathDto } from '../../swagger/data-contracts'
-import axiosInstance from '../../api/api'
 import { useDispatch, useSelector } from 'react-redux'
-import { showNotification } from '../Notification/notificationSlice'
 import { RootState } from '../redux/store'
 import { sendTextForProcessing } from './trackingToolThunks'
 import * as yup from 'yup'
diff --git a/frontend/src/features/TrackingTool/TrackingTool.tsx b/frontend/src/features/TrackingTool/TrackingTool.tsx
index f096c8c..3e1e275 100644
--- a/frontend/src/features/TrackingTool/TrackingTool.tsx
+++ b/frontend/src/features/TrackingTool/TrackingTool.tsx
@@ -1,18 +1,18 @@
 import {
-  Button,
-  Card,
-  CardContent,
-  Grid,
-  Stack,
-  Typography,
+    Button,
+    Card,
+    CardContent,
+    Grid,
+    Stack,
+    Typography,
 } from "@mui/material"
-import { Fragment, useEffect, useState } from "react"
+import { Fragment, useEffect, useRef, useState } from "react"
 import { MapContainer, TileLayer, useMap } from "react-leaflet"
 import mapConfig from "../../config/mapConfig"
 import TextPath from "react-leaflet-textpath"
 import PlaintextUpload from "./PlaintextUpload"
 import FileUpload from "./FileUpload"
-import L from "leaflet"
+import L, { Map } from "leaflet"
 import DeleteIcon from "@mui/icons-material/Delete"
 import { PathDto } from "../../swagger/data-contracts"
 import { formatHtmlStringToReactDom } from "../../utils/formatting/HtmlUtils"
@@ -26,124 +26,150 @@ import ClearIcon from "@mui/icons-material/Clear"
 
 // Page with tracking tool
 const TrackingTool = () => {
-  // Path response from the API
-  const pathDto = useSelector((state: RootState) => state.trackingTool.pathDto)
-  const pathVariants = useSelector(
-    (state: RootState) => state.trackingTool.pathVariants
-  )
-  const mapCenter = useSelector(
-    (state: RootState) => state.trackingTool.mapCenter
-  )
+    const dispatch = useDispatch()
 
-  // Consume any error
-  const err = useSelector((state: RootState) => state.trackingTool.lastError)
-  const dispatch = useDispatch()
-
-  useEffect(() => {
-    if (!err) {
-      return
-    }
-    const error = `${err}`
-    dispatch(consumeError())
-    dispatch(
-      showNotification({
-        message: error,
-        severity: "error",
-      })
+    // Path response from the API
+    const pathDto = useSelector(
+        (state: RootState) => state.trackingTool.pathDto
+    )
+    const pathVariants = useSelector(
+        (state: RootState) => state.trackingTool.pathVariants
     )
-  }, [err, dispatch])
+    const mapCenter = useSelector(
+        (state: RootState) => state.trackingTool.mapCenter
+    )
+
+    // Consume any error
+    const err = useSelector((state: RootState) => state.trackingTool.lastError)
+    useEffect(() => {
+        if (!err) {
+            return
+        }
+        const error = `${err}`
+        dispatch(consumeError())
+        dispatch(
+            showNotification({
+                message: error,
+                severity: "error",
+            })
+        )
+    }, [err, dispatch])
 
-  return (
-    <Fragment>
-      <Typography variant="h3" sx={{ mb: 2 }} fontWeight="bold">
-        Tracking Tool
-      </Typography>
+    const mapRef = useRef<Map | undefined>(undefined)
+    useEffect(() => {
+        if (!mapRef || !mapRef.current) {
+            console.log("No map ref")
+            return
+        }
 
-      <Grid container>
-        <Grid item xs={12}>
-          {pathDto && pathDto?.foundCatalogItems?.length === 0 && (
-            <Typography
-              variant="body1"
-              sx={{ mb: 2 }}
-              fontWeight="500"
-              align="center"
-              color="error.main"
-            >
-              Looks like no path / catalog items match this query.
+        const map = mapRef.current
+        map.setView(mapCenter, mapConfig.defaultZoom, {
+            animate: true,
+        })
+    }, [mapCenter, mapRef])
+
+    return (
+        <Fragment>
+            <Typography variant="h3" sx={{ mb: 2 }} fontWeight="bold">
+                Tracking Tool
             </Typography>
-          )}
-          {!pathDto && (
-            <Stack
-              direction="row"
-              alignItems="flex-start"
-              spacing={2}
-              sx={{ mt: 1 }}
-            >
-              <Typography variant="h5" sx={{ mb: 2 }} fontWeight="500">
-                Upload:
-              </Typography>
-              <PlaintextUpload />
-              <FileUpload />
-            </Stack>
-          )}
 
-          {pathDto && (
-            <Stack alignItems="flex-end">
-              <Button
-                startIcon={<ClearIcon />}
-                sx={{ mb: 1 }}
-                variant="contained"
-                onClick={() => dispatch(clear())}
-              >
-                Clear Map
-              </Button>
-            </Stack>
-          )}
-        </Grid>
+            <Grid container>
+                <Grid item xs={12}>
+                    {pathDto && pathDto?.foundCatalogItems?.length === 0 && (
+                        <Typography
+                            variant="body1"
+                            sx={{ mb: 2 }}
+                            fontWeight="500"
+                            align="center"
+                            color="error.main"
+                        >
+                            Looks like no path / catalog items match this query.
+                        </Typography>
+                    )}
+                    {!pathDto && (
+                        <Stack
+                            direction="row"
+                            alignItems="flex-start"
+                            spacing={2}
+                            sx={{ mt: 1 }}
+                        >
+                            <Typography
+                                variant="h5"
+                                sx={{ mb: 2 }}
+                                fontWeight="500"
+                            >
+                                Upload:
+                            </Typography>
+                            <PlaintextUpload />
+                            <FileUpload />
+                        </Stack>
+                    )}
+
+                    {pathDto && (
+                        <Stack alignItems="flex-end">
+                            <Button
+                                startIcon={<ClearIcon />}
+                                sx={{ mb: 1 }}
+                                variant="contained"
+                                onClick={() => dispatch(clear())}
+                            >
+                                Clear Map
+                            </Button>
+                        </Stack>
+                    )}
+                </Grid>
 
-        <Grid
-          item
-          xs={12}
-          md={12}
-          style={{
-            minHeight: "60vh",
-            maxHeight: "100vh",
-            width: "100%",
-          }}
-        >
-          <MapContainer
-            center={[mapCenter[0], mapCenter[1]]}
-            zoom={mapConfig.defaultZoom}
-            style={{ height: "100%", minHeight: "100%" }}
-          >
-            <TileLayer
-              attribution={mapConfig.attribution}
-              url={mapConfig.url}
-            />
-            {pathVariants?.map((pathVariant, idx) => (
-              <MapPath idx={idx} />
-            ))}
-          </MapContainer>
-          {pathDto && (
-            <Fragment>
-              <Card variant="outlined" sx={{ mt: 2 }}>
-                <CardContent>
-                  <Stack direction="column">
-                    <Typography variant="h5" sx={{ mb: 1 }} fontWeight="600">
-                      Processed Text
-                    </Typography>
-                    <Typography variant="body2">
-                      {formatHtmlStringToReactDom(pathDto.text ?? "")}
-                    </Typography>
-                  </Stack>
-                </CardContent>
-              </Card>
-            </Fragment>
-          )}
-        </Grid>
-      </Grid>
-    </Fragment>
-  )
+                <Grid
+                    item
+                    xs={12}
+                    md={12}
+                    style={{
+                        minHeight: "60vh",
+                        maxHeight: "100vh",
+                        width: "100%",
+                    }}
+                >
+                    <MapContainer
+                        center={[mapCenter[0], mapCenter[1]]}
+                        zoom={mapConfig.defaultZoom}
+                        style={{ height: "100%", minHeight: "100%" }}
+                        whenCreated={(map) => { mapRef.current = map }}
+                    >
+                        <TileLayer
+                            attribution={mapConfig.attribution}
+                            url={mapConfig.url}
+                        />
+                        {pathVariants?.map((pathVariant, idx) => (
+                            <MapPath idx={idx} />
+                        ))}
+                    </MapContainer>
+                    {pathDto && (
+                        <Fragment>
+                            <Card variant="outlined" sx={{ mt: 2 }}>
+                                <CardContent>
+                                    <Stack direction="column">
+                                        <Typography
+                                            variant="h5"
+                                            sx={{ mb: 1 }}
+                                            fontWeight="600"
+                                        >
+                                            Processed Text
+                                        </Typography>
+                                        <Typography variant="body2">
+                                            {formatHtmlStringToReactDom(
+                                                pathDto.text ?? ""
+                                            )}
+                                        </Typography>
+                                    </Stack>
+                                </CardContent>
+                            </Card>
+                        </Fragment>
+                    )}
+                </Grid>
+            </Grid>
+        </Fragment>
+    )
 }
 
 export default TrackingTool
diff --git a/frontend/src/features/TrackingTool/trackingToolSlice.ts b/frontend/src/features/TrackingTool/trackingToolSlice.ts
index d81a8d0..4486635 100644
--- a/frontend/src/features/TrackingTool/trackingToolSlice.ts
+++ b/frontend/src/features/TrackingTool/trackingToolSlice.ts
@@ -31,15 +31,21 @@ const initialState: TrackingToolState = {
     currentPage: 0,
 }
 
-// Returns tuple of average latitude and longitude
-const calculateMapCenter = (pathVariant: PathVariant): LatLngTuple => [
-    pathVariant
-        .map((item) => item.catalogItem.latitude ?? 0)
-        .reduce((a, b) => a + b, 0) / pathVariant.length,
-    pathVariant
-        .map((item) => item.catalogItem.longitude ?? 0)
-        .reduce((a, b) => a + b, 0) / pathVariant.length,
-]
+const calculateMapCenter = (pathVariant: PathVariant): LatLngTuple | undefined => {
+    const displayableItems = pathVariant.filter((item) => item.displayable)
+    if (displayableItems.length === 0) {
+        return undefined
+    }
+
+    return [
+        displayableItems
+            .map((item) => item.catalogItem.latitude ?? 0)
+            .reduce((a, b) => a + b, 0) / displayableItems.length,
+        displayableItems
+            .map((item) => item.catalogItem.longitude ?? 0)
+            .reduce((a, b) => a + b, 0) / displayableItems.length,
+    ]
+}
 
 const persistConfig = {
     key: "auth",
@@ -93,15 +99,13 @@ export const trackingToolSlice = createSlice({
         builder.addCase(sendTextForProcessing.fulfilled, (state, action) => {
             const pathDto: PathDto = action.payload
             const pathVariants = buildPathVariants(pathDto)
+
+            const mapCenter = calculateMapCenter(pathVariants[state.primaryPathIdx])
             return {
                 ...state,
                 pathVariants,
                 pathDto,
-                // TODO calculate correctly
-                mapCenter:
-                    pathVariants.length > 0
-                        ? calculateMapCenter(pathVariants[0])
-                        : (state.mapCenter as LatLngTuple),
+                mapCenter: mapCenter ?? state.mapCenter,
                 isLoading: false,
                 dialogApiCallSuccess: true,
                 currentPage: 0,
