ag visual farm added to the dev branch field related stuff not imported yet, and the geocoder has some bugs with the new version
This commit is contained in:
parent
b4da0d6859
commit
022925d7d7
41 changed files with 8895 additions and 79 deletions
1325
package-lock.json
generated
1325
package-lock.json
generated
File diff suppressed because it is too large
Load diff
10
package.json
10
package.json
|
|
@ -23,6 +23,8 @@
|
||||||
"@date-io/moment": "^3.2.0",
|
"@date-io/moment": "^3.2.0",
|
||||||
"@emotion/react": "^11.13.3",
|
"@emotion/react": "^11.13.3",
|
||||||
"@emotion/styled": "^11.13.0",
|
"@emotion/styled": "^11.13.0",
|
||||||
|
"@mapbox/mapbox-gl-draw": "^1.5.0",
|
||||||
|
"@mapbox/mapbox-gl-geocoder": "^5.0.3",
|
||||||
"@mui/icons-material": "^6.1.6",
|
"@mui/icons-material": "^6.1.6",
|
||||||
"@mui/material": "^6.1.6",
|
"@mui/material": "^6.1.6",
|
||||||
"@mui/styles": "^6.1.6",
|
"@mui/styles": "^6.1.6",
|
||||||
|
|
@ -31,16 +33,19 @@
|
||||||
"@turf/area": "^7.2.0",
|
"@turf/area": "^7.2.0",
|
||||||
"@turf/turf": "^7.2.0",
|
"@turf/turf": "^7.2.0",
|
||||||
"@types/classnames": "^2.3.0",
|
"@types/classnames": "^2.3.0",
|
||||||
|
"@types/mapbox-gl": "^3.4.1",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"deep-object-diff": "^1.1.9",
|
"deep-object-diff": "^1.1.9",
|
||||||
"geojson": "^0.5.0",
|
"geojson": "^0.5.0",
|
||||||
"json-2-csv": "^5.5.8",
|
"json-2-csv": "^5.5.8",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"mapbox-gl": "^3.10.0",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"moment-timezone": "^0.5.46",
|
"moment-timezone": "^0.5.46",
|
||||||
"mui-tel-input": "^7.0.0",
|
"mui-tel-input": "^7.0.0",
|
||||||
"notistack": "^3.0.1",
|
"notistack": "^3.0.1",
|
||||||
|
"openweathermap-ts": "^1.2.10",
|
||||||
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#dev",
|
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#dev",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-beautiful-dnd": "^13.1.1",
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
|
|
@ -56,13 +61,16 @@
|
||||||
"react-router-dom": "^6.28.0",
|
"react-router-dom": "^6.28.0",
|
||||||
"react-virtualized-auto-sizer": "^1.0.25",
|
"react-virtualized-auto-sizer": "^1.0.25",
|
||||||
"recharts": "^2.15.1",
|
"recharts": "^2.15.1",
|
||||||
"victory": "^37.3.6"
|
"victory": "^37.3.6",
|
||||||
|
"weather-icons-react": "^1.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/types": "^7.25.8",
|
"@babel/types": "^7.25.8",
|
||||||
"@eslint/js": "^9.11.1",
|
"@eslint/js": "^9.11.1",
|
||||||
"@types/jsonexport": "^3.0.5",
|
"@types/jsonexport": "^3.0.5",
|
||||||
"@types/lodash": "^4.17.13",
|
"@types/lodash": "^4.17.13",
|
||||||
|
"@types/mapbox__mapbox-gl-draw": "^1.4.8",
|
||||||
|
"@types/mapbox__mapbox-gl-geocoder": "^5.0.0",
|
||||||
"@types/react": "^18.3.10",
|
"@types/react": "^18.3.10",
|
||||||
"@types/react-beautiful-dnd": "^13.1.8",
|
"@types/react-beautiful-dnd": "^13.1.8",
|
||||||
"@types/react-color": "^3.0.13",
|
"@types/react-color": "^3.0.13",
|
||||||
|
|
|
||||||
833
src/bin/BinyardDisplay.tsx
Normal file
833
src/bin/BinyardDisplay.tsx
Normal file
|
|
@ -0,0 +1,833 @@
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionDetails,
|
||||||
|
AccordionSummary,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
Chip,
|
||||||
|
darken,
|
||||||
|
Divider,
|
||||||
|
FormControl,
|
||||||
|
FormControlLabel,
|
||||||
|
Grid2 as Grid,
|
||||||
|
ImageList as GridList,
|
||||||
|
ImageListItem as GridListTile,
|
||||||
|
InputLabel,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
Select,
|
||||||
|
Skeleton,
|
||||||
|
Theme,
|
||||||
|
Typography,
|
||||||
|
useTheme
|
||||||
|
} from "@mui/material";
|
||||||
|
import { green, yellow } from "@mui/material/colors";
|
||||||
|
import { makeStyles } from "@mui/styles";
|
||||||
|
import { ArrowBackIos, ArrowForwardIos, ExpandMore, MoreVert } from "@mui/icons-material";
|
||||||
|
//import { Skeleton } from "@material-ui/lab";
|
||||||
|
import BinInventoryChart, { GrainAmount } from "charts/BinInventoryChart";
|
||||||
|
import BinUtilizationChart from "charts/BinUtilizationChart";
|
||||||
|
//import QrCodeGenerator, { QrCodeKey } from "common/QrCodeGenerator";
|
||||||
|
import GrainDescriber, { grainName } from "grain/GrainDescriber";
|
||||||
|
import { useMobile, useWidth } from "hooks";
|
||||||
|
import { Bin } from "models";
|
||||||
|
import { pond } from "protobuf-ts/pond";
|
||||||
|
import { useBinAPI, useGlobalState } from "providers";
|
||||||
|
import React, { useEffect, useState, useCallback, SetStateAction } from "react";
|
||||||
|
import { getGrainUnit, stringToMaterialColour } from "utils";
|
||||||
|
//import BinsFansStatusTable from "./BinFansStatusTable";
|
||||||
|
import BinsList from "./BinsList";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
yardKey: string;
|
||||||
|
insert?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) => ({
|
||||||
|
"@keyframes pulsate": {
|
||||||
|
to: {
|
||||||
|
boxShadow: "0 0 0 16px" + yellow["500"] + "00"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
green: {
|
||||||
|
color: green["500"],
|
||||||
|
"&:hover": {
|
||||||
|
color: green["600"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
gridList: {
|
||||||
|
width: "100%",
|
||||||
|
flexWrap: "nowrap",
|
||||||
|
transform: "translateZ(0)"
|
||||||
|
},
|
||||||
|
pulse: {
|
||||||
|
boxShadow: "0 0 0 0 " + yellow["500"] + "75",
|
||||||
|
animation: "$pulsate 1.75s infinite cubic-bezier(0.66, 0.33, 0, 1)"
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
padding: 6,
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
borderRadius: "18px",
|
||||||
|
background: "rgba(0,0,0,0)",
|
||||||
|
"&:hover": {
|
||||||
|
background:
|
||||||
|
"radial-gradient(closest-side, rgba(150, 150, 150, 0.5) 50%, rgba(150, 150, 150, 0.5))"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
accordion: {
|
||||||
|
background: darken(theme.palette.background.paper, 0.4)
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function BinyardDisplay(props: Props) {
|
||||||
|
const { yardKey, insert } = props;
|
||||||
|
const classes = useStyles();
|
||||||
|
const binAPI = useBinAPI();
|
||||||
|
const theme = useTheme();
|
||||||
|
const width = useWidth();
|
||||||
|
const isMobile = useMobile();
|
||||||
|
const [carouselIndex, setCarouselIndex] = useState(0);
|
||||||
|
const [binMenuAnchorEl, setBinMenuAnchorEl] = useState<Element | null>(null);
|
||||||
|
const [{ as }] = useGlobalState();
|
||||||
|
const maxBins = 40;
|
||||||
|
const [binsLoading, setBinsLoading] = useState(false);
|
||||||
|
const [expandTotal, setExpandTotal] = useState(false);
|
||||||
|
const [displayedInventory, setDisplayedInventory] = useState<GrainAmount[]>([]);
|
||||||
|
const [displayedFertilizer, setDisplayedFertilizer] = useState<GrainAmount[]>([]);
|
||||||
|
const [expandUtilization, setExpandUtilization] = useState(false);
|
||||||
|
const [expandFanTable, setExpandFanTable] = useState(false);
|
||||||
|
const [displayGrain, setDisplayGrain] = useState(true);
|
||||||
|
const [displayFert, setDisplayFert] = useState(false);
|
||||||
|
const [displayEmpty, setDisplayEmpty] = useState(false);
|
||||||
|
const [yardBins, setYardBins] = useState<Bin[]>([]);
|
||||||
|
const [grainBins, setGrainBins] = useState<Bin[]>([]);
|
||||||
|
const [fertBins, setFertBins] = useState<Bin[]>([]);
|
||||||
|
const [emptyBins, setEmptyBins] = useState<Bin[]>([]);
|
||||||
|
const [binTotal, setBinTotal] = useState(0);
|
||||||
|
const [binMetrics, setBinMetrics] = useState<pond.BinMetrics>();
|
||||||
|
const [contentFilter, setContentFilter] = useState<string>("");
|
||||||
|
const [totalFanControllers, setTotalFanControllers] = useState(0);
|
||||||
|
const [totalFanControllersOn, setTotalFanControllersOn] = useState(0);
|
||||||
|
const [openQrGenerator, setOpenQrGenerator] = useState(false);
|
||||||
|
const [order, setOrder] = useState<"asc" | "desc">("asc");
|
||||||
|
const [orderBy, setOrderBy] = useState<string>("name");
|
||||||
|
|
||||||
|
//const [qrKeyList, setQrKeyList] = useState<QrCodeKey[]>([]);
|
||||||
|
|
||||||
|
const [scrollTranslations, setScrollTranslations] = useState({
|
||||||
|
bins: 0,
|
||||||
|
empty: 0,
|
||||||
|
fertilizer: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadBins = useCallback(() => {
|
||||||
|
setBinsLoading(true);
|
||||||
|
//load the bins based on the yard key
|
||||||
|
binAPI
|
||||||
|
.listBinsAndData(maxBins, 0, "asc", "name", undefined, as, undefined, [yardKey], ["binyard"])
|
||||||
|
.then(resp => {
|
||||||
|
let yardBins: Bin[] = [];
|
||||||
|
let grainBins: Bin[] = [];
|
||||||
|
let fertBins: Bin[] = [];
|
||||||
|
let emptyBins: Bin[] = [];
|
||||||
|
//let qr: QrCodeKey[] = [];
|
||||||
|
//set the list of bins into the yard bins
|
||||||
|
resp.data.bins.forEach(b => {
|
||||||
|
let bin = Bin.any(b);
|
||||||
|
yardBins.push(bin);
|
||||||
|
if (bin.empty()) {
|
||||||
|
emptyBins.push(bin);
|
||||||
|
} else if (bin.storage() === pond.BinStorage.BIN_STORAGE_FERTILIZER) {
|
||||||
|
fertBins.push(bin);
|
||||||
|
} else {
|
||||||
|
grainBins.push(bin);
|
||||||
|
}
|
||||||
|
//build qr keys here
|
||||||
|
// qr.push({
|
||||||
|
// key: bin.key(),
|
||||||
|
// name: bin.name()
|
||||||
|
// });
|
||||||
|
});
|
||||||
|
setYardBins(yardBins);
|
||||||
|
setBinTotal(resp.data.total);
|
||||||
|
//setQrKeyList(qr);
|
||||||
|
setGrainBins(grainBins);
|
||||||
|
if (fertBins.length > 0) {
|
||||||
|
setDisplayFert(true);
|
||||||
|
}
|
||||||
|
if (emptyBins.length > 0) {
|
||||||
|
setDisplayEmpty(true);
|
||||||
|
}
|
||||||
|
setFertBins(fertBins);
|
||||||
|
setEmptyBins(emptyBins);
|
||||||
|
setBinsLoading(false);
|
||||||
|
|
||||||
|
let metrics = pond.BinMetrics.fromObject(resp.data.metrics ?? {});
|
||||||
|
let inventory: GrainAmount[] = [];
|
||||||
|
let fertInventory: GrainAmount[] = [];
|
||||||
|
if (metrics) {
|
||||||
|
metrics.grainInventory.forEach(grain => {
|
||||||
|
inventory.push({
|
||||||
|
grain: grain.grainType,
|
||||||
|
bushelAmount: grain.bushelAmount,
|
||||||
|
grainName: grainName(grain.grainType)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
metrics.customInventory.forEach(invObject => {
|
||||||
|
if (invObject.storageType === pond.BinStorage.BIN_STORAGE_UNSUPPORTED_GRAIN) {
|
||||||
|
inventory.push({
|
||||||
|
grain: pond.Grain.GRAIN_CUSTOM,
|
||||||
|
bushelAmount: invObject.amount,
|
||||||
|
grainName: invObject.name
|
||||||
|
});
|
||||||
|
} else if (invObject.storageType === pond.BinStorage.BIN_STORAGE_FERTILIZER) {
|
||||||
|
fertInventory.push({
|
||||||
|
grain: pond.Grain.GRAIN_CUSTOM,
|
||||||
|
bushelAmount: invObject.amount * 35.239,
|
||||||
|
grainName: invObject.name
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setBinMetrics(metrics);
|
||||||
|
setDisplayedInventory(inventory);
|
||||||
|
setDisplayedFertilizer(fertInventory);
|
||||||
|
});
|
||||||
|
}, [as, binAPI, yardKey]);
|
||||||
|
|
||||||
|
const loadMoreBins = (filter: string, total?: number, offset?: number) => {
|
||||||
|
setBinsLoading(true);
|
||||||
|
binAPI
|
||||||
|
.listBins(
|
||||||
|
total ?? maxBins,
|
||||||
|
offset ?? 0,
|
||||||
|
order,
|
||||||
|
orderBy,
|
||||||
|
filter,
|
||||||
|
as,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
[yardKey],
|
||||||
|
["binyard"]
|
||||||
|
)
|
||||||
|
.then(resp => {
|
||||||
|
let bins = yardBins;
|
||||||
|
//if the offset was 0 or undefined we started from the beginning so replace the yard bins with the new ones
|
||||||
|
if (!offset || offset === 0) {
|
||||||
|
bins = resp.data.bins.map(b => Bin.any(b));
|
||||||
|
//else append the new ones on to the end of the existing yard bins
|
||||||
|
} else {
|
||||||
|
bins = bins.concat(resp.data.bins.map(b => Bin.any(b)));
|
||||||
|
}
|
||||||
|
//loop through the new list of bins setting the bins into the correct sections
|
||||||
|
let grainBins: Bin[] = [];
|
||||||
|
let fertBins: Bin[] = [];
|
||||||
|
let emptyBins: Bin[] = [];
|
||||||
|
//let qr: QrCodeKey[] = [];
|
||||||
|
//set the list of bins into the yard bins
|
||||||
|
resp.data.bins.forEach(b => {
|
||||||
|
let bin = Bin.any(b);
|
||||||
|
yardBins.push(bin);
|
||||||
|
if (bin.empty()) {
|
||||||
|
emptyBins.push(bin);
|
||||||
|
} else if (bin.storage() === pond.BinStorage.BIN_STORAGE_FERTILIZER) {
|
||||||
|
fertBins.push(bin);
|
||||||
|
} else {
|
||||||
|
grainBins.push(bin);
|
||||||
|
}
|
||||||
|
//build qr keys here
|
||||||
|
// qr.push({
|
||||||
|
// key: bin.key(),
|
||||||
|
// name: bin.name()
|
||||||
|
// });
|
||||||
|
});
|
||||||
|
setYardBins(bins);
|
||||||
|
setBinTotal(resp.data.total);
|
||||||
|
//setQrKeyList(qr);
|
||||||
|
setGrainBins(grainBins);
|
||||||
|
if (fertBins.length > 0) {
|
||||||
|
setDisplayFert(true);
|
||||||
|
}
|
||||||
|
if (emptyBins.length > 0) {
|
||||||
|
setDisplayEmpty(true);
|
||||||
|
}
|
||||||
|
setFertBins(fertBins);
|
||||||
|
setEmptyBins(emptyBins);
|
||||||
|
setBinsLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadBins();
|
||||||
|
}, [yardKey, loadBins]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let ebt = sessionStorage.getItem("expandBinTotal");
|
||||||
|
if (ebt === "true") {
|
||||||
|
setExpandTotal(true);
|
||||||
|
} else {
|
||||||
|
setExpandTotal(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
let ebu = sessionStorage.getItem("expandBinUtilization");
|
||||||
|
if (ebu === "true") {
|
||||||
|
setExpandUtilization(true);
|
||||||
|
} else {
|
||||||
|
setExpandUtilization(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
let fanTable = sessionStorage.getItem("expandFanTable");
|
||||||
|
if (fanTable === "true") {
|
||||||
|
setExpandFanTable(true);
|
||||||
|
} else {
|
||||||
|
setExpandFanTable(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const duplicateBin = (bin: Bin) => {
|
||||||
|
binAPI.addBin(bin.settings).then(resp => {
|
||||||
|
loadBins();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const binsContent = () => {
|
||||||
|
if (!binsLoading && binTotal === 0) {
|
||||||
|
return (
|
||||||
|
<Box textAlign="center" marginTop={1}>
|
||||||
|
<Typography variant="subtitle1">No bins found</Typography>
|
||||||
|
<Typography variant="body1" color="textSecondary">
|
||||||
|
Create a bin and it will appear here
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Grid container>
|
||||||
|
{displayGrain && (
|
||||||
|
<React.Fragment>
|
||||||
|
<Grid size={12}>
|
||||||
|
<Divider />
|
||||||
|
</Grid>
|
||||||
|
<Grid size={12}>
|
||||||
|
<BinsList
|
||||||
|
gridView
|
||||||
|
bins={grainBins}
|
||||||
|
duplicateBin={duplicateBin}
|
||||||
|
title={"Grain Bins"}
|
||||||
|
// because this only triggers in scroll view it is no longer necessary as we dont do the scroll here
|
||||||
|
// loadMore={newTranslation => {
|
||||||
|
// //only triggered in the scroll view so this will never trigger in grid view
|
||||||
|
// if (yardBins.length < binTotal) {
|
||||||
|
// loadMoreBins(contentFilter);
|
||||||
|
// setScrollTranslations({ ...scrollTranslations, bins: newTranslation });
|
||||||
|
// }
|
||||||
|
// }}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
{displayFert && (
|
||||||
|
<React.Fragment>
|
||||||
|
<Grid size={12}>
|
||||||
|
<Divider />
|
||||||
|
</Grid>
|
||||||
|
<Grid size={12}>
|
||||||
|
<BinsList
|
||||||
|
gridView
|
||||||
|
bins={fertBins}
|
||||||
|
duplicateBin={duplicateBin}
|
||||||
|
title={"Fertilizer Bins"}
|
||||||
|
// loadMore={newTranslation => {
|
||||||
|
// //only triggered in the scroll view so this will never trigger in grid view
|
||||||
|
// if (yardBins.length < binTotal) {
|
||||||
|
// loadMoreBins(contentFilter);
|
||||||
|
// setScrollTranslations({ ...scrollTranslations, fertilizer: newTranslation });
|
||||||
|
// }
|
||||||
|
// }}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
{displayEmpty && (
|
||||||
|
<React.Fragment>
|
||||||
|
<Grid size={12}>
|
||||||
|
<Divider />
|
||||||
|
</Grid>
|
||||||
|
<Grid size={12}>
|
||||||
|
<BinsList
|
||||||
|
gridView
|
||||||
|
bins={emptyBins}
|
||||||
|
duplicateBin={duplicateBin}
|
||||||
|
title={"Empty Bins"}
|
||||||
|
// loadMore={newTranslation => {
|
||||||
|
// //only triggered in the scroll view so this will never trigger in grid view
|
||||||
|
// if (yardBins.length < binTotal) {
|
||||||
|
// loadMoreBins(contentFilter);
|
||||||
|
// setScrollTranslations({ ...scrollTranslations, empty: newTranslation });
|
||||||
|
// }
|
||||||
|
// }}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const binsHeader = () => {
|
||||||
|
return (
|
||||||
|
<Menu
|
||||||
|
id="yardMenu"
|
||||||
|
anchorEl={binMenuAnchorEl ? binMenuAnchorEl : null}
|
||||||
|
open={binMenuAnchorEl !== null}
|
||||||
|
onClose={() => {
|
||||||
|
setBinMenuAnchorEl(null);
|
||||||
|
}}
|
||||||
|
disableAutoFocusItem>
|
||||||
|
<MenuItem>
|
||||||
|
<FormControl style={{ marginLeft: theme.spacing(2) }}>
|
||||||
|
<InputLabel id="demo-simple-select-label">Order</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="demo-simple-select-label"
|
||||||
|
id="demo-simple-select"
|
||||||
|
value={order}
|
||||||
|
onChange={event => setOrder(event.target.value as SetStateAction<"asc" | "desc">)}>
|
||||||
|
<MenuItem value={"asc"}>Ascending</MenuItem>
|
||||||
|
<MenuItem value={"desc"}>Descending</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem>
|
||||||
|
<FormControl style={{ marginLeft: theme.spacing(2) }}>
|
||||||
|
<InputLabel id="demo-simple-select-label">Order By</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="demo-simple-select-label"
|
||||||
|
id="demo-simple-select"
|
||||||
|
value={orderBy}
|
||||||
|
onChange={event => setOrderBy(event.target.value as SetStateAction<string>)}>
|
||||||
|
<MenuItem value={"name"}>Name</MenuItem>
|
||||||
|
<MenuItem value={"timestamp"}>Timestamp</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</MenuItem>
|
||||||
|
<Divider />
|
||||||
|
<MenuItem>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={displayGrain}
|
||||||
|
onChange={(e, checked) => {
|
||||||
|
setDisplayGrain(checked);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={<Typography>Grain</Typography>}
|
||||||
|
/>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={displayFert}
|
||||||
|
onChange={(e, checked) => {
|
||||||
|
setDisplayFert(checked);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={<Typography>Fertilizer</Typography>}
|
||||||
|
/>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={displayEmpty}
|
||||||
|
onChange={(e, checked) => {
|
||||||
|
setDisplayEmpty(checked);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={<Typography>Empty Bins</Typography>}
|
||||||
|
/>
|
||||||
|
</MenuItem>
|
||||||
|
{/* <MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
setOpenQrGenerator(true);
|
||||||
|
}}>
|
||||||
|
<Typography>Generate QR Codes</Typography>
|
||||||
|
</MenuItem> */}
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const binsByGrainType = () => {
|
||||||
|
return (
|
||||||
|
<Box width={1} marginBottom={1}>
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
direction="row"
|
||||||
|
alignContent="center"
|
||||||
|
alignItems="center">
|
||||||
|
<Grid>
|
||||||
|
<Box display="flex" paddingY={1} alignContent="center" alignItems="center">
|
||||||
|
<Typography variant="h6" style={{ fontWeight: 650 }}>
|
||||||
|
Bins - ({yardBins.length} of {binTotal})
|
||||||
|
</Typography>
|
||||||
|
{contentFilter && (
|
||||||
|
<Box marginLeft={1}>
|
||||||
|
<Chip
|
||||||
|
size="small"
|
||||||
|
onDelete={() => {
|
||||||
|
setContentFilter("");
|
||||||
|
loadMoreBins("");
|
||||||
|
}}
|
||||||
|
label={contentFilter}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{yardBins.length < binTotal && (
|
||||||
|
<Button
|
||||||
|
style={{ marginLeft: 10 }}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => {
|
||||||
|
loadMoreBins(contentFilter, 0, yardBins.length);
|
||||||
|
}}>
|
||||||
|
Load All
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<MoreVert
|
||||||
|
className={classes.icon}
|
||||||
|
onClick={event => {
|
||||||
|
let target = event.currentTarget;
|
||||||
|
setBinMenuAnchorEl(target);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
{binsLoading ? <Skeleton variant="rectangular" height="200px" /> : binsContent()}
|
||||||
|
{binsHeader()}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const desktopInventory = () => {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Grid container direction="row" alignContent="center" alignItems="center">
|
||||||
|
<Grid style={{ height: "200px" }} size={6}>
|
||||||
|
<BinInventoryChart
|
||||||
|
customLabel="Grain"
|
||||||
|
inventory={displayedInventory}
|
||||||
|
onClick={(grain: pond.Grain, grainName: string) => {
|
||||||
|
let filter = "";
|
||||||
|
if (grain !== pond.Grain.GRAIN_NONE && grain !== pond.Grain.GRAIN_CUSTOM) {
|
||||||
|
filter = pond.Grain[grain];
|
||||||
|
} else {
|
||||||
|
filter = grainName;
|
||||||
|
}
|
||||||
|
setContentFilter(filter);
|
||||||
|
loadMoreBins(filter);
|
||||||
|
}}
|
||||||
|
//activeGrain={grainFilter}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid style={{ height: "200px" }} size={6}>
|
||||||
|
<BinInventoryChart
|
||||||
|
customLabel="Fertilizer"
|
||||||
|
inventory={displayedFertilizer}
|
||||||
|
onClick={(grain: pond.Grain, grainName: string) => {
|
||||||
|
let filter = "";
|
||||||
|
if (grain !== pond.Grain.GRAIN_NONE && grain !== pond.Grain.GRAIN_CUSTOM) {
|
||||||
|
filter = pond.Grain[grain];
|
||||||
|
} else {
|
||||||
|
filter = grainName;
|
||||||
|
}
|
||||||
|
setContentFilter(filter);
|
||||||
|
loadMoreBins(filter);
|
||||||
|
}}
|
||||||
|
customUnit={"L"}
|
||||||
|
//activeGrain={grainFilter}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const mobileViewCarousel = () => {
|
||||||
|
let length = 2;
|
||||||
|
let charts: JSX.Element[] = [
|
||||||
|
<BinInventoryChart
|
||||||
|
customLabel="Grain"
|
||||||
|
inventory={displayedInventory}
|
||||||
|
onClick={(grain: pond.Grain, grainName: string) => {
|
||||||
|
let filter = "";
|
||||||
|
if (grain !== pond.Grain.GRAIN_NONE && grain !== pond.Grain.GRAIN_CUSTOM) {
|
||||||
|
filter = pond.Grain[grain];
|
||||||
|
} else {
|
||||||
|
filter = grainName;
|
||||||
|
}
|
||||||
|
setContentFilter(filter);
|
||||||
|
loadMoreBins(filter);
|
||||||
|
}}
|
||||||
|
//activeGrain={grainFilter}
|
||||||
|
/>,
|
||||||
|
<BinInventoryChart
|
||||||
|
customLabel="Fertilizer"
|
||||||
|
inventory={displayedFertilizer}
|
||||||
|
onClick={(grain: pond.Grain, grainName: string) => {
|
||||||
|
let filter = "";
|
||||||
|
if (grain !== pond.Grain.GRAIN_NONE && grain !== pond.Grain.GRAIN_CUSTOM) {
|
||||||
|
filter = pond.Grain[grain];
|
||||||
|
} else {
|
||||||
|
filter = grainName;
|
||||||
|
}
|
||||||
|
setContentFilter(filter);
|
||||||
|
loadMoreBins(filter);
|
||||||
|
}}
|
||||||
|
customUnit={"L"}
|
||||||
|
//activeGrain={grainFilter}
|
||||||
|
/>
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Grid container direction="row" alignContent="center" alignItems="center">
|
||||||
|
<Grid size={2}>
|
||||||
|
<Button
|
||||||
|
style={{ height: 160 }}
|
||||||
|
onClick={() => {
|
||||||
|
const newIndex = carouselIndex - 1;
|
||||||
|
setCarouselIndex(newIndex < 0 ? length - 1 : newIndex);
|
||||||
|
}}>
|
||||||
|
<ArrowBackIos />
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={8}>
|
||||||
|
<Box height={"180px"} flexDirection="row" display="flex">
|
||||||
|
{charts[carouselIndex]}
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={2}>
|
||||||
|
<Button
|
||||||
|
style={{ height: 160 }}
|
||||||
|
onClick={() => {
|
||||||
|
const newIndex = carouselIndex + 1;
|
||||||
|
setCarouselIndex(newIndex >= length ? 0 : newIndex);
|
||||||
|
}}>
|
||||||
|
<ArrowForwardIos />
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalInventory = () => {
|
||||||
|
return (
|
||||||
|
<Accordion
|
||||||
|
className={classes.accordion}
|
||||||
|
expanded={expandTotal}
|
||||||
|
onChange={(_, expanded) => {
|
||||||
|
setExpandTotal(expanded);
|
||||||
|
sessionStorage.setItem("expandBinTotal", expanded.toString());
|
||||||
|
}}>
|
||||||
|
<AccordionSummary expandIcon={<ExpandMore />}>
|
||||||
|
<Typography variant="h6" style={{ fontWeight: 650 }}>
|
||||||
|
Total Inventory
|
||||||
|
</Typography>
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
{isMobile || insert ? mobileViewCarousel() : desktopInventory()}
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const binUtilizationList = () => {
|
||||||
|
const hasInventory = binMetrics && binMetrics.grainInventory.length > 0;
|
||||||
|
const useWeight = getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT;
|
||||||
|
return (
|
||||||
|
<Accordion
|
||||||
|
className={classes.accordion}
|
||||||
|
style={{ marginTop: 5 }}
|
||||||
|
expanded={expandUtilization}
|
||||||
|
onChange={(_, expanded) => {
|
||||||
|
setExpandUtilization(expanded);
|
||||||
|
sessionStorage.setItem("expandBinUtilization", expanded.toString());
|
||||||
|
}}>
|
||||||
|
<AccordionSummary expandIcon={<ExpandMore />}>
|
||||||
|
<Typography variant="h6" style={{ fontWeight: 650 }}>
|
||||||
|
Bin Utilization
|
||||||
|
</Typography>
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
{hasInventory ? (
|
||||||
|
<Box width={"100%"}>
|
||||||
|
<Typography variant="caption" color="textSecondary">
|
||||||
|
Click a chart to filter bins by grain
|
||||||
|
</Typography>
|
||||||
|
<GridList
|
||||||
|
className={classes.gridList}
|
||||||
|
cols={
|
||||||
|
props.insert && !isMobile
|
||||||
|
? 3.25
|
||||||
|
: width === "xs"
|
||||||
|
? 3.25
|
||||||
|
: width === "sm"
|
||||||
|
? 5.5
|
||||||
|
: width === "md"
|
||||||
|
? 6.5
|
||||||
|
: width === "lg"
|
||||||
|
? 7.5
|
||||||
|
: 8.5
|
||||||
|
}>
|
||||||
|
{binMetrics &&
|
||||||
|
binMetrics.grainInventory.map((inv, key) => (
|
||||||
|
<GridListTile key={key}>
|
||||||
|
<BinUtilizationChart
|
||||||
|
grain={inv.grainType}
|
||||||
|
customUnit={useWeight ? " mT" : " bu"}
|
||||||
|
bushelAmount={
|
||||||
|
useWeight
|
||||||
|
? inv.bushelAmount / GrainDescriber(inv.grainType).bushelsPerTonne
|
||||||
|
: inv.bushelAmount
|
||||||
|
}
|
||||||
|
bushelCapacity={
|
||||||
|
useWeight
|
||||||
|
? inv.bushelCapacity / GrainDescriber(inv.grainType).bushelsPerTonne
|
||||||
|
: inv.bushelCapacity
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
setContentFilter(pond.Grain[inv.grainType]);
|
||||||
|
loadMoreBins(pond.Grain[inv.grainType]);
|
||||||
|
}}
|
||||||
|
grainActive={contentFilter === pond.Grain[inv.grainType]}
|
||||||
|
/>
|
||||||
|
</GridListTile>
|
||||||
|
))}
|
||||||
|
{binMetrics &&
|
||||||
|
binMetrics.customInventory.map((inv, key) => {
|
||||||
|
//default to bushel values
|
||||||
|
let amount = inv.amount;
|
||||||
|
let cap = inv.capacity;
|
||||||
|
let unit = " bu";
|
||||||
|
if (inv.storageType === pond.BinStorage.BIN_STORAGE_FERTILIZER) {
|
||||||
|
amount = amount * 35.239;
|
||||||
|
cap = cap * 35.239;
|
||||||
|
unit = " L";
|
||||||
|
}
|
||||||
|
if (useWeight && inv.bushelsPerTonne > 1) {
|
||||||
|
amount = amount / inv.bushelsPerTonne;
|
||||||
|
cap = cap / inv.bushelsPerTonne;
|
||||||
|
unit = " mT";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GridListTile key={key}>
|
||||||
|
<BinUtilizationChart
|
||||||
|
grain={pond.Grain.GRAIN_CUSTOM}
|
||||||
|
bushelAmount={amount}
|
||||||
|
bushelCapacity={cap}
|
||||||
|
customUnit={unit}
|
||||||
|
onClick={() => {
|
||||||
|
setContentFilter(inv.name);
|
||||||
|
loadMoreBins(inv.name);
|
||||||
|
}}
|
||||||
|
grainActive={contentFilter === inv.name}
|
||||||
|
customLabel={inv.name}
|
||||||
|
customColour={stringToMaterialColour(inv.name)}
|
||||||
|
/>
|
||||||
|
</GridListTile>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</GridList>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Typography color="textSecondary" style={{ margin: "2rem", marginTop: "0rem" }}>
|
||||||
|
No active bins
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const binFanTable = () => {
|
||||||
|
return (
|
||||||
|
<Accordion
|
||||||
|
className={classes.accordion}
|
||||||
|
expanded={expandFanTable}
|
||||||
|
style={{ marginTop: 5 }}
|
||||||
|
onChange={(_, expanded) => {
|
||||||
|
setExpandFanTable(expanded);
|
||||||
|
sessionStorage.setItem("expandFanTable", expanded.toString());
|
||||||
|
}}>
|
||||||
|
<AccordionSummary expandIcon={<ExpandMore />}>
|
||||||
|
<Typography variant="h6" style={{ fontWeight: 650 }}>
|
||||||
|
Fan Status{" "}
|
||||||
|
{totalFanControllers > 0 && !isMobile
|
||||||
|
? " - " + totalFanControllersOn + " of " + totalFanControllers + " controllers on"
|
||||||
|
: ""}
|
||||||
|
</Typography>
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
{/* <BinsFansStatusTable
|
||||||
|
bins={yardBins}
|
||||||
|
isLoading={binsLoading}
|
||||||
|
getControllerInfo={(total, on) => {
|
||||||
|
setTotalFanControllers(total);
|
||||||
|
setTotalFanControllersOn(on);
|
||||||
|
}}
|
||||||
|
/> */}
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box height={1}>
|
||||||
|
<Box paddingY={0.5} paddingX={1}>
|
||||||
|
<Grid container>
|
||||||
|
<Grid size={12}>
|
||||||
|
{binsByGrainType()}
|
||||||
|
</Grid>
|
||||||
|
<Grid size={12}>
|
||||||
|
{binsLoading ? <Skeleton variant="rectangular" height="200px" /> : totalInventory()}
|
||||||
|
</Grid>
|
||||||
|
<Grid size={12}>
|
||||||
|
<Divider />
|
||||||
|
</Grid>
|
||||||
|
<Grid size={12}>
|
||||||
|
{binsLoading ? <Skeleton variant="rectangular" height="200px" /> : binUtilizationList()}
|
||||||
|
</Grid>
|
||||||
|
<Grid size={12}>
|
||||||
|
<Divider />
|
||||||
|
</Grid>
|
||||||
|
<Grid size={12}>
|
||||||
|
{binsLoading ? <Skeleton variant="rectangular" height="200px" /> : binFanTable()}
|
||||||
|
</Grid>
|
||||||
|
<Grid size={12}>
|
||||||
|
<Divider />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
{/* <QrCodeGenerator
|
||||||
|
open={openQrGenerator}
|
||||||
|
close={() => {
|
||||||
|
setOpenQrGenerator(false);
|
||||||
|
}}
|
||||||
|
keyList={qrKeyList}
|
||||||
|
requiredUrlAffix={"bins"}
|
||||||
|
/> */}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
43
src/charts/MiniPie.tsx
Normal file
43
src/charts/MiniPie.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Cell, Pie, PieChart, ResponsiveContainer } from "recharts";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
max: number;
|
||||||
|
current: number;
|
||||||
|
colour: string;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Data {
|
||||||
|
key: string;
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MiniPie(props: Props) {
|
||||||
|
const [data, setData] = useState<Data[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let data: Data[] = [];
|
||||||
|
data.push({ key: "current", value: props.current });
|
||||||
|
setData(data);
|
||||||
|
}, [props]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ResponsiveContainer width={props.size}>
|
||||||
|
<PieChart style={{ cursor: "pointer" }}>
|
||||||
|
<Pie
|
||||||
|
outerRadius={"100%"}
|
||||||
|
data={data}
|
||||||
|
dataKey="value"
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
startAngle={-90}
|
||||||
|
endAngle={(props.current / props.max) * -360 - 90}>
|
||||||
|
{data.map((entry: Data, index: number) => (
|
||||||
|
<Cell key={`cell-${index}`} fill={props.colour} />
|
||||||
|
))}
|
||||||
|
</Pie>
|
||||||
|
</PieChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
151
src/common/DisplayDrawer.tsx
Normal file
151
src/common/DisplayDrawer.tsx
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
import {
|
||||||
|
AppBar,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Drawer,
|
||||||
|
Grid2 as Grid,
|
||||||
|
IconButton,
|
||||||
|
Theme,
|
||||||
|
Typography
|
||||||
|
} from "@mui/material";
|
||||||
|
import { ArrowBackIos, ArrowForwardIos, Close, Fullscreen } from "@mui/icons-material";
|
||||||
|
import { useMobile } from "hooks";
|
||||||
|
import DeleteIcon from "products/AgIcons/Delete";
|
||||||
|
import GearMarkerIcon from "products/AgIcons/GearMarker";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { makeStyles } from "@mui/styles";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open: boolean;
|
||||||
|
onClose: (refresh?: boolean) => void;
|
||||||
|
displayPrev: () => void;
|
||||||
|
displayNext: () => void;
|
||||||
|
title: string;
|
||||||
|
subheader?: JSX.Element;
|
||||||
|
drawerBody: JSX.Element;
|
||||||
|
removeElement?: () => void;
|
||||||
|
updateElement?: () => void;
|
||||||
|
objectActions?: JSX.Element;
|
||||||
|
width: string | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) => ({
|
||||||
|
drawerMobile: {
|
||||||
|
borderRadius: 30
|
||||||
|
},
|
||||||
|
drawerDesktop: {
|
||||||
|
position: "relative",
|
||||||
|
top: 64
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
position: "static",
|
||||||
|
background: theme.palette.background.default,
|
||||||
|
paddingLeft: 15,
|
||||||
|
paddingRight: 15,
|
||||||
|
paddingBottom: 15,
|
||||||
|
zIndex: 101
|
||||||
|
},
|
||||||
|
appBar: {
|
||||||
|
position: "static",
|
||||||
|
borderRadius: 30
|
||||||
|
},
|
||||||
|
drawerPaper: {
|
||||||
|
overflowX: "hidden"
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function DisplayDrawer(props: Props) {
|
||||||
|
const {
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
displayPrev,
|
||||||
|
displayNext,
|
||||||
|
title,
|
||||||
|
drawerBody,
|
||||||
|
removeElement,
|
||||||
|
updateElement,
|
||||||
|
objectActions,
|
||||||
|
width
|
||||||
|
} = props;
|
||||||
|
const isMobile = useMobile();
|
||||||
|
const classes = useStyles();
|
||||||
|
const [fullScreen, setFullScreen] = useState(false);
|
||||||
|
const [drawerHeight, setDrawerHeight] = useState(60);
|
||||||
|
|
||||||
|
const toggleFullScreen = () => {
|
||||||
|
if (!fullScreen) {
|
||||||
|
setDrawerHeight(94);
|
||||||
|
setFullScreen(true);
|
||||||
|
} else {
|
||||||
|
setDrawerHeight(60);
|
||||||
|
setFullScreen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
open={open}
|
||||||
|
anchor={isMobile ? "bottom" : "right"}
|
||||||
|
BackdropProps={{ invisible: true }}
|
||||||
|
variant={isMobile ? "temporary" : "persistent"}
|
||||||
|
classes={{ paper: classes.drawerPaper }}>
|
||||||
|
<Box
|
||||||
|
className={isMobile ? classes.drawerMobile : classes.drawerDesktop}
|
||||||
|
style={{ width: isMobile ? "100%" : width, height: isMobile ? drawerHeight + "vh" : 0 }}>
|
||||||
|
<Box className={classes.header} style={{ width: isMobile ? "100%" : width }}>
|
||||||
|
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||||
|
<Box>
|
||||||
|
<IconButton onClick={() => onClose()}>
|
||||||
|
<Close />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
{updateElement && (
|
||||||
|
<IconButton onClick={updateElement}>
|
||||||
|
<GearMarkerIcon />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
{removeElement && (
|
||||||
|
<IconButton onClick={removeElement}>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
{objectActions && objectActions}
|
||||||
|
{isMobile && (
|
||||||
|
<IconButton onClick={() => toggleFullScreen()}>
|
||||||
|
<Fullscreen />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<AppBar className={classes.appBar} color="secondary">
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
direction="row"
|
||||||
|
alignItems="center"
|
||||||
|
wrap="nowrap">
|
||||||
|
<Grid>
|
||||||
|
<Button onClick={() => displayPrev()}>
|
||||||
|
<ArrowBackIos style={{ color: "black" }} />
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<Box>
|
||||||
|
<Typography align="center" variant="h5">
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<Button onClick={() => displayNext()}>
|
||||||
|
<ArrowForwardIos style={{ color: "black" }} />
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</AppBar>
|
||||||
|
</Box>
|
||||||
|
<Box>{drawerBody}</Box>
|
||||||
|
</Box>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
66
src/common/MapCard.tsx
Normal file
66
src/common/MapCard.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { createStyles, Card, CardContent, Typography } from "@mui/material";
|
||||||
|
import { withStyles, WithStyles } from "@mui/styles";
|
||||||
|
import React from "react";
|
||||||
|
//import { Theme } from "@material-ui/core/styles/createMuiTheme";
|
||||||
|
//import withStyles, { WithStyles } from "@material-ui/core/styles/withStyles";
|
||||||
|
import MapGL, { Coordinate } from "./MapGL";
|
||||||
|
|
||||||
|
const styles = () =>
|
||||||
|
createStyles({
|
||||||
|
card: {
|
||||||
|
position: "relative",
|
||||||
|
height: "100%",
|
||||||
|
minHeight: "350px"
|
||||||
|
},
|
||||||
|
cardContent: {
|
||||||
|
height: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center"
|
||||||
|
},
|
||||||
|
currentLocation: {
|
||||||
|
width: "100%"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Props extends WithStyles<typeof styles> {
|
||||||
|
path: Array<Coordinate>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {}
|
||||||
|
|
||||||
|
class MapCard extends React.Component<Props, State> {
|
||||||
|
render() {
|
||||||
|
const { classes, path } = this.props;
|
||||||
|
let paths: Map<string, Array<Coordinate>> = new Map().set("path", path);
|
||||||
|
let approx = "Approximate location: Unknown";
|
||||||
|
if (path.length > 0) {
|
||||||
|
let sorted = path.sort((a: Coordinate, b: Coordinate) => {
|
||||||
|
if (a.timestamp < b.timestamp) return 1;
|
||||||
|
else if (b.timestamp < a.timestamp) return -1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
let loc = sorted[0].latitude + ", " + sorted[0].longitude;
|
||||||
|
approx = "Approximate location: " + loc;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
// <Grid item xs={12} sm={12} md={6} lg={4} xl={3}>
|
||||||
|
<Card raised={true} className={classes.card}>
|
||||||
|
<CardContent className={classes.cardContent}>
|
||||||
|
<MapGL paths={paths} />
|
||||||
|
<Typography
|
||||||
|
className={classes.currentLocation}
|
||||||
|
color="textSecondary"
|
||||||
|
align="center"
|
||||||
|
variant="subtitle1">
|
||||||
|
{approx}
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
//</Grid>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withStyles(styles)(MapCard);
|
||||||
|
|
@ -3,7 +3,7 @@ import { Beenhere } from "@mui/icons-material";
|
||||||
import { useThemeType } from "hooks";
|
import { useThemeType } from "hooks";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import ReactMapGL, { Marker } from "react-map-gl";
|
import ReactMapGL, { Marker } from "react-map-gl/mapbox-legacy";
|
||||||
|
|
||||||
export interface Coordinate {
|
export interface Coordinate {
|
||||||
latitude: number;
|
latitude: number;
|
||||||
|
|
@ -87,16 +87,18 @@ export default function MapGL(props: Props) {
|
||||||
return (
|
return (
|
||||||
<ReactMapGL
|
<ReactMapGL
|
||||||
mapStyle={style}
|
mapStyle={style}
|
||||||
width="100%"
|
initialViewState={viewport}
|
||||||
height="100%"
|
//width="100%"
|
||||||
|
//height="100%"
|
||||||
{...viewport}
|
{...viewport}
|
||||||
onViewStateChange={(e) => {
|
// onViewStateChange={(e: any) => {
|
||||||
setViewport({
|
// setViewport({
|
||||||
longitude: e.viewState.longitude,
|
// longitude: e.viewState.longitude,
|
||||||
latitude: e.viewState.latitude,
|
// latitude: e.viewState.latitude,
|
||||||
zoom: e.viewState.zoom,
|
// zoom: e.viewState.zoom,
|
||||||
});
|
// });
|
||||||
}}>
|
// }}
|
||||||
|
>
|
||||||
{markers()}
|
{markers()}
|
||||||
</ReactMapGL>
|
</ReactMapGL>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ import NotificationButton from "common/NotificationButton";
|
||||||
// import InteractionSettings from "interactions/InteractionSettings";
|
// import InteractionSettings from "interactions/InteractionSettings";
|
||||||
import { cloneDeep } from "lodash";
|
import { cloneDeep } from "lodash";
|
||||||
// import { Component, Device, deviceScope, Interaction } from "models";
|
// import { Component, Device, deviceScope, Interaction } from "models";
|
||||||
import { Device, deviceScope } from "models";
|
import { Component, Device, deviceScope } from "models";
|
||||||
// import { MatchParams } from "navigation/Routes";
|
// import { MatchParams } from "navigation/Routes";
|
||||||
// import { isShareableLink } from "pbHelpers/Device";
|
// import { isShareableLink } from "pbHelpers/Device";
|
||||||
import { pond } from "protobuf-ts/pond";
|
import { pond } from "protobuf-ts/pond";
|
||||||
|
|
@ -96,7 +96,7 @@ const useStyles = makeStyles((_theme: Theme) => {
|
||||||
interface Props {
|
interface Props {
|
||||||
device: Device;
|
device: Device;
|
||||||
isPaused: boolean;
|
isPaused: boolean;
|
||||||
// components: Component[];
|
components: Component[];
|
||||||
// interactions: Interaction[];
|
// interactions: Interaction[];
|
||||||
refreshCallback: () => void;
|
refreshCallback: () => void;
|
||||||
availablePositions: DeviceAvailabilityMap;
|
availablePositions: DeviceAvailabilityMap;
|
||||||
|
|
@ -129,7 +129,7 @@ export default function DeviceActions(props: Props) {
|
||||||
const {
|
const {
|
||||||
device,
|
device,
|
||||||
isPaused,
|
isPaused,
|
||||||
// components,
|
components,
|
||||||
// interactions,
|
// interactions,
|
||||||
refreshCallback,
|
refreshCallback,
|
||||||
availablePositions,
|
availablePositions,
|
||||||
|
|
@ -383,8 +383,8 @@ export default function DeviceActions(props: Props) {
|
||||||
closeDialogCallback={() => closeDialog("isDeviceSettingsDialogOpen")}
|
closeDialogCallback={() => closeDialog("isDeviceSettingsDialogOpen")}
|
||||||
refreshCallback={refreshCallback}
|
refreshCallback={refreshCallback}
|
||||||
canEdit={canWrite}
|
canEdit={canWrite}
|
||||||
// components={components}
|
components={components}
|
||||||
components={[]}
|
//components={[]}
|
||||||
/>
|
/>
|
||||||
<ComponentSettings
|
<ComponentSettings
|
||||||
mode="add"
|
mode="add"
|
||||||
|
|
|
||||||
391
src/device/DeviceViewer.tsx
Normal file
391
src/device/DeviceViewer.tsx
Normal file
|
|
@ -0,0 +1,391 @@
|
||||||
|
import { Box, Grid2 as Grid, Theme, Typography } from "@mui/material";
|
||||||
|
import {makeStyles} from "@mui/styles"
|
||||||
|
//import { Theme } from "@material-ui/core/styles/createMuiTheme";
|
||||||
|
import Loader from "common/Loader";
|
||||||
|
import MapCard from "common/MapCard";
|
||||||
|
import { Coordinate } from "common/MapGL";
|
||||||
|
import ComponentCard from "component/ComponentCard";
|
||||||
|
import ComponentSettings from "component/ComponentSettings";
|
||||||
|
import DeviceOverview from "device/DeviceOverview";
|
||||||
|
import {
|
||||||
|
useComponentAPI,
|
||||||
|
useDeviceAPI,
|
||||||
|
useInteractionsAPI,
|
||||||
|
useSnackbar,
|
||||||
|
//useUsageAPI,
|
||||||
|
useUserAPI
|
||||||
|
} from "hooks";
|
||||||
|
import { cloneDeep } from "lodash";
|
||||||
|
import { Component, Device as DeviceModel, deviceScope, Interaction } from "models";
|
||||||
|
import moment from "moment";
|
||||||
|
import { sameComponentID, sortComponents } from "pbHelpers/Component";
|
||||||
|
import {
|
||||||
|
FindAvailablePositions,
|
||||||
|
DeviceAvailabilityMap,
|
||||||
|
OffsetAvailabilityMap
|
||||||
|
} from "pbHelpers/DeviceAvailability";
|
||||||
|
import { pond } from "protobuf-ts/pond";
|
||||||
|
import { quack } from "protobuf-ts/quack";
|
||||||
|
import { useGlobalState } from "providers";
|
||||||
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
|
//import { Redirect } from "react-router";
|
||||||
|
import { or } from "utils/types";
|
||||||
|
import DeviceActions from "./DeviceActions";
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) => ({
|
||||||
|
title: {
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis"
|
||||||
|
},
|
||||||
|
noComponents: {
|
||||||
|
marginTop: theme.spacing(16)
|
||||||
|
},
|
||||||
|
addComponentText: {
|
||||||
|
...theme.typography.subtitle2,
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
cursor: "pointer"
|
||||||
|
},
|
||||||
|
deviceLoading: {
|
||||||
|
position: "relative",
|
||||||
|
height: "100%",
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
marginTop: theme.spacing(16),
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center"
|
||||||
|
},
|
||||||
|
rateLimit: {
|
||||||
|
marginTop: theme.spacing(1),
|
||||||
|
color: theme.palette.text.secondary
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
interface Props {
|
||||||
|
device: DeviceModel;
|
||||||
|
isMobile?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Device(props: Props) {
|
||||||
|
const classes = useStyles();
|
||||||
|
const [{ user }] = useGlobalState();
|
||||||
|
const { error } = useSnackbar();
|
||||||
|
const deviceID = props.device.id();
|
||||||
|
const deviceAPI = useDeviceAPI();
|
||||||
|
const componentAPI = useComponentAPI();
|
||||||
|
const interactionsAPI = useInteractionsAPI();
|
||||||
|
//const usageAPI = useUsageAPI();
|
||||||
|
const userAPI = useUserAPI();
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
|
const [device, setDevice] = useState<DeviceModel>(DeviceModel.create());
|
||||||
|
const [components, setComponents] = useState<Map<string, Component>>(new Map());
|
||||||
|
const [interactions, setInteractions] = useState<Interaction[]>([]);
|
||||||
|
const [permissions, setPermissions] = useState<pond.Permission[]>([]);
|
||||||
|
const [preferences, setPreferences] = useState<pond.UserPreferences>(
|
||||||
|
pond.UserPreferences.create()
|
||||||
|
);
|
||||||
|
const [invalidDevice, setInvalidDevice] = useState<boolean>(false);
|
||||||
|
const [isAddComponentDialogOpen, setIsAddComponentDialogOpen] = useState<boolean>(false);
|
||||||
|
const [availablePositions, setAvailablePositions] = useState<DeviceAvailabilityMap>(new Map());
|
||||||
|
const [availableOffsets, setAvailableOffsets] = useState<OffsetAvailabilityMap>(new Map());
|
||||||
|
const [latitude, setLatitude] = useState<number>(NaN);
|
||||||
|
const [longitude, setLongitude] = useState<number>(NaN);
|
||||||
|
const [cellularUsage, setCellularUsage] = useState<number>(0);
|
||||||
|
const [cellularStatus, setCellularStatus] = useState<string>("");
|
||||||
|
const rate = 30;
|
||||||
|
|
||||||
|
const load = useCallback(() => {
|
||||||
|
if (user.id() === "") return;
|
||||||
|
setIsLoading(true);
|
||||||
|
//let devicePromise = deviceAPI.get(deviceID);
|
||||||
|
let userPromise = userAPI.getUser(user.id(), deviceScope(deviceID.toString()));
|
||||||
|
let componentsPromise = componentAPI.list(
|
||||||
|
deviceID,
|
||||||
|
undefined,
|
||||||
|
[deviceID.toString()],
|
||||||
|
["device"],
|
||||||
|
true
|
||||||
|
);
|
||||||
|
let interactionsPromise = interactionsAPI.listInteractionsByDevice(deviceID);
|
||||||
|
let groupPromise: Promise<any> = Promise.resolve(undefined);
|
||||||
|
Promise.all([userPromise, componentsPromise, interactionsPromise, groupPromise])
|
||||||
|
.then(([userRes, componentsRes, interactionsRes]) => {
|
||||||
|
//let rDevice = DeviceModel.any(deviceRes.data);
|
||||||
|
let rawComponents: Array<any> = or(componentsRes.data.components, []);
|
||||||
|
let rComponents: Map<string, Component> = new Map();
|
||||||
|
rawComponents.forEach((rawComponent: any) => {
|
||||||
|
let component = Component.any(rawComponent);
|
||||||
|
rComponents.set(component.key(), component);
|
||||||
|
});
|
||||||
|
|
||||||
|
setComponents(rComponents);
|
||||||
|
|
||||||
|
setInteractions(interactionsRes);
|
||||||
|
setPermissions(userRes.permissions);
|
||||||
|
setPreferences(userRes.preferences);
|
||||||
|
|
||||||
|
setDevice(props.device);
|
||||||
|
setLongitude(props.device.status.longitude ? props.device.status.longitude : NaN);
|
||||||
|
setLatitude(props.device.status.latitude ? props.device.status.latitude : NaN);
|
||||||
|
let available = FindAvailablePositions(
|
||||||
|
Array.from(rComponents.values()),
|
||||||
|
props.device.settings.product
|
||||||
|
);
|
||||||
|
setAvailablePositions(available.GetAvailability());
|
||||||
|
setAvailableOffsets(available.offsetAvailability);
|
||||||
|
})
|
||||||
|
.catch((err: any) => {
|
||||||
|
setDevice(DeviceModel.create());
|
||||||
|
setComponents(new Map());
|
||||||
|
setInteractions([]);
|
||||||
|
setAvailablePositions(new Map());
|
||||||
|
setAvailableOffsets(new Map());
|
||||||
|
setPermissions([]);
|
||||||
|
setPreferences(pond.UserPreferences.create());
|
||||||
|
setInvalidDevice(true);
|
||||||
|
error(err);
|
||||||
|
})
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
// usageAPI
|
||||||
|
// .getUsage(deviceID, moment().subtract(1, "days"))
|
||||||
|
// .then((res: any) => {
|
||||||
|
// let usage = res.data;
|
||||||
|
// if (usage) {
|
||||||
|
// let rCellularStatus = "active";
|
||||||
|
// let rCellularUsage = 0;
|
||||||
|
// let sessions: any[] = [];
|
||||||
|
// if (usage.sessions) {
|
||||||
|
// sessions = usage.sessions.filter((session: any) => {
|
||||||
|
// return moment(session.begin).isAfter(moment().subtract(1, "days"));
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// sessions.forEach((session: any) => (rCellularUsage += Number(or(session.bytes, 0))));
|
||||||
|
// setCellularStatus(rCellularStatus);
|
||||||
|
// setCellularUsage(rCellularUsage);
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// .catch(err => {});
|
||||||
|
}, [componentAPI, props.device, deviceID, error, interactionsAPI, /*usageAPI*/, userAPI, user]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
load();
|
||||||
|
}, [load]);
|
||||||
|
|
||||||
|
const getOrderedComponents = () => {
|
||||||
|
if (preferences.childDisplayOrder.length === Array.from(components.values()).length) {
|
||||||
|
return Array.from(components.values()).sort((a, b: Component) =>
|
||||||
|
sortComponents(a, b, preferences.childDisplayOrder)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Array.from(components.values());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayedComponentsBlacklist = [
|
||||||
|
quack.ComponentType.COMPONENT_TYPE_POWER,
|
||||||
|
quack.ComponentType.COMPONENT_TYPE_MODEM
|
||||||
|
];
|
||||||
|
|
||||||
|
const displayedComponents = () => {
|
||||||
|
return [...components.values()].filter(
|
||||||
|
c => !displayedComponentsBlacklist.includes(c.settings.type)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleComponentChanged = (component: Component) => {
|
||||||
|
let updatedComponents = cloneDeep(components);
|
||||||
|
let updatedComponent = cloneDeep(component);
|
||||||
|
if (updatedComponents.has(updatedComponent.key())) {
|
||||||
|
updatedComponents.set(updatedComponent.key(), updatedComponent);
|
||||||
|
setComponents(updatedComponents);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// const productCard = () => {
|
||||||
|
// return <DeviceProductCard device={device} components={components} />;
|
||||||
|
// };
|
||||||
|
|
||||||
|
const componentCards = () => {
|
||||||
|
let orderedComponents = getOrderedComponents();
|
||||||
|
let componentCards = [];
|
||||||
|
let hasGPS = false;
|
||||||
|
for (let i = 0; i < orderedComponents.length; i++) {
|
||||||
|
let c = orderedComponents[i];
|
||||||
|
if (displayedComponentsBlacklist.includes(c.settings.type)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let id: quack.IComponentID = quack.ComponentID.fromObject({
|
||||||
|
type: c.settings.type,
|
||||||
|
addressType: c.settings.addressType,
|
||||||
|
address: c.settings.address
|
||||||
|
});
|
||||||
|
let filteredInteractions = interactions.filter(interaction => {
|
||||||
|
let isSource = false;
|
||||||
|
let isSink = false;
|
||||||
|
if (interaction.settings) {
|
||||||
|
isSource = sameComponentID(interaction.settings.source, id);
|
||||||
|
isSink = sameComponentID(interaction.settings.sink, id);
|
||||||
|
}
|
||||||
|
return isSource || isSink;
|
||||||
|
});
|
||||||
|
if (id.type === quack.ComponentType.COMPONENT_TYPE_GPS) {
|
||||||
|
hasGPS = true;
|
||||||
|
}
|
||||||
|
componentCards.push(
|
||||||
|
<ComponentCard
|
||||||
|
device={device}
|
||||||
|
availablePositions={availablePositions}
|
||||||
|
availableOffsets={availableOffsets}
|
||||||
|
component={c}
|
||||||
|
components={orderedComponents}
|
||||||
|
interactions={filteredInteractions}
|
||||||
|
permissions={permissions}
|
||||||
|
key={i}
|
||||||
|
showMobile={props.isMobile}
|
||||||
|
refreshCallback={(updatedComponent?: Component) =>
|
||||||
|
updatedComponent ? handleComponentChanged(updatedComponent) : load()
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!hasGPS && !isNaN(latitude) && !isNaN(longitude)) {
|
||||||
|
let path: Array<Coordinate> = [];
|
||||||
|
path.push({ latitude: latitude, longitude: longitude, timestamp: "" });
|
||||||
|
componentCards.push(<MapCard key={"map"} path={path} />);
|
||||||
|
}
|
||||||
|
|
||||||
|
return componentCards;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUsage = () => {
|
||||||
|
let usage = undefined;
|
||||||
|
if (cellularStatus && cellularStatus !== "") {
|
||||||
|
usage = { status: cellularStatus, bytes: cellularUsage };
|
||||||
|
}
|
||||||
|
return usage;
|
||||||
|
};
|
||||||
|
|
||||||
|
const anyComponentsReportingFast = () => {
|
||||||
|
let fast = false;
|
||||||
|
components.forEach(c => {
|
||||||
|
if (
|
||||||
|
c.settings.measurementPeriodMs > 0 &&
|
||||||
|
c.settings.reportPeriodMs > 0 &&
|
||||||
|
c.settings.reportPeriodMs < rate * 1000
|
||||||
|
) {
|
||||||
|
fast = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return fast;
|
||||||
|
};
|
||||||
|
|
||||||
|
const componentsTab = () => {
|
||||||
|
return (
|
||||||
|
<Box padding={1} marginTop={1}>
|
||||||
|
{anyComponentsReportingFast() && (
|
||||||
|
<Typography className={classes.rateLimit} variant="caption">
|
||||||
|
Live updates are rate limited to {rate}s to ensure a smooth browsing experience
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<span className={classes.deviceLoading}>
|
||||||
|
<Loader />
|
||||||
|
</span>
|
||||||
|
) : displayedComponents().length > 0 ? (
|
||||||
|
<Grid container direction="row" spacing={2}>
|
||||||
|
{componentCards()}
|
||||||
|
</Grid>
|
||||||
|
) : (
|
||||||
|
<Box marginTop={4}>
|
||||||
|
<Typography variant="subtitle1" align="center" color="textSecondary">
|
||||||
|
No components found
|
||||||
|
</Typography>
|
||||||
|
{permissions.includes(pond.Permission.PERMISSION_WRITE) && (
|
||||||
|
<React.Fragment>
|
||||||
|
<Typography variant="body2" align="center" color="textSecondary">
|
||||||
|
<span
|
||||||
|
className={classes.addComponentText}
|
||||||
|
onClick={() => setIsAddComponentDialogOpen(true)}>
|
||||||
|
{" Add "}
|
||||||
|
</span>
|
||||||
|
a component to {device.name()}.
|
||||||
|
</Typography>
|
||||||
|
<ComponentSettings
|
||||||
|
mode="add"
|
||||||
|
device={device}
|
||||||
|
isDialogOpen={or(isAddComponentDialogOpen, false)}
|
||||||
|
closeDialogCallback={() => setIsAddComponentDialogOpen(false)}
|
||||||
|
availablePositions={availablePositions}
|
||||||
|
availableOffsets={availableOffsets}
|
||||||
|
refreshCallback={load}
|
||||||
|
canEdit={permissions.includes(pond.Permission.PERMISSION_WRITE)}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isPaused = () => {
|
||||||
|
let paused = false;
|
||||||
|
if (cellularStatus && cellularStatus !== "") {
|
||||||
|
if (cellularStatus.toLowerCase().includes("pause")) {
|
||||||
|
paused = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return paused;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleNotificationPreference = () => {
|
||||||
|
let updatedPreferences = cloneDeep(preferences);
|
||||||
|
updatedPreferences.notify = !preferences.notify;
|
||||||
|
deviceAPI
|
||||||
|
.updatePreferences(deviceID, updatedPreferences)
|
||||||
|
.then(() => setPreferences(updatedPreferences))
|
||||||
|
.catch(() => {
|
||||||
|
error(
|
||||||
|
"Error occured while " +
|
||||||
|
(preferences.notify ? "enabling" : "disabling") +
|
||||||
|
" notifications"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// if (invalidDevice) {
|
||||||
|
// return <Redirect to="/404" />;
|
||||||
|
// } else {
|
||||||
|
return (
|
||||||
|
<Box padding={1}>
|
||||||
|
<Grid container direction="row" alignItems="center">
|
||||||
|
<Grid style={{ order: 5 }}>
|
||||||
|
<DeviceActions
|
||||||
|
device={device}
|
||||||
|
isPaused={isPaused()}
|
||||||
|
components={getOrderedComponents()}
|
||||||
|
//interactions={interactions}
|
||||||
|
availablePositions={availablePositions}
|
||||||
|
availableOffsets={availableOffsets}
|
||||||
|
permissions={permissions}
|
||||||
|
refreshCallback={load}
|
||||||
|
preferences={preferences}
|
||||||
|
toggleNotificationPreference={toggleNotificationPreference}
|
||||||
|
isLoading={isLoading}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid style={{ order: 4 }}>
|
||||||
|
<DeviceOverview
|
||||||
|
device={device}
|
||||||
|
components={[...components.values()]}
|
||||||
|
usage={getUsage()}
|
||||||
|
loading={isLoading}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
{componentsTab()}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
//}
|
||||||
|
}
|
||||||
704
src/maps/MapBase.tsx
Normal file
704
src/maps/MapBase.tsx
Normal file
|
|
@ -0,0 +1,704 @@
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import "mapbox-gl/dist/mapbox-gl.css";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
Grid,
|
||||||
|
Icon,
|
||||||
|
IconButton,
|
||||||
|
LinearProgress,
|
||||||
|
MenuItem,
|
||||||
|
Select,
|
||||||
|
Theme
|
||||||
|
} from "@mui/material";
|
||||||
|
import { HomeMarker } from "models/HomeMarker";
|
||||||
|
import { useGlobalState, useHomeMarkerAPI } from "providers";
|
||||||
|
import { useMobile, useSnackbar } from "hooks";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import HomeIcon from "products/AgIcons/HomeIcon";
|
||||||
|
//import { Theme } from "@material-ui/core/styles/createMuiTheme";
|
||||||
|
import { GpsFixed, Layers } from "@mui/icons-material";
|
||||||
|
//import Geocoder from "react-map-gl-geocoder";
|
||||||
|
import "maps/mapboxStyleOverrides.css";
|
||||||
|
import { pond } from "protobuf-ts/pond";
|
||||||
|
import MarkerMove from "products/AgIcons/MarkerMove";
|
||||||
|
import ResponsiveDialog from "common/ResponsiveDialog";
|
||||||
|
import Markers, { MarkerData } from "./mapMarkers/Markers";
|
||||||
|
import GeoMapLayer from "./mapLayers/geoMapLayer";
|
||||||
|
import Geocoder from "./mapControllers/Geocoder";
|
||||||
|
import { GeometryMapping, shapeFromCoords } from "models/GeometryMapping";
|
||||||
|
import { FeatureCollection, Feature } from "geojson";
|
||||||
|
import DrawController from "./mapControllers/drawController";
|
||||||
|
//import { Geometry } from "geojson";
|
||||||
|
import Map, { MapRef, Marker, MarkerDragEvent } from "react-map-gl/mapbox-legacy";
|
||||||
|
import { getDistanceUnit } from "utils";
|
||||||
|
import { MapMouseEvent } from "mapbox-gl";
|
||||||
|
import { makeStyles } from "@mui/styles";
|
||||||
|
import { Result } from "@mapbox/mapbox-gl-geocoder";
|
||||||
|
|
||||||
|
//const MAPBOX_TOKEN = process.env.REACT_APP_MAPBOX_ACCESS_TOKEN;
|
||||||
|
const MAPBOX_TOKEN = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN;
|
||||||
|
|
||||||
|
const mapStyleOptions = [
|
||||||
|
{
|
||||||
|
label: "Dark",
|
||||||
|
value: "mapbox://styles/mapbox/dark-v10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Satellite with streets",
|
||||||
|
value: "mapbox://styles/mapbox/satellite-streets-v11"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) => ({
|
||||||
|
iconButtons: {
|
||||||
|
background: theme.palette.background.default,
|
||||||
|
margin: 5,
|
||||||
|
boxShadow: "4px 4px 10px black"
|
||||||
|
},
|
||||||
|
pin: {
|
||||||
|
borderRadius: "50rem",
|
||||||
|
display: "inline-block",
|
||||||
|
borderBottomRightRadius: "0",
|
||||||
|
transform: "rotate(45deg)",
|
||||||
|
cursor: "pointer",
|
||||||
|
boxShadow: "4px 4px 10px black"
|
||||||
|
},
|
||||||
|
geoDot: {
|
||||||
|
height: "25px",
|
||||||
|
width: "25px",
|
||||||
|
backgroundColor: "yellow",
|
||||||
|
borderRadius: "50%",
|
||||||
|
border: "5px solid black"
|
||||||
|
},
|
||||||
|
geoIconColor: {
|
||||||
|
color: "black"
|
||||||
|
},
|
||||||
|
geoIconSearch: {
|
||||||
|
animation: "$rotation 3s infinite"
|
||||||
|
},
|
||||||
|
"@keyframes rotation": {
|
||||||
|
from: {
|
||||||
|
transform: "rotate(0deg)"
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
transform: "rotate(360deg)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
export interface ViewData {
|
||||||
|
longitude: number;
|
||||||
|
latitude: number;
|
||||||
|
zoom: number;
|
||||||
|
transitionDuration?: number;
|
||||||
|
xOffset?: number;
|
||||||
|
yOffset?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MeasurementData {
|
||||||
|
key: string;
|
||||||
|
geoShape: string;
|
||||||
|
coordinates: number[][];
|
||||||
|
segmentDistanceKm: number[];
|
||||||
|
totalDistanceKm: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
mapTools: JSX.Element;
|
||||||
|
mapClick(mapClickEvent: MapMouseEvent): void;
|
||||||
|
currentView: ViewData;
|
||||||
|
ignoreHomeLoad?: boolean;
|
||||||
|
defaultMapStyle?: string;
|
||||||
|
layerOptions?: JSX.Element;
|
||||||
|
markerData?: MarkerData[];
|
||||||
|
displayMarkerTitles?: boolean;
|
||||||
|
displayMarkerDetails?: boolean;
|
||||||
|
customCursor?: JSX.Element;
|
||||||
|
objectGeoData?: Map<string, pond.GeoData>;
|
||||||
|
measurementData?: Map<string, MeasurementData>;
|
||||||
|
objectTitles?: boolean;
|
||||||
|
editorMode?: "drawPolygon" | "drawLine" | "drawPoint" | "edit" | "none" | "delete";
|
||||||
|
placingMarker?: boolean;
|
||||||
|
linePointLimit?: number;
|
||||||
|
customSearchEntries?: Result[];
|
||||||
|
drawMeasurement?: boolean;
|
||||||
|
geocoderResultFunction?: (result: Result) => void;
|
||||||
|
geocoderTransitionFunction?: (result: Result) => void;
|
||||||
|
cutHoleInPolygon?: (object: string, coordinates: any[], geoType: string) => void;
|
||||||
|
editGeoCallback?: (object: string, borders: pond.Shape[], holes?: pond.Shape[]) => void;
|
||||||
|
addNewShape?: (coordinates: any[], geoType: string) => void;
|
||||||
|
addNewMeasurement?: (coordinates: number[][], geoType: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MapBase(props: Props) {
|
||||||
|
const [startingView, setStartingView] = useState<ViewData | undefined>();
|
||||||
|
//const [viewport, setViewport] = useState<ViewData>(props.currentView);
|
||||||
|
const transDuration = 3000; //in milliseconds
|
||||||
|
const homeMarkerAPI = useHomeMarkerAPI();
|
||||||
|
const classes = useStyles();
|
||||||
|
const isMobile = useMobile();
|
||||||
|
const [mapCursor, setMapCursor] = useState("auto");
|
||||||
|
const [hcHovered, setHCHovered] = useState(false);
|
||||||
|
const [gcHovered, setGCHovered] = useState(false);
|
||||||
|
const [dcHovered, setDCHovered] = useState(false);
|
||||||
|
const mapRef = useRef<MapRef>(null);
|
||||||
|
const [haveHome, setHaveHome] = useState(false);
|
||||||
|
const [homeKey, setHomeKey] = useState("");
|
||||||
|
const [homePin, setHomePin] = useState<{ longitude: number; latitude: number }>();
|
||||||
|
const [{ user, as }] = useGlobalState();
|
||||||
|
const { openSnack } = useSnackbar();
|
||||||
|
const [stateWatch, setStateWatch] = useState(0);
|
||||||
|
const [watching, setWatching] = useState(false);
|
||||||
|
const [locationFound, setLocationFound] = useState(true);
|
||||||
|
const [geoPosition, setGeoPosition] = useState({ longitude: 0, latitude: 0 });
|
||||||
|
const [dragAllowed, setDragAllowed] = useState(false);
|
||||||
|
const [mapStyle, setMapStyle] = useState(
|
||||||
|
props.defaultMapStyle ?? "mapbox://styles/mapbox/satellite-streets-v11"
|
||||||
|
);
|
||||||
|
const [styles, setStyles] = useState(false);
|
||||||
|
const [msHovered, setMSHovered] = useState(false);
|
||||||
|
const [layerIDs, setLayerIDs] = useState<string[]>([]);
|
||||||
|
const [geoCollection, setGeoCollection] = useState<FeatureCollection>();
|
||||||
|
const [measurementCollection, setMeasurementCollection] = useState<FeatureCollection>();
|
||||||
|
const [groupSelect, setGroupSelect] = useState(false);
|
||||||
|
const [currentZoom, setCurrentZoom] = useState(props.currentView.zoom);
|
||||||
|
|
||||||
|
const movePos = () => {
|
||||||
|
navigator.geolocation.getCurrentPosition(
|
||||||
|
pos => {
|
||||||
|
setGeoPosition({ longitude: pos.coords.longitude, latitude: pos.coords.latitude });
|
||||||
|
setLocationFound(true);
|
||||||
|
if (mapRef.current) {
|
||||||
|
if (!mapRef.current.isMoving()) {
|
||||||
|
mapRef.current.flyTo({
|
||||||
|
center: [pos.coords.longitude, pos.coords.latitude],
|
||||||
|
duration: transDuration,
|
||||||
|
zoom: 18
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
//error callback function
|
||||||
|
},
|
||||||
|
{ enableHighAccuracy: true }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const geolocateToggle = () => {
|
||||||
|
if (!watching) {
|
||||||
|
setWatching(true);
|
||||||
|
setStateWatch(navigator.geolocation.watchPosition(e => movePos()));
|
||||||
|
setLocationFound(false);
|
||||||
|
} else {
|
||||||
|
navigator.geolocation.clearWatch(stateWatch);
|
||||||
|
setWatching(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//used to change the viewport from the parent
|
||||||
|
useEffect(() => {
|
||||||
|
//setViewport(props.currentView);
|
||||||
|
if (mapRef.current) {
|
||||||
|
let xOffset = props.currentView.xOffset ?? 0;
|
||||||
|
let yOffset = props.currentView.yOffset ?? 0;
|
||||||
|
mapRef.current.flyTo({
|
||||||
|
offset: [xOffset, yOffset], //this is in pixels
|
||||||
|
center: [props.currentView.longitude, props.currentView.latitude], //this is in degrees
|
||||||
|
duration: props.currentView.transitionDuration,
|
||||||
|
zoom: props.currentView.zoom
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [props.currentView]);
|
||||||
|
|
||||||
|
const homeControl = () => {
|
||||||
|
if (!haveHome) {
|
||||||
|
setHaveHome(true);
|
||||||
|
if (mapRef.current) {
|
||||||
|
setHomePin({
|
||||||
|
longitude: mapRef.current.getCenter().lng,
|
||||||
|
latitude: mapRef.current.getCenter().lat
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// save the pin to the database
|
||||||
|
saveHomeMarker();
|
||||||
|
} else {
|
||||||
|
if (mapRef.current && homePin) {
|
||||||
|
mapRef.current.flyTo({
|
||||||
|
center: [homePin.longitude, homePin.latitude],
|
||||||
|
duration: transDuration
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadHomeMarkers = useCallback(() => {
|
||||||
|
homeMarkerAPI
|
||||||
|
.listHomeMarkers(1, 0, undefined, undefined, undefined, undefined, as)
|
||||||
|
.then(resp => {
|
||||||
|
if (resp.data.homeMarker.length < 1) {
|
||||||
|
setHomePin(undefined);
|
||||||
|
setHaveHome(false);
|
||||||
|
setStartingView(props.currentView);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let hm = HomeMarker.any(resp.data.homeMarker[0]);
|
||||||
|
setHomeKey(hm.key());
|
||||||
|
setHomePin({
|
||||||
|
longitude: hm.long(),
|
||||||
|
latitude: hm.lat()
|
||||||
|
});
|
||||||
|
setHaveHome(true);
|
||||||
|
if (props.ignoreHomeLoad) {
|
||||||
|
setStartingView(props.currentView);
|
||||||
|
} else if (!startingView) {
|
||||||
|
setStartingView({
|
||||||
|
longitude: hm.long(),
|
||||||
|
latitude: hm.lat(),
|
||||||
|
zoom: user.settings.mapZoom
|
||||||
|
});
|
||||||
|
} else if (mapRef.current) {
|
||||||
|
mapRef.current.flyTo({
|
||||||
|
center: [hm.long(), hm.lat()],
|
||||||
|
zoom: user.settings.mapZoom,
|
||||||
|
duration: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
openSnack("Could not load Home Pin");
|
||||||
|
});
|
||||||
|
}, [as, homeMarkerAPI, openSnack, props.ignoreHomeLoad, user]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
const saveHomeMarker = () => {
|
||||||
|
if (mapRef.current) {
|
||||||
|
let hm = HomeMarker.create();
|
||||||
|
hm.settings.userId = as ? as : user.id();
|
||||||
|
hm.settings.longitude = mapRef.current.getCenter().lng;
|
||||||
|
hm.settings.latitude = mapRef.current.getCenter().lat;
|
||||||
|
homeMarkerAPI
|
||||||
|
.addHomeMarker(hm.settings)
|
||||||
|
.then(resp => {
|
||||||
|
setHomeKey(resp.data.homeMarker);
|
||||||
|
openSnack("New Home Marker Created");
|
||||||
|
})
|
||||||
|
.catch(err => openSnack("Failed to save new Home Marker"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateHomeMarker = (longitude: number, latitude: number) => {
|
||||||
|
let hm = HomeMarker.create();
|
||||||
|
if (homePin) {
|
||||||
|
hm.settings.latitude = latitude;
|
||||||
|
hm.settings.longitude = longitude;
|
||||||
|
hm.settings.userId = as ? as : user.id();
|
||||||
|
homeMarkerAPI
|
||||||
|
.updateHomeMarker(homeKey, hm.settings)
|
||||||
|
.then(resp => {
|
||||||
|
openSnack("Home Marker Moved");
|
||||||
|
})
|
||||||
|
.catch(err => openSnack("Failed to Update Home Marker"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadHomeMarkers();
|
||||||
|
}, [loadHomeMarkers, as]);
|
||||||
|
|
||||||
|
const homeDragEnd = (event: MarkerDragEvent) => {
|
||||||
|
let longitude = event.lngLat.lng;
|
||||||
|
let latitude = event.lngLat.lat;
|
||||||
|
setHomePin({ longitude, latitude });
|
||||||
|
// update the pin in the database
|
||||||
|
updateHomeMarker(longitude, latitude);
|
||||||
|
};
|
||||||
|
|
||||||
|
const tools = () => {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
marginLeft: 10,
|
||||||
|
position: "absolute",
|
||||||
|
top: isMobile ? "120px" : "7%",
|
||||||
|
left: isMobile ? 0 : 75
|
||||||
|
}}
|
||||||
|
className="mapboxgl-ctrl-top-left mapboxgl-ctrl">
|
||||||
|
{props.mapTools}
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
marginRight: 10,
|
||||||
|
position: "absolute",
|
||||||
|
bottom: isMobile ? "7%" : "3%",
|
||||||
|
right: 10
|
||||||
|
}}
|
||||||
|
className="mapboxgl-ctrl-bottom-right mapboxgl-ctrl">
|
||||||
|
<Box>
|
||||||
|
{dragAllowed && (
|
||||||
|
<IconButton
|
||||||
|
className={classes.iconButtons}
|
||||||
|
style={
|
||||||
|
groupSelect ? { background: "yellow" } : dcHovered ? { background: "grey" } : {}
|
||||||
|
}
|
||||||
|
title="Group Select"
|
||||||
|
onClick={() => setGroupSelect(!groupSelect)}
|
||||||
|
onMouseOver={() => setDCHovered(true)}
|
||||||
|
onMouseOut={() => setDCHovered(false)}>
|
||||||
|
<MarkerMove />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<Grid container direction={isMobile ? "column" : "row"}>
|
||||||
|
<Grid item>
|
||||||
|
<IconButton
|
||||||
|
className={classes.iconButtons}
|
||||||
|
style={
|
||||||
|
dragAllowed ? { background: "yellow" } : dcHovered ? { background: "grey" } : {}
|
||||||
|
}
|
||||||
|
title="Toggle Drag"
|
||||||
|
onClick={() => {
|
||||||
|
setGroupSelect(false);
|
||||||
|
setDragAllowed(!dragAllowed);
|
||||||
|
}}
|
||||||
|
onMouseOver={() => setDCHovered(true)}
|
||||||
|
onMouseOut={() => setDCHovered(false)}>
|
||||||
|
<MarkerMove />
|
||||||
|
</IconButton>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<IconButton
|
||||||
|
className={classes.iconButtons}
|
||||||
|
style={msHovered ? { background: "grey" } : {}}
|
||||||
|
title="Map Style"
|
||||||
|
onClick={() => setStyles(true)}
|
||||||
|
onMouseOver={() => setMSHovered(true)}
|
||||||
|
onMouseOut={() => setMSHovered(false)}>
|
||||||
|
<Layers />
|
||||||
|
</IconButton>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<IconButton
|
||||||
|
className={classes.iconButtons}
|
||||||
|
style={hcHovered ? { background: "grey" } : {}}
|
||||||
|
title="Home Pin"
|
||||||
|
onClick={homeControl}
|
||||||
|
onMouseOver={() => setHCHovered(true)}
|
||||||
|
onMouseOut={() => setHCHovered(false)}>
|
||||||
|
<HomeIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<IconButton
|
||||||
|
className={classes.iconButtons}
|
||||||
|
style={
|
||||||
|
watching ? { background: "yellow" } : gcHovered ? { background: "grey" } : {}
|
||||||
|
}
|
||||||
|
title="Find Me"
|
||||||
|
onClick={geolocateToggle}
|
||||||
|
onMouseOver={() => setGCHovered(true)}
|
||||||
|
onMouseOut={() => setGCHovered(false)}>
|
||||||
|
<Icon className={locationFound ? "" : classes.geoIconSearch}>
|
||||||
|
<GpsFixed
|
||||||
|
classes={{
|
||||||
|
colorSecondary: classes.geoIconColor
|
||||||
|
}}
|
||||||
|
color={watching ? "secondary" : "inherit"}
|
||||||
|
/>
|
||||||
|
</Icon>
|
||||||
|
</IconButton>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapClick = (event: MapMouseEvent) => {
|
||||||
|
if (!watching) {
|
||||||
|
props.mapClick(event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStyleDialog = () => {
|
||||||
|
return (
|
||||||
|
<ResponsiveDialog open={styles} onClose={() => setStyles(false)}>
|
||||||
|
<DialogTitle>Select Map Style</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Select
|
||||||
|
fullWidth
|
||||||
|
label="Style"
|
||||||
|
value={mapStyle}
|
||||||
|
onChange={e => setMapStyle(e.target.value as string)}>
|
||||||
|
{mapStyleOptions.map(op => (
|
||||||
|
<MenuItem key={op.value} value={op.value}>
|
||||||
|
{op.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
{/* layer options from the object controller */}
|
||||||
|
{props.layerOptions}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => setStyles(false)}>Close</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</ResponsiveDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseEnter = useCallback(() => setMapCursor("pointer"), []);
|
||||||
|
const onMouseLeave = useCallback(() => setMapCursor("auto"), []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.editorMode === "drawPolygon" || props.placingMarker) {
|
||||||
|
setMapCursor("crosshair");
|
||||||
|
} else {
|
||||||
|
setMapCursor("auto");
|
||||||
|
}
|
||||||
|
}, [props.editorMode, props.placingMarker]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.objectGeoData) {
|
||||||
|
let feats: Feature[] = [];
|
||||||
|
props.objectGeoData.forEach(data => {
|
||||||
|
let newFeature = GeometryMapping.geoJSON(data.geoShape, data.shapes, data.holes) as Feature;
|
||||||
|
newFeature.id = data.objectKey;
|
||||||
|
newFeature.properties = {
|
||||||
|
title: data.title,
|
||||||
|
objectKey: data.objectKey,
|
||||||
|
fill: data.colour,
|
||||||
|
lineWidth: data.geoShape === "LineString" ? 5 : 2,
|
||||||
|
origin: data.origin
|
||||||
|
};
|
||||||
|
feats.push(newFeature);
|
||||||
|
});
|
||||||
|
let collection: FeatureCollection = {
|
||||||
|
type: "FeatureCollection",
|
||||||
|
features: feats
|
||||||
|
};
|
||||||
|
setGeoCollection(collection);
|
||||||
|
}
|
||||||
|
}, [props.objectGeoData]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
//add the measurementdata into the feature collection to be passed to our geo map layer
|
||||||
|
if (props.measurementData) {
|
||||||
|
let feats: Feature[] = [];
|
||||||
|
props.measurementData.forEach(data => {
|
||||||
|
let newFeature = GeometryMapping.geoJSON(data.geoShape, [
|
||||||
|
shapeFromCoords(data.coordinates)
|
||||||
|
]) as Feature;
|
||||||
|
newFeature.id = data.key;
|
||||||
|
newFeature.properties = {
|
||||||
|
lineWidth: 5,
|
||||||
|
origin: pond.DataOrigin.DATA_ORIGIN_ADAPTIVE,
|
||||||
|
totalDist:
|
||||||
|
getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS
|
||||||
|
? (data.totalDistanceKm / 1000).toFixed(2)
|
||||||
|
: (data.totalDistanceKm * 3280.8398950131).toFixed(2)
|
||||||
|
};
|
||||||
|
feats.push(newFeature);
|
||||||
|
});
|
||||||
|
let collection: FeatureCollection = {
|
||||||
|
type: "FeatureCollection",
|
||||||
|
features: feats
|
||||||
|
};
|
||||||
|
setMeasurementCollection(collection);
|
||||||
|
}
|
||||||
|
}, [props.measurementData]);
|
||||||
|
|
||||||
|
const onUpdate = (event: any) => {
|
||||||
|
let borders: pond.Shape[] = [];
|
||||||
|
let holes: pond.Shape[] = [];
|
||||||
|
let objectKey: string = "";
|
||||||
|
if (event.action === "change_coordinates" && event.features) {
|
||||||
|
let feature = event.features[0];
|
||||||
|
if (feature) {
|
||||||
|
let coords: any[] = feature.geometry.coordinates;
|
||||||
|
objectKey = feature.id;
|
||||||
|
if (feature.geometry.type === "Polygon") {
|
||||||
|
coords.forEach((poly: any[], i) => {
|
||||||
|
//the first shape is the border
|
||||||
|
if (i === 0) {
|
||||||
|
borders.push(shapeFromCoords(poly));
|
||||||
|
} else {
|
||||||
|
holes.push(shapeFromCoords(poly));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (feature.geometry.type === "LineString") {
|
||||||
|
borders.push(shapeFromCoords(coords));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (props.editGeoCallback) {
|
||||||
|
props.editGeoCallback(objectKey, borders, holes.length > 0 ? holes : undefined);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCreate = (event: any, objectToCut?: string, measurement?: boolean) => {
|
||||||
|
let feature = event.features[0];
|
||||||
|
console.log(feature)
|
||||||
|
//if the string exist we are adding a hole to an existing object
|
||||||
|
if (objectToCut) {
|
||||||
|
if (props.cutHoleInPolygon) {
|
||||||
|
props.cutHoleInPolygon(objectToCut, feature.geometry.coordinates, feature.geometry.type);
|
||||||
|
}
|
||||||
|
} else if (measurement) {
|
||||||
|
if (props.addNewMeasurement) {
|
||||||
|
props.addNewMeasurement(feature.geometry.coordinates, feature.geometry.type);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (props.addNewShape) {
|
||||||
|
console.log("add new shape")
|
||||||
|
props.addNewShape(feature.geometry.coordinates, feature.geometry.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getEditableFeatures = () => {
|
||||||
|
let editableFeatures: FeatureCollection = {
|
||||||
|
type: "FeatureCollection",
|
||||||
|
features: []
|
||||||
|
};
|
||||||
|
if (geoCollection) {
|
||||||
|
geoCollection.features.forEach(feature => {
|
||||||
|
//if the origin of the data comes from us (ie. a field/construction site with the adaptive origin)
|
||||||
|
if (
|
||||||
|
feature.properties &&
|
||||||
|
feature.properties.origin === pond.DataOrigin.DATA_ORIGIN_ADAPTIVE
|
||||||
|
) {
|
||||||
|
editableFeatures.features.push(feature);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return editableFeatures;
|
||||||
|
};
|
||||||
|
|
||||||
|
return startingView ? (
|
||||||
|
<Box height="100%">
|
||||||
|
{tools()}
|
||||||
|
{mapStyleDialog()}
|
||||||
|
<Map
|
||||||
|
ref={mapRef}
|
||||||
|
maxZoom={18}
|
||||||
|
initialViewState={startingView}
|
||||||
|
// {...viewport}
|
||||||
|
mapStyle={mapStyle}
|
||||||
|
onZoomEnd={e => {
|
||||||
|
setCurrentZoom(e.viewState.zoom);
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => {
|
||||||
|
if (
|
||||||
|
!props.editorMode ||
|
||||||
|
!props.placingMarker ||
|
||||||
|
props.editorMode === "none" ||
|
||||||
|
!props.placingMarker
|
||||||
|
)
|
||||||
|
onMouseEnter();
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
if (
|
||||||
|
!props.editorMode ||
|
||||||
|
!props.placingMarker ||
|
||||||
|
props.editorMode === "none" ||
|
||||||
|
!props.placingMarker
|
||||||
|
)
|
||||||
|
onMouseLeave();
|
||||||
|
}}
|
||||||
|
interactiveLayerIds={layerIDs}
|
||||||
|
// onMove={e => {
|
||||||
|
// setViewport({
|
||||||
|
// longitude: e.viewState.longitude,
|
||||||
|
// latitude: e.viewState.latitude,
|
||||||
|
// zoom: e.viewState.zoom
|
||||||
|
// });
|
||||||
|
// }}
|
||||||
|
mapboxAccessToken={MAPBOX_TOKEN}
|
||||||
|
cursor={mapCursor}
|
||||||
|
onClick={e => {
|
||||||
|
mapClick(e);
|
||||||
|
}}
|
||||||
|
doubleClickZoom={false}>
|
||||||
|
{/* map controllers */}
|
||||||
|
{MAPBOX_TOKEN && (
|
||||||
|
<Geocoder
|
||||||
|
mapboxAccessToken={MAPBOX_TOKEN}
|
||||||
|
position="top-right"
|
||||||
|
customEntries={props.customSearchEntries}
|
||||||
|
resultFunction={props.geocoderResultFunction}
|
||||||
|
customTransition={props.geocoderTransitionFunction}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<DrawController
|
||||||
|
featureCollection={getEditableFeatures()}
|
||||||
|
editMode={props.editorMode}
|
||||||
|
onCreate={onCreate}
|
||||||
|
onUpdate={onUpdate}
|
||||||
|
measurement={props.drawMeasurement}
|
||||||
|
linePointLimit={props.linePointLimit}
|
||||||
|
/>
|
||||||
|
{/* markers */}
|
||||||
|
{props.markerData && (
|
||||||
|
<Markers
|
||||||
|
markerData={props.markerData}
|
||||||
|
dragOn={dragAllowed}
|
||||||
|
groupSelect={groupSelect}
|
||||||
|
mapZoomLevel={currentZoom}
|
||||||
|
displayDetails={props.displayMarkerDetails}
|
||||||
|
showMarkerTitle={props.displayMarkerTitles}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{watching && locationFound && (
|
||||||
|
<Marker longitude={geoPosition.longitude} latitude={geoPosition.latitude}>
|
||||||
|
<Box className={classes.geoDot} />
|
||||||
|
</Marker>
|
||||||
|
)}
|
||||||
|
{haveHome && homePin && (
|
||||||
|
<Marker
|
||||||
|
longitude={homePin.longitude}
|
||||||
|
latitude={homePin.latitude}
|
||||||
|
draggable
|
||||||
|
onDragEnd={homeDragEnd}
|
||||||
|
offset={[0, -25]}>
|
||||||
|
<Box
|
||||||
|
className={classes.pin}
|
||||||
|
style={{ pointerEvents: "none", width: 50, height: 50, background: "red" }}>
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
transform: "rotate(-45deg)",
|
||||||
|
width: 30,
|
||||||
|
height: 30,
|
||||||
|
marginTop: -10,
|
||||||
|
marginLeft: -10,
|
||||||
|
pointerEvents: "none"
|
||||||
|
}}>
|
||||||
|
<HomeIcon type="light" />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Marker>
|
||||||
|
)}
|
||||||
|
{/* map layers */}
|
||||||
|
{geoCollection && (
|
||||||
|
<GeoMapLayer
|
||||||
|
objectCollection={geoCollection}
|
||||||
|
measurementCollection={measurementCollection}
|
||||||
|
showTitle={props.objectTitles}
|
||||||
|
setInteractiveLayers={e => {
|
||||||
|
setLayerIDs(e);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Map>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Box>
|
||||||
|
<LinearProgress />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
99
src/maps/MapMarkerSettings.tsx
Normal file
99
src/maps/MapMarkerSettings.tsx
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
Slider,
|
||||||
|
TextField
|
||||||
|
} from "@mui/material";
|
||||||
|
import ColourPicker from "common/ColourPicker";
|
||||||
|
import ResponsiveDialog from "common/ResponsiveDialog";
|
||||||
|
import { pond } from "protobuf-ts/pond";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open: boolean;
|
||||||
|
close: () => void;
|
||||||
|
theme: pond.ObjectTheme;
|
||||||
|
sizeControl?: boolean;
|
||||||
|
currentSize?: number;
|
||||||
|
colourControl?: boolean;
|
||||||
|
updateObject: (newTheme: pond.ObjectTheme) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MapMarkerSettings(props: Props) {
|
||||||
|
const { theme, sizeControl, colourControl, updateObject, currentSize } = props;
|
||||||
|
//const [objectSettings, setObjectSettings] = useState(props.objectSettings);
|
||||||
|
const [newColour, setNewColour] = useState("");
|
||||||
|
const [newSize, setNewSize] = useState(50);
|
||||||
|
const markerMin = 40;
|
||||||
|
const markerMax = 125;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentSize) {
|
||||||
|
setNewSize(currentSize);
|
||||||
|
} else {
|
||||||
|
setNewSize(50);
|
||||||
|
}
|
||||||
|
}, [currentSize]);
|
||||||
|
|
||||||
|
const reSize = (val: number) => {
|
||||||
|
let size = val;
|
||||||
|
if (val > markerMax) {
|
||||||
|
size = markerMax;
|
||||||
|
}
|
||||||
|
if (val < markerMin) {
|
||||||
|
size = markerMin;
|
||||||
|
}
|
||||||
|
setNewSize(size);
|
||||||
|
};
|
||||||
|
|
||||||
|
const update = () => {
|
||||||
|
let newTheme = theme;
|
||||||
|
newTheme.color = newColour;
|
||||||
|
newTheme.height = newSize;
|
||||||
|
newTheme.width = newSize;
|
||||||
|
updateObject(newTheme);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ResponsiveDialog open={props.open} onClose={props.close}>
|
||||||
|
<DialogTitle>Change Marker Settings</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
{sizeControl && (
|
||||||
|
<Box paddingTop={1.5}>
|
||||||
|
Marker Size
|
||||||
|
<Slider
|
||||||
|
value={newSize}
|
||||||
|
min={markerMin}
|
||||||
|
max={markerMax}
|
||||||
|
onChange={(e, val) => reSize(val as number)}
|
||||||
|
valueLabelDisplay="auto"
|
||||||
|
/>
|
||||||
|
<TextField type="number" value={newSize} onChange={e => reSize(+e.target.value)} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{colourControl && (
|
||||||
|
<Box>
|
||||||
|
Marker Colour
|
||||||
|
<ColourPicker colour={newColour} onChange={color => setNewColour(color)} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button style={{ color: "red" }} onClick={props.close}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
onClick={() => {
|
||||||
|
update();
|
||||||
|
props.close();
|
||||||
|
}}>
|
||||||
|
Update Marker
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</ResponsiveDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
146
src/maps/mapControllers/Geocoder.tsx
Normal file
146
src/maps/mapControllers/Geocoder.tsx
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
import { ControlPosition, useMap } from "react-map-gl/mapbox-legacy";
|
||||||
|
import MapboxGeocoder, { Result } from "@mapbox/mapbox-gl-geocoder";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
// export interface GeocoderObject {
|
||||||
|
// id: string;
|
||||||
|
// place_name: string;
|
||||||
|
// center: number[];
|
||||||
|
// place_type: string[];
|
||||||
|
// }
|
||||||
|
|
||||||
|
// interface GeocoderResult {
|
||||||
|
// result: GeocoderObject;
|
||||||
|
// }
|
||||||
|
interface Props {
|
||||||
|
mapboxAccessToken: string;
|
||||||
|
position: ControlPosition;
|
||||||
|
customEntries?: Result[];
|
||||||
|
customTransition?: (objectResult: Result) => void;
|
||||||
|
resultFunction?: (objectResult: Result) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
//const MAPBOX_TOKEN = process.env.REACT_APP_MAPBOX_ACCESS_TOKEN;
|
||||||
|
// const MAPBOX_TOKEN = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN;
|
||||||
|
|
||||||
|
// const geoCoder = new MapboxGeocoder({
|
||||||
|
// accessToken: MAPBOX_TOKEN ?? "",
|
||||||
|
// flyTo: true,
|
||||||
|
// marker: false,
|
||||||
|
// zoom: 18
|
||||||
|
// });
|
||||||
|
|
||||||
|
export default function Geocoder(props: Props) {
|
||||||
|
const { mapboxAccessToken, position, customEntries, resultFunction, customTransition } = props;
|
||||||
|
const { current: map } = useMap();
|
||||||
|
|
||||||
|
const localSearch = (query: string) => {
|
||||||
|
const matchesCoords = query.match(
|
||||||
|
/^[ ]*(?:Lat: )?(-?\d+\.?\d*)[, ]+(?:Lng: )?(-?\d+\.?\d*)[ ]*$/i
|
||||||
|
);
|
||||||
|
let matchingResults: Result[] = [];
|
||||||
|
if (matchesCoords) {
|
||||||
|
const coord1 = Number(matchesCoords[1]);
|
||||||
|
const coord2 = Number(matchesCoords[2]);
|
||||||
|
const matchingCoords: Result[] = [];
|
||||||
|
|
||||||
|
if (coord1 < -90 || coord1 > 90) {
|
||||||
|
// coord1 is longitude
|
||||||
|
matchingCoords.push({
|
||||||
|
id: "Lat: " + coord2 + " Lng: " + coord1,
|
||||||
|
place_name: "Lat: " + coord2 + " Lng: " + coord1,
|
||||||
|
center: [coord1, coord2],
|
||||||
|
place_type: ["coordinate"]
|
||||||
|
} as Result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (coord2 < -90 || coord2 > 90) {
|
||||||
|
// coord2 is longitude
|
||||||
|
matchingCoords.push({
|
||||||
|
id: "Lat: " + coord1 + " Lng: " + coord2,
|
||||||
|
place_name: "Lat: " + coord1 + " Lng: " + coord2,
|
||||||
|
center: [coord2, coord1],
|
||||||
|
place_type: ["coordinate"]
|
||||||
|
} as Result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchingCoords.length === 0) {
|
||||||
|
// else could be either lng, lat or lat, lng
|
||||||
|
matchingCoords.push({
|
||||||
|
id: "Lat: " + coord2 + " Lng: " + coord1,
|
||||||
|
place_name: "Lat: " + coord2 + " Lng: " + coord1,
|
||||||
|
center: [coord1, coord2],
|
||||||
|
place_type: ["coordinate"]
|
||||||
|
} as Result);
|
||||||
|
matchingCoords.push({
|
||||||
|
id: "Lat: " + coord1 + " Lng: " + coord2,
|
||||||
|
place_name: "Lat: " + coord1 + " Lng: " + coord2,
|
||||||
|
center: [coord2, coord1],
|
||||||
|
place_type: ["coordinate"]
|
||||||
|
} as Result);
|
||||||
|
}
|
||||||
|
|
||||||
|
matchingResults = matchingResults.concat(matchingCoords);
|
||||||
|
}
|
||||||
|
|
||||||
|
customEntries?.forEach(obj => {
|
||||||
|
if (obj.place_name.toLowerCase().includes(query.toLowerCase())) {
|
||||||
|
matchingResults.push(obj);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return matchingResults;
|
||||||
|
};
|
||||||
|
|
||||||
|
// //set the geocoder options
|
||||||
|
// useEffect(() => {
|
||||||
|
// //connects the localSearch function to the geocoder if there are custom entries passed in
|
||||||
|
// if (customEntries) {
|
||||||
|
// //need to determine a new way to handle custom search with updated geocoder
|
||||||
|
// //geoCoder.options.localGeocoder = localSearch;
|
||||||
|
// }
|
||||||
|
// //disables the transition that the geocoder would fire if a custom transition was passed in
|
||||||
|
// if (customTransition) {
|
||||||
|
// geoCoder.setFlyTo(false);
|
||||||
|
// }
|
||||||
|
// }, [customEntries, customTransition]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
//attach custom functions to event handlers
|
||||||
|
useEffect(() => {
|
||||||
|
const geoCoder = new MapboxGeocoder({
|
||||||
|
accessToken: mapboxAccessToken,
|
||||||
|
localGeocoder: localSearch,
|
||||||
|
flyTo: customTransition ? false : true,
|
||||||
|
marker: false,
|
||||||
|
zoom: 18,
|
||||||
|
});
|
||||||
|
//events that fire as the search bar is typed into, note the function will run AFTER the event fires
|
||||||
|
|
||||||
|
//event is fired when the search bar is cleared
|
||||||
|
geoCoder.on("clear", () => {});
|
||||||
|
|
||||||
|
//event is fired when starting to load the results
|
||||||
|
geoCoder.on("loading", () => {});
|
||||||
|
|
||||||
|
//event is fired when the results come back from a search
|
||||||
|
geoCoder.on("results", () => {});
|
||||||
|
|
||||||
|
//event is fired when there is an error
|
||||||
|
geoCoder.on("error", () => {});
|
||||||
|
|
||||||
|
//event is fired when an option is selected from the results
|
||||||
|
geoCoder.on("result", (res: Result) => {
|
||||||
|
if (resultFunction) {
|
||||||
|
resultFunction(res);
|
||||||
|
}
|
||||||
|
if (customTransition) {
|
||||||
|
customTransition(res);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
//once the map ref and access token are set up add the control to the map
|
||||||
|
if (map && mapboxAccessToken) {
|
||||||
|
map.addControl(geoCoder, position);
|
||||||
|
}
|
||||||
|
}, [map, mapboxAccessToken, position]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
112
src/maps/mapControllers/drawController.tsx
Normal file
112
src/maps/mapControllers/drawController.tsx
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import { useMap } from "react-map-gl/mapbox-legacy";
|
||||||
|
import { FeatureCollection } from "geojson";
|
||||||
|
import MapboxDraw from "@mapbox/mapbox-gl-draw";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
featureCollection: FeatureCollection;
|
||||||
|
editMode?: "drawPolygon" | "drawLine" | "drawPoint" | "edit" | "none" | "delete";
|
||||||
|
linePointLimit?: number;
|
||||||
|
measurement?: boolean;
|
||||||
|
onCreate: (event: any, objectToCut?: string, measurement?: boolean) => void;
|
||||||
|
onUpdate: (event: any) => void;
|
||||||
|
}
|
||||||
|
const draw = new MapboxDraw({
|
||||||
|
displayControlsDefault: false
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function DrawController(props: Props) {
|
||||||
|
const { featureCollection, editMode, onCreate, onUpdate, linePointLimit, measurement } = props;
|
||||||
|
//const [shapeToCut, setShapeToCut] = useState();
|
||||||
|
const cutRef = useRef(); //the object id for what shape to cut a hole
|
||||||
|
const mRef = useRef<boolean>();
|
||||||
|
const lineLimitRef = useRef<number | undefined>();
|
||||||
|
const pointsRef = useRef<number[][]>([]);
|
||||||
|
const { current: map } = useMap();
|
||||||
|
|
||||||
|
//use effect is just for adding and removing the controller from the map
|
||||||
|
useEffect(() => {
|
||||||
|
if (map) {
|
||||||
|
if (editMode && editMode !== "none" && !map.hasControl(draw)) {
|
||||||
|
map.addControl(draw);
|
||||||
|
} else if ((!editMode || editMode === "none") && map.hasControl(draw)) {
|
||||||
|
map.removeControl(draw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [map, editMode]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
lineLimitRef.current = linePointLimit;
|
||||||
|
mRef.current = measurement;
|
||||||
|
}, [linePointLimit, measurement]);
|
||||||
|
|
||||||
|
//useEffect for adding event handlers to the map
|
||||||
|
useEffect(() => {
|
||||||
|
if (map) {
|
||||||
|
map.on("draw.create", e => {
|
||||||
|
//console.log("draw create");
|
||||||
|
//using refs rather than state variables you can effectively get the state in an event listener
|
||||||
|
onCreate(e, cutRef.current, mRef.current);
|
||||||
|
});
|
||||||
|
map.on("draw.update", e => {
|
||||||
|
onUpdate(e);
|
||||||
|
});
|
||||||
|
map.on("click", ["shapefill", "shapeborder"], e => {
|
||||||
|
//set the ref to be undefined
|
||||||
|
cutRef.current = undefined;
|
||||||
|
if (map.hasControl(draw) && e.features) {
|
||||||
|
let mode = draw.getMode();
|
||||||
|
let id = e.features[0].properties?.objectKey;
|
||||||
|
if (mode === "simple_select" && id) {
|
||||||
|
if (draw.get(id)) {
|
||||||
|
draw.changeMode("direct_select", {
|
||||||
|
// The id of the feature that will be directly selected (required)
|
||||||
|
featureId: id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (mode === "draw_polygon") {
|
||||||
|
//if a feature was clicked on set ref to have its id
|
||||||
|
cutRef.current = id;
|
||||||
|
//setShapeToCut(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
map.on("click", e => {
|
||||||
|
if (map.hasControl(draw) && draw.getMode() === "draw_line_string") {
|
||||||
|
if (lineLimitRef.current) {
|
||||||
|
pointsRef.current.push([e.lngLat.lng, e.lngLat.lat]);
|
||||||
|
if (pointsRef.current.length === lineLimitRef.current) {
|
||||||
|
draw.changeMode("simple_select"); //changing the mode causes the create event to fire
|
||||||
|
pointsRef.current = [];
|
||||||
|
draw.changeMode("draw_line_string");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [map]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
//useEffect for changing draw mode for the controller based on the editor mode from the object controller
|
||||||
|
useEffect(() => {
|
||||||
|
if (editMode === "drawPolygon") {
|
||||||
|
draw.deleteAll();
|
||||||
|
draw.changeMode("draw_polygon");
|
||||||
|
} else if (editMode === "drawLine") {
|
||||||
|
draw.deleteAll();
|
||||||
|
draw.changeMode("draw_line_string");
|
||||||
|
} else if (editMode === "drawPoint") {
|
||||||
|
draw.deleteAll();
|
||||||
|
draw.changeMode("draw_point");
|
||||||
|
} else if (editMode === "delete") {
|
||||||
|
draw.deleteAll();
|
||||||
|
draw.changeMode("simple_select");
|
||||||
|
} else if (editMode === "edit") {
|
||||||
|
draw.set(featureCollection);
|
||||||
|
if (draw.getMode() !== "simple_select" && draw.getMode() !== "direct_select") {
|
||||||
|
draw.changeMode("simple_select");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [editMode, featureCollection]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
148
src/maps/mapDrawers/BinDrawer.tsx
Normal file
148
src/maps/mapDrawers/BinDrawer.tsx
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
import { Box } from "@mui/material";
|
||||||
|
import DisplayDrawer from "common/DisplayDrawer";
|
||||||
|
import MapMarkerSettings from "maps/MapMarkerSettings";
|
||||||
|
import { Bin as IBin } from "models";
|
||||||
|
import Bin from "pages/Bin";
|
||||||
|
import { pond } from "protobuf-ts/pond";
|
||||||
|
import { useBinAPI, useSnackbar } from "providers";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
selectedBin: string;
|
||||||
|
bins: Map<string, IBin>;
|
||||||
|
removeMarker: (key: string) => void;
|
||||||
|
updateMarker: (key: string, newSettings: pond.BinSettings) => void;
|
||||||
|
moveMap: (long: number, lat: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BinDrawer(props: Props) {
|
||||||
|
const { open, onClose, selectedBin, bins, removeMarker, updateMarker, moveMap } = props;
|
||||||
|
const [bin, setBin] = useState<IBin>(IBin.create());
|
||||||
|
const [openMarkerSettings, setOpenMarkerSettings] = useState(false);
|
||||||
|
const binAPI = useBinAPI();
|
||||||
|
const { openSnack } = useSnackbar();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let b = bins.get(selectedBin);
|
||||||
|
if (b) {
|
||||||
|
setBin(b);
|
||||||
|
}
|
||||||
|
}, [selectedBin, bins]);
|
||||||
|
|
||||||
|
const closeDrawer = () => {
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayNext = () => {
|
||||||
|
let binArr = Array.from(bins.values());
|
||||||
|
let index = binArr.indexOf(bin);
|
||||||
|
let found = false;
|
||||||
|
do {
|
||||||
|
if (index === binArr.length - 1) {
|
||||||
|
index = 0;
|
||||||
|
} else {
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
let nextBin = binArr[index];
|
||||||
|
let location = nextBin.location();
|
||||||
|
if (
|
||||||
|
location !== null &&
|
||||||
|
location !== undefined &&
|
||||||
|
location.longitude !== 0 &&
|
||||||
|
location.longitude !== 0
|
||||||
|
) {
|
||||||
|
setBin(nextBin);
|
||||||
|
moveMap(location.longitude, location.latitude);
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
} while (!found);
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayPrev = () => {
|
||||||
|
let binArr = Array.from(bins.values());
|
||||||
|
let index = binArr.indexOf(bin);
|
||||||
|
let found = false;
|
||||||
|
do {
|
||||||
|
if (index === 0) {
|
||||||
|
index = binArr.length - 1;
|
||||||
|
} else {
|
||||||
|
index--;
|
||||||
|
}
|
||||||
|
let nextBin = binArr[index];
|
||||||
|
let location = nextBin.location();
|
||||||
|
if (
|
||||||
|
location !== null &&
|
||||||
|
location !== undefined &&
|
||||||
|
location.longitude !== 0 &&
|
||||||
|
location.longitude !== 0
|
||||||
|
) {
|
||||||
|
setBin(nextBin);
|
||||||
|
moveMap(location.longitude, location.latitude);
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
} while (!found);
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawerBody = () => {
|
||||||
|
return <Box>{bin.key() !== "" && <Bin binKey={bin.key()} displayMobile fromMap />}</Box>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const remove = () => {
|
||||||
|
let settings = bin.settings;
|
||||||
|
settings.location = null;
|
||||||
|
|
||||||
|
binAPI
|
||||||
|
.updateBin(bin.key(), settings)
|
||||||
|
.then(resp => {
|
||||||
|
openSnack("Removed bin marker");
|
||||||
|
removeMarker(bin.key());
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
openSnack("Failed to remove bin marker");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const update = () => {
|
||||||
|
setOpenMarkerSettings(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<DisplayDrawer
|
||||||
|
open={open}
|
||||||
|
onClose={closeDrawer}
|
||||||
|
displayNext={displayNext}
|
||||||
|
displayPrev={displayPrev}
|
||||||
|
title={bin.name()}
|
||||||
|
width={"40vw"}
|
||||||
|
drawerBody={drawerBody()}
|
||||||
|
updateElement={update}
|
||||||
|
removeElement={remove}
|
||||||
|
/>
|
||||||
|
<MapMarkerSettings
|
||||||
|
close={() => {
|
||||||
|
setOpenMarkerSettings(false);
|
||||||
|
}}
|
||||||
|
open={openMarkerSettings}
|
||||||
|
theme={bin.settings.theme ?? pond.ObjectTheme.create()}
|
||||||
|
sizeControl
|
||||||
|
currentSize={bin.settings.theme?.height}
|
||||||
|
updateObject={newTheme => {
|
||||||
|
let settings = bin.settings;
|
||||||
|
settings.theme = newTheme;
|
||||||
|
binAPI
|
||||||
|
.updateBin(bin.key(), settings)
|
||||||
|
.then(resp => {
|
||||||
|
openSnack("marker settings updated");
|
||||||
|
updateMarker(bin.key(), settings);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
openSnack("failed to update marker settings");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
153
src/maps/mapDrawers/BinYardDrawer.tsx
Normal file
153
src/maps/mapDrawers/BinYardDrawer.tsx
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
import BinyardDisplay from "bin/BinyardDisplay";
|
||||||
|
import DisplayDrawer from "common/DisplayDrawer";
|
||||||
|
import MapMarkerSettings from "maps/MapMarkerSettings";
|
||||||
|
//import Bins from "pages/Bins";
|
||||||
|
import { pond } from "protobuf-ts/pond";
|
||||||
|
import { useBinYardAPI, useSnackbar } from "providers";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
selectedYard: string;
|
||||||
|
yards: Map<string, pond.BinYardSettings>;
|
||||||
|
removeMarker: (key: string) => void;
|
||||||
|
updateMarker: (key: string, objectSettings: pond.BinYardSettings) => void;
|
||||||
|
moveMap: (long: number, lat: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BinYardDrawer(props: Props) {
|
||||||
|
const { open, onClose, selectedYard, yards, removeMarker, updateMarker, moveMap } = props;
|
||||||
|
const [yard, setYard] = useState<pond.BinYardSettings>(pond.BinYardSettings.create());
|
||||||
|
const [openMarkerSettings, setOpenMarkerSettings] = useState(false);
|
||||||
|
const { openSnack } = useSnackbar();
|
||||||
|
const binyardAPI = useBinYardAPI();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let yard = yards.get(selectedYard);
|
||||||
|
if (yard) {
|
||||||
|
setYard(yard);
|
||||||
|
}
|
||||||
|
}, [selectedYard, yards]);
|
||||||
|
|
||||||
|
const closeDrawer = () => {
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayNext = () => {
|
||||||
|
let yardArr = Array.from(yards.values());
|
||||||
|
let index = yardArr.indexOf(yard);
|
||||||
|
let found = false;
|
||||||
|
do {
|
||||||
|
if (index === yardArr.length - 1) {
|
||||||
|
index = 0;
|
||||||
|
} else {
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
let nextYard = yardArr[index];
|
||||||
|
if (
|
||||||
|
nextYard.latitude !== 0 &&
|
||||||
|
nextYard.longitude !== 0 &&
|
||||||
|
nextYard.latitude !== undefined &&
|
||||||
|
nextYard.longitude !== undefined
|
||||||
|
) {
|
||||||
|
setYard(nextYard);
|
||||||
|
moveMap(nextYard.longitude, nextYard.latitude);
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
} while (!found);
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayPrev = () => {
|
||||||
|
let yardArr = Array.from(yards.values());
|
||||||
|
let index = yardArr.indexOf(yard);
|
||||||
|
let found = false;
|
||||||
|
do {
|
||||||
|
if (index === 0) {
|
||||||
|
index = yardArr.length - 1;
|
||||||
|
} else {
|
||||||
|
index--;
|
||||||
|
}
|
||||||
|
let nextYard = yardArr[index];
|
||||||
|
if (
|
||||||
|
nextYard.latitude !== 0 &&
|
||||||
|
nextYard.longitude !== 0 &&
|
||||||
|
nextYard.latitude !== undefined &&
|
||||||
|
nextYard.longitude !== undefined
|
||||||
|
) {
|
||||||
|
setYard(nextYard);
|
||||||
|
moveMap(nextYard.longitude, nextYard.latitude);
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
} while (!found);
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawerBody = () => {
|
||||||
|
//return <Bins insert yardFilter={yard.key} />;
|
||||||
|
return <BinyardDisplay insert yardKey={yard.key} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* function to remove the marker and coordinates from the object
|
||||||
|
*/
|
||||||
|
const remove = () => {
|
||||||
|
//set the long/lat of the yard to 0 and call an update
|
||||||
|
let settings = yard;
|
||||||
|
settings.longitude = 0;
|
||||||
|
settings.latitude = 0;
|
||||||
|
binyardAPI
|
||||||
|
.updateBinYard(yard.key, settings)
|
||||||
|
.then(resp => {
|
||||||
|
openSnack("Marker Removed");
|
||||||
|
//then use the removeMarker prop function to update the markers in the parent map
|
||||||
|
removeMarker(yard.key);
|
||||||
|
})
|
||||||
|
.catch(resp => {
|
||||||
|
openSnack("there was a problem removing the marker");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* function to update the settings of the marker
|
||||||
|
*/
|
||||||
|
const update = () => {
|
||||||
|
setOpenMarkerSettings(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<DisplayDrawer
|
||||||
|
open={open}
|
||||||
|
onClose={closeDrawer}
|
||||||
|
displayNext={displayNext}
|
||||||
|
displayPrev={displayPrev}
|
||||||
|
title={yard.name}
|
||||||
|
width={"40vw"}
|
||||||
|
drawerBody={drawerBody()}
|
||||||
|
updateElement={update}
|
||||||
|
removeElement={remove}
|
||||||
|
/>
|
||||||
|
<MapMarkerSettings
|
||||||
|
close={() => {
|
||||||
|
setOpenMarkerSettings(false);
|
||||||
|
}}
|
||||||
|
open={openMarkerSettings}
|
||||||
|
theme={yard.theme ?? pond.ObjectTheme.create()}
|
||||||
|
colourControl
|
||||||
|
updateObject={newTheme => {
|
||||||
|
let settings = yard;
|
||||||
|
settings.theme = newTheme;
|
||||||
|
binyardAPI
|
||||||
|
.updateBinYard(yard.key, settings)
|
||||||
|
.then(resp => {
|
||||||
|
openSnack("marker settings updated");
|
||||||
|
updateMarker(yard.key, settings);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
openSnack("failed to update marker settings");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
153
src/maps/mapDrawers/DeviceDrawer.tsx
Normal file
153
src/maps/mapDrawers/DeviceDrawer.tsx
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
import { Box } from "@mui/material";
|
||||||
|
import DisplayDrawer from "common/DisplayDrawer";
|
||||||
|
import DeviceViewer from "device/DeviceViewer";
|
||||||
|
import MapMarkerSettings from "maps/MapMarkerSettings";
|
||||||
|
import { Device } from "models";
|
||||||
|
import { pond } from "protobuf-ts/pond";
|
||||||
|
import { useDeviceAPI, useSnackbar } from "providers";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
selectedDevice: string;
|
||||||
|
devices: Map<string, Device>;
|
||||||
|
removeMarker: (key: string) => void;
|
||||||
|
updateMarker: (key: string, newSettings: pond.DeviceSettings) => void;
|
||||||
|
moveMap: (long: number, lat: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DeviceDrawer(props: Props) {
|
||||||
|
const { open, onClose, selectedDevice, devices, removeMarker, updateMarker, moveMap } = props;
|
||||||
|
const [device, setDevice] = useState<Device>(Device.create());
|
||||||
|
const [openMarkerSettings, setOpenMarkerSettings] = useState(false);
|
||||||
|
const deviceAPI = useDeviceAPI();
|
||||||
|
const { openSnack } = useSnackbar();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let d = devices.get(selectedDevice);
|
||||||
|
if (d) {
|
||||||
|
setDevice(d);
|
||||||
|
}
|
||||||
|
}, [selectedDevice, devices]);
|
||||||
|
|
||||||
|
const closeDrawer = () => {
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayNext = () => {
|
||||||
|
let devArr = Array.from(devices.values());
|
||||||
|
let index = devArr.indexOf(device);
|
||||||
|
let found = false;
|
||||||
|
do {
|
||||||
|
if (index === devArr.length - 1) {
|
||||||
|
index = 0;
|
||||||
|
} else {
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
let nextDev = devArr[index];
|
||||||
|
if (
|
||||||
|
nextDev.location().longitude !== 0 &&
|
||||||
|
nextDev.location().longitude !== 0 &&
|
||||||
|
nextDev.location().latitude !== undefined &&
|
||||||
|
nextDev.location().longitude !== undefined
|
||||||
|
) {
|
||||||
|
setDevice(nextDev);
|
||||||
|
moveMap(nextDev.location().longitude, nextDev.location().latitude);
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
} while (!found);
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayPrev = () => {
|
||||||
|
let devArr = Array.from(devices.values());
|
||||||
|
let index = devArr.indexOf(device);
|
||||||
|
let found = false;
|
||||||
|
do {
|
||||||
|
if (index === 0) {
|
||||||
|
index = devArr.length - 1;
|
||||||
|
} else {
|
||||||
|
index--;
|
||||||
|
}
|
||||||
|
let nextDev = devArr[index];
|
||||||
|
if (
|
||||||
|
nextDev.location().longitude !== 0 &&
|
||||||
|
nextDev.location().longitude !== 0 &&
|
||||||
|
nextDev.location().latitude !== undefined &&
|
||||||
|
nextDev.location().longitude !== undefined
|
||||||
|
) {
|
||||||
|
setDevice(nextDev);
|
||||||
|
moveMap(nextDev.location().longitude, nextDev.location().latitude);
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
} while (!found);
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawerBody = () => {
|
||||||
|
return device.id() !== 0 ? <DeviceViewer device={device} isMobile={true} /> : <Box></Box>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* function to remove the marker and coordinates from the object
|
||||||
|
*/
|
||||||
|
const remove = () => {
|
||||||
|
//set the long/lat of the yard to 0 and call an update
|
||||||
|
let settings = device.settings;
|
||||||
|
settings.longitude = 0;
|
||||||
|
settings.latitude = 0;
|
||||||
|
deviceAPI
|
||||||
|
.update(device.id(), settings)
|
||||||
|
.then(resp => {
|
||||||
|
openSnack("Marker Removed");
|
||||||
|
//then use the removeMarker prop function to update the markers in the parent map
|
||||||
|
removeMarker(device.id().toString());
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
openSnack("Failed to remove marker");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* function to update the settings of the marker
|
||||||
|
*/
|
||||||
|
const update = () => {
|
||||||
|
setOpenMarkerSettings(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<DisplayDrawer
|
||||||
|
open={open}
|
||||||
|
onClose={closeDrawer}
|
||||||
|
displayNext={displayNext}
|
||||||
|
displayPrev={displayPrev}
|
||||||
|
title={device.name()}
|
||||||
|
width={"40vw"}
|
||||||
|
drawerBody={drawerBody()}
|
||||||
|
updateElement={update}
|
||||||
|
removeElement={remove}
|
||||||
|
/>
|
||||||
|
<MapMarkerSettings
|
||||||
|
close={() => {
|
||||||
|
setOpenMarkerSettings(false);
|
||||||
|
}}
|
||||||
|
open={openMarkerSettings}
|
||||||
|
theme={device.settings.theme ?? pond.ObjectTheme.create()}
|
||||||
|
colourControl
|
||||||
|
updateObject={newTheme => {
|
||||||
|
let settings = device.settings;
|
||||||
|
settings.theme = newTheme;
|
||||||
|
deviceAPI
|
||||||
|
.update(device.id(), settings)
|
||||||
|
.then(resp => {
|
||||||
|
openSnack("marker settings updated");
|
||||||
|
updateMarker(device.id().toString(), settings);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
openSnack("failed to update marker settings");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
326
src/maps/mapDrawers/FieldDrawer.tsx
Normal file
326
src/maps/mapDrawers/FieldDrawer.tsx
Normal file
|
|
@ -0,0 +1,326 @@
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Divider,
|
||||||
|
Drawer,
|
||||||
|
Grid2 as Grid,
|
||||||
|
IconButton,
|
||||||
|
Tab,
|
||||||
|
Tabs,
|
||||||
|
Theme,
|
||||||
|
Typography
|
||||||
|
} from "@mui/material";
|
||||||
|
import { Notes } from "@mui/icons-material";
|
||||||
|
import DisplayDrawer from "common/DisplayDrawer";
|
||||||
|
//import HarvestPlanDisplay from "harvestPlan/HarvestPlanDisplay";
|
||||||
|
import { Field, fieldScope, /*HarvestPlan,*/ teamScope } from "models";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
//import TaskViewer from "tasks/TaskViewer";
|
||||||
|
import Weather from "weather/weather";
|
||||||
|
import { getThemeType } from "theme";
|
||||||
|
import GrainDescriber from "grain/GrainDescriber";
|
||||||
|
import Chat from "chat/Chat";
|
||||||
|
import { pond } from "protobuf-ts/pond";
|
||||||
|
import { useMobile } from "hooks";
|
||||||
|
import { useGlobalState, /*useHarvestPlanAPI,*/ useUserAPI } from "providers";
|
||||||
|
import { makeStyles } from "@mui/styles";
|
||||||
|
//import FieldActions from "field/FieldActions";
|
||||||
|
|
||||||
|
interface TabPanelProps {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
index: any;
|
||||||
|
value: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabPanelMine(props: TabPanelProps) {
|
||||||
|
const { children, value, index, ...other } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="tabpanel"
|
||||||
|
hidden={value !== index}
|
||||||
|
aria-labelledby={`simple-tab-${index}`}
|
||||||
|
style={{ height: "94%", paddingTop: "15px" }}
|
||||||
|
{...other}>
|
||||||
|
{value === index && <React.Fragment>{children}</React.Fragment>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
interface Props {
|
||||||
|
open: boolean;
|
||||||
|
closeDrawer: () => void;
|
||||||
|
selectedFieldKey: string;
|
||||||
|
fields: Map<string, Field>;
|
||||||
|
openSettings: (fieldKey: string) => void;
|
||||||
|
moveMap: (long: number, lat: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) => ({
|
||||||
|
avatar: {
|
||||||
|
color: getThemeType() === "light" ? theme.palette.common.black : theme.palette.common.white,
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
width: theme.spacing(5),
|
||||||
|
height: theme.spacing(5),
|
||||||
|
border: "1px solid",
|
||||||
|
borderColor: theme.palette.divider
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
backgroundColor: getThemeType() === "light" ? "rgb(245, 245, 245)" : "rgb(50, 50, 50)",
|
||||||
|
padding: 5
|
||||||
|
},
|
||||||
|
light: {
|
||||||
|
backgroundColor: getThemeType() === "light" ? "rgb(235, 235, 235)" : "rgb(60, 60, 60)",
|
||||||
|
padding: 5
|
||||||
|
}
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function FieldDrawer(props: Props) {
|
||||||
|
const { open, closeDrawer, selectedFieldKey, fields, moveMap } = props;
|
||||||
|
const [field, setField] = useState<Field>(Field.create());
|
||||||
|
//const [hPlan, setHPlan] = useState<HarvestPlan>(HarvestPlan.create());
|
||||||
|
const [value, setValue] = useState(0);
|
||||||
|
const classes = useStyles();
|
||||||
|
const [openNote, setOpenNote] = useState(false);
|
||||||
|
const isMobile = useMobile();
|
||||||
|
const [{ as, user }] = useGlobalState();
|
||||||
|
//const hPlanAPI = useHarvestPlanAPI();
|
||||||
|
const [planLoading, setPlanLoading] = useState(false);
|
||||||
|
const userAPI = useUserAPI();
|
||||||
|
const [permissions, setPermissions] = useState<pond.Permission[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let field = fields.get(selectedFieldKey);
|
||||||
|
if (field) {
|
||||||
|
setField(field);
|
||||||
|
}
|
||||||
|
}, [selectedFieldKey, fields]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let scope;
|
||||||
|
if (as) {
|
||||||
|
//if they are viewing as a team regardless of whether the field is adaptive or external (from JD or CNH) get the users permission to the team
|
||||||
|
scope = teamScope(as);
|
||||||
|
} else if (
|
||||||
|
field.settings.fieldGeoData &&
|
||||||
|
field.settings.fieldGeoData.origin === pond.DataOrigin.DATA_ORIGIN_ADAPTIVE
|
||||||
|
) {
|
||||||
|
//if they are viewing as a user and the field is an adaptive field get the users permission to the field
|
||||||
|
scope = fieldScope(field.key());
|
||||||
|
}
|
||||||
|
|
||||||
|
//if the scope was set get the permissions the user has in that scope
|
||||||
|
if (scope) {
|
||||||
|
userAPI.getUser(user.id(), scope).then(resp => {
|
||||||
|
field.permissions = resp.permissions;
|
||||||
|
setPermissions(resp.permissions);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
//if scope wasn't set that means that they are viewing an external field as a user and so will have full permissions
|
||||||
|
//because they would have full permissions to the organization that the field is linked to since users cannot share organizations
|
||||||
|
//and they are only linked to team/user combinations during creation and cannot be shared afterward
|
||||||
|
let perms = [
|
||||||
|
pond.Permission.PERMISSION_READ,
|
||||||
|
pond.Permission.PERMISSION_WRITE,
|
||||||
|
pond.Permission.PERMISSION_SHARE,
|
||||||
|
pond.Permission.PERMISSION_USERS
|
||||||
|
];
|
||||||
|
field.permissions = perms;
|
||||||
|
setPermissions(perms);
|
||||||
|
}
|
||||||
|
}, [as, user, userAPI, field]); //eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// if (field.key() !== "") {
|
||||||
|
// hPlanAPI
|
||||||
|
// .listHarvestPlans(1, 0, "desc", "createDate", field.key(), as)
|
||||||
|
// .then(resp => {
|
||||||
|
// if (resp.data.harvestPlan.length > 0) {
|
||||||
|
// let plan = resp.data.harvestPlan[0];
|
||||||
|
// setHPlan(HarvestPlan.any(plan));
|
||||||
|
// } else {
|
||||||
|
// setHPlan(HarvestPlan.create());
|
||||||
|
// }
|
||||||
|
// setPlanLoading(false);
|
||||||
|
// })
|
||||||
|
// .catch(err => {
|
||||||
|
// //openSnack("Failed to load plan");
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// }, [field, as, hPlanAPI]);
|
||||||
|
|
||||||
|
const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => {
|
||||||
|
setValue(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayNext = () => {
|
||||||
|
let arr = Array.from(fields.values());
|
||||||
|
let index = arr.indexOf(field);
|
||||||
|
let found = false;
|
||||||
|
do {
|
||||||
|
if (index === arr.length - 1) {
|
||||||
|
index = 0;
|
||||||
|
} else {
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
let nextField = arr[index];
|
||||||
|
let location = nextField.center();
|
||||||
|
if (
|
||||||
|
location !== null &&
|
||||||
|
location !== undefined &&
|
||||||
|
location.longitude !== 0 &&
|
||||||
|
location.longitude !== 0
|
||||||
|
) {
|
||||||
|
setField(nextField);
|
||||||
|
moveMap(location.longitude, location.latitude);
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
} while (!found);
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayPrev = () => {
|
||||||
|
let arr = Array.from(fields.values());
|
||||||
|
let index = arr.indexOf(field);
|
||||||
|
let found = false;
|
||||||
|
do {
|
||||||
|
if (index === 0) {
|
||||||
|
index = arr.length - 1;
|
||||||
|
} else {
|
||||||
|
index--;
|
||||||
|
}
|
||||||
|
let nextField = arr[index];
|
||||||
|
let location = nextField.center();
|
||||||
|
if (
|
||||||
|
location !== null &&
|
||||||
|
location !== undefined &&
|
||||||
|
location.longitude !== 0 &&
|
||||||
|
location.longitude !== 0
|
||||||
|
) {
|
||||||
|
setField(nextField);
|
||||||
|
moveMap(location.longitude, location.latitude);
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
} while (!found);
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawerBody = () => {
|
||||||
|
let taskLoadKeys: string[] = [];
|
||||||
|
if (!planLoading) {
|
||||||
|
field.key() !== "" && taskLoadKeys.push(field.key());
|
||||||
|
//hPlan.key() !== "" && taskLoadKeys.push(hPlan.key());
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Tabs
|
||||||
|
style={{ position: "sticky" }}
|
||||||
|
value={value}
|
||||||
|
centered
|
||||||
|
onChange={handleChange}
|
||||||
|
TabIndicatorProps={{ style: { background: "rgba(255,255,0,255)" } }}>
|
||||||
|
<Tab label="Field Overview" />
|
||||||
|
<Tab label="Weather" />
|
||||||
|
<Tab label="Field Activities" />
|
||||||
|
</Tabs>
|
||||||
|
<TabPanelMine value={value} index={0}>
|
||||||
|
<Box paddingLeft={2} paddingRight={2} paddingBottom={5}>
|
||||||
|
<IconButton className={classes.avatar} onClick={() => setOpenNote(true)}>
|
||||||
|
<Notes />
|
||||||
|
</IconButton>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h5" style={{ fontWeight: 700 }}>
|
||||||
|
{field.fieldName()} Details
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Grid container direction="column" alignItems="center">
|
||||||
|
<Grid container direction="row" className={classes.dark}>
|
||||||
|
<Grid>Approximate Area:</Grid>
|
||||||
|
<Grid>{field.acres()}ac</Grid>
|
||||||
|
</Grid>
|
||||||
|
<Grid container direction="row" className={classes.light}>
|
||||||
|
<Grid>Grain:</Grid>
|
||||||
|
<Grid>
|
||||||
|
{field.grain() === pond.Grain.GRAIN_CUSTOM
|
||||||
|
? field.customType()
|
||||||
|
: GrainDescriber(field.crop()).name}
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<Grid container direction="row" className={classes.dark}>
|
||||||
|
<Grid>Grain Variant:</Grid>
|
||||||
|
<Grid>{field.settings.grainSubtype}</Grid>
|
||||||
|
</Grid>
|
||||||
|
<Grid container direction="row" className={classes.light}>
|
||||||
|
<Grid>Land Location:</Grid>
|
||||||
|
<Grid>{field.landLoc()}</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<Box padding={3}>
|
||||||
|
<Divider style={{ padding: 2 }} />
|
||||||
|
</Box>
|
||||||
|
{/* <HarvestPlanDisplay
|
||||||
|
plan={hPlan}
|
||||||
|
permissions={permissions}
|
||||||
|
planField={field}
|
||||||
|
loading={planLoading}
|
||||||
|
fieldList={Array.from(fields.values())}
|
||||||
|
changePlan={updatedPlan => {
|
||||||
|
if (updatedPlan) {
|
||||||
|
setHPlan(updatedPlan);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/> */}
|
||||||
|
</Box>
|
||||||
|
</TabPanelMine>
|
||||||
|
<TabPanelMine value={value} index={1}>
|
||||||
|
<Weather longitude={field.center().longitude} latitude={field.center().latitude} />
|
||||||
|
</TabPanelMine>
|
||||||
|
<TabPanelMine value={value} index={2}>
|
||||||
|
{/* <TaskViewer drawerView objectKey={field.key()} loadKeys={taskLoadKeys} /> */}
|
||||||
|
</TabPanelMine>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const noteDrawer = () => {
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
anchor={isMobile ? "bottom" : "right"}
|
||||||
|
open={openNote}
|
||||||
|
onClose={() => setOpenNote(false)}>
|
||||||
|
<Box height={isMobile ? "50vh" : "100vh"} padding={2}>
|
||||||
|
<Typography style={{ fontWeight: 650 }}>Notes</Typography>
|
||||||
|
<Chat type={pond.NoteType.NOTE_TYPE_FIELD} objectKey={field.key()} />
|
||||||
|
</Box>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<DisplayDrawer
|
||||||
|
open={open}
|
||||||
|
onClose={closeDrawer}
|
||||||
|
displayNext={displayNext}
|
||||||
|
displayPrev={displayPrev}
|
||||||
|
title={field.name()}
|
||||||
|
width={"40vw"}
|
||||||
|
drawerBody={drawerBody()}
|
||||||
|
updateElement={
|
||||||
|
field.settings.fieldGeoData &&
|
||||||
|
field.settings.fieldGeoData.origin === pond.DataOrigin.DATA_ORIGIN_ADAPTIVE
|
||||||
|
? () => {
|
||||||
|
props.openSettings(selectedFieldKey);
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
// objectActions={
|
||||||
|
// field.settings.fieldGeoData &&
|
||||||
|
// field.settings.fieldGeoData.origin === pond.DataOrigin.DATA_ORIGIN_ADAPTIVE ? (
|
||||||
|
// <FieldActions field={field} permissions={permissions} refreshCallback={() => {}} />
|
||||||
|
// ) : (
|
||||||
|
// undefined
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
/>
|
||||||
|
{noteDrawer()}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
148
src/maps/mapDrawers/GrainBagDrawer.tsx
Normal file
148
src/maps/mapDrawers/GrainBagDrawer.tsx
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
import { Box } from "@mui/material";
|
||||||
|
import DisplayDrawer from "common/DisplayDrawer";
|
||||||
|
import MapMarkerSettings from "maps/MapMarkerSettings";
|
||||||
|
import { GrainBag as BagModel } from "models/GrainBag";
|
||||||
|
//import GrainBag from "pages/grainBag";
|
||||||
|
import { pond } from "protobuf-ts/pond";
|
||||||
|
import { useGrainBagAPI, useSnackbar } from "providers";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
selectedBag: string;
|
||||||
|
grainBags: Map<string, BagModel>;
|
||||||
|
removeMarker: (key: string) => void;
|
||||||
|
updateMarker: (key: string, newSettings: pond.GrainBagSettings) => void;
|
||||||
|
moveMap: (long: number, lat: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function GrainBagDrawer(props: Props) {
|
||||||
|
const { open, onClose, selectedBag, grainBags, removeMarker, updateMarker, moveMap } = props;
|
||||||
|
const [bag, setBag] = useState<BagModel>(BagModel.create());
|
||||||
|
const [openMarkerSettings, setOpenMarkerSettings] = useState(false);
|
||||||
|
const bagAPI = useGrainBagAPI();
|
||||||
|
const { openSnack } = useSnackbar();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let b = grainBags.get(selectedBag);
|
||||||
|
if (b) {
|
||||||
|
setBag(b);
|
||||||
|
}
|
||||||
|
}, [selectedBag, grainBags]);
|
||||||
|
|
||||||
|
const closeDrawer = () => {
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayNext = () => {
|
||||||
|
let bagArr = Array.from(grainBags.values());
|
||||||
|
let index = bagArr.indexOf(bag);
|
||||||
|
let found = false;
|
||||||
|
do {
|
||||||
|
if (index === bagArr.length - 1) {
|
||||||
|
index = 0;
|
||||||
|
} else {
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
let nextBag = bagArr[index];
|
||||||
|
let location = nextBag.centerLocation();
|
||||||
|
if (
|
||||||
|
location !== null &&
|
||||||
|
location !== undefined &&
|
||||||
|
location.longitude !== 0 &&
|
||||||
|
location.longitude !== 0
|
||||||
|
) {
|
||||||
|
setBag(nextBag);
|
||||||
|
moveMap(location.longitude, location.latitude);
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
} while (!found);
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayPrev = () => {
|
||||||
|
let bagArr = Array.from(grainBags.values());
|
||||||
|
let index = bagArr.indexOf(bag);
|
||||||
|
let found = false;
|
||||||
|
do {
|
||||||
|
if (index === 0) {
|
||||||
|
index = bagArr.length - 1;
|
||||||
|
} else {
|
||||||
|
index--;
|
||||||
|
}
|
||||||
|
let nextBag = bagArr[index];
|
||||||
|
let location = nextBag.centerLocation();
|
||||||
|
if (
|
||||||
|
location !== null &&
|
||||||
|
location !== undefined &&
|
||||||
|
location.longitude !== 0 &&
|
||||||
|
location.longitude !== 0
|
||||||
|
) {
|
||||||
|
setBag(nextBag);
|
||||||
|
moveMap(location.longitude, location.latitude);
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
} while (!found);
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawerBody = () => {
|
||||||
|
//return <Box>{bag.key() !== "" && <GrainBag bagKey={bag.key()} mobileView />}</Box>;
|
||||||
|
return <Box></Box>
|
||||||
|
};
|
||||||
|
|
||||||
|
const update = () => {
|
||||||
|
setOpenMarkerSettings(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const remove = () => {
|
||||||
|
let settings = bag.settings;
|
||||||
|
settings.startLocation = undefined;
|
||||||
|
settings.endLocation = undefined;
|
||||||
|
bagAPI
|
||||||
|
.updateGrainBag(bag.key(), bag.name(), settings)
|
||||||
|
.then(resp => {
|
||||||
|
openSnack("Grain bag location removed");
|
||||||
|
removeMarker(bag.key());
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
openSnack("Failed to remove grain bag location");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<DisplayDrawer
|
||||||
|
open={open}
|
||||||
|
onClose={closeDrawer}
|
||||||
|
displayNext={displayNext}
|
||||||
|
displayPrev={displayPrev}
|
||||||
|
title={bag.name()}
|
||||||
|
width={"40vw"}
|
||||||
|
drawerBody={drawerBody()}
|
||||||
|
updateElement={update}
|
||||||
|
removeElement={remove}
|
||||||
|
/>
|
||||||
|
<MapMarkerSettings
|
||||||
|
close={() => {
|
||||||
|
setOpenMarkerSettings(false);
|
||||||
|
}}
|
||||||
|
open={openMarkerSettings}
|
||||||
|
colourControl
|
||||||
|
theme={bag.settings.theme ?? pond.ObjectTheme.create()}
|
||||||
|
updateObject={newTheme => {
|
||||||
|
let settings = bag.settings;
|
||||||
|
settings.theme = newTheme;
|
||||||
|
bagAPI
|
||||||
|
.updateGrainBag(bag.key(), bag.name(), settings)
|
||||||
|
.then(resp => {
|
||||||
|
openSnack("marker settings updated");
|
||||||
|
updateMarker(bag.key(), settings);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
openSnack("failed to update marker settings");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
95
src/maps/mapLayers/geoMapLayer.tsx
Normal file
95
src/maps/mapLayers/geoMapLayer.tsx
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { Layer, Source } from "react-map-gl/mapbox-legacy";
|
||||||
|
import { FeatureCollection } from "geojson";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
objectCollection: FeatureCollection;
|
||||||
|
measurementCollection?: FeatureCollection;
|
||||||
|
showTitle?: boolean;
|
||||||
|
setInteractiveLayers: (layers: string[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function GeoMapLayer(props: Props) {
|
||||||
|
const { objectCollection, measurementCollection, showTitle, setInteractiveLayers } = props;
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// console.log(objectCollection);
|
||||||
|
// console.log(measurementCollection);
|
||||||
|
// }, [measurementCollection, objectCollection]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setInteractiveLayers(["shapefill", "shapeborder", "measurementBorder"]);
|
||||||
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Source type="geojson" data={objectCollection} id={"objects"}>
|
||||||
|
<Layer
|
||||||
|
id="shapefill"
|
||||||
|
type="fill"
|
||||||
|
paint={{
|
||||||
|
"fill-color": ["get", "fill"], //using react-map-gl's data driven expressions you can get data from the properties of the feature
|
||||||
|
"fill-opacity": 0.7
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Layer
|
||||||
|
id="shapeborder"
|
||||||
|
type="line"
|
||||||
|
layout={{
|
||||||
|
"line-join": "round",
|
||||||
|
"line-cap": "round"
|
||||||
|
}}
|
||||||
|
paint={{
|
||||||
|
"line-color": "white",
|
||||||
|
"line-width": ["get", "lineWidth"]
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{showTitle && (
|
||||||
|
<Layer
|
||||||
|
id="shapetitle"
|
||||||
|
type="symbol"
|
||||||
|
layout={{
|
||||||
|
"text-field": ["get", "title"],
|
||||||
|
"text-size": 18,
|
||||||
|
"text-font": ["Open Sans Bold"],
|
||||||
|
"text-anchor": "center"
|
||||||
|
}}
|
||||||
|
paint={{
|
||||||
|
"text-color": "white"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Source>
|
||||||
|
<Source type="geojson" data={measurementCollection} id={"measurements"}>
|
||||||
|
<Layer
|
||||||
|
id="measurementBorder"
|
||||||
|
type="line"
|
||||||
|
layout={{
|
||||||
|
"line-join": "round",
|
||||||
|
"line-cap": "round"
|
||||||
|
}}
|
||||||
|
paint={{
|
||||||
|
"line-color": "white",
|
||||||
|
"line-width": ["get", "lineWidth"]
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Layer
|
||||||
|
id="totalDistance"
|
||||||
|
type="symbol"
|
||||||
|
layout={{
|
||||||
|
"text-field": ["get", "totalDist"],
|
||||||
|
"text-size": 18,
|
||||||
|
"text-font": ["Open Sans Bold"],
|
||||||
|
//"symbol-placement": "line-center",
|
||||||
|
"text-anchor": "bottom-left",
|
||||||
|
"icon-allow-overlap": true,
|
||||||
|
"text-offset": [0, -1]
|
||||||
|
}}
|
||||||
|
paint={{
|
||||||
|
"text-color": "white"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Source>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
247
src/maps/mapMarkers/Markers.tsx
Normal file
247
src/maps/mapMarkers/Markers.tsx
Normal file
|
|
@ -0,0 +1,247 @@
|
||||||
|
import { Box,Grid2 as Grid, Theme, Tooltip, Typography } from "@mui/material";
|
||||||
|
import { makeStyles } from "@mui/styles";
|
||||||
|
import MiniPie from "charts/MiniPie";
|
||||||
|
import { useMobile } from "hooks";
|
||||||
|
import { clone } from "lodash";
|
||||||
|
import { pond } from "protobuf-ts/pond";
|
||||||
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
|
import { Marker } from "react-map-gl/mapbox-legacy";
|
||||||
|
|
||||||
|
//interface for markers
|
||||||
|
export interface MarkerData {
|
||||||
|
title: string; //displayed in the tooltip
|
||||||
|
longitude: number;
|
||||||
|
latitude: number;
|
||||||
|
colour: string;
|
||||||
|
centered?: boolean;
|
||||||
|
details?: string[];
|
||||||
|
markerIcon?: JSX.Element;
|
||||||
|
subtype?: number;
|
||||||
|
graphPercent?: number; //if defined show graph behind markericon
|
||||||
|
customImage?: JSX.Element; //if defined use image rather than basic marker with icon
|
||||||
|
customSize?: number; //if the size is different from the normal or mini markers
|
||||||
|
mini?: boolean; //whether the marker is the small scouting marker or the normal size object marker
|
||||||
|
visibleLevels?: { min?: number; max?: number }; //zoom levels that determine when the marker is visible
|
||||||
|
clickFunc?: (event: React.PointerEvent<HTMLElement>, index: number, isMobile: boolean) => void; //function the marker will run when clicked
|
||||||
|
updateFunc?: (location: pond.Location) => void; //function the marker will run when something changes
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
markerData: MarkerData[];
|
||||||
|
mapZoomLevel: number;
|
||||||
|
dragOn?: boolean;
|
||||||
|
groupSelect?: boolean;
|
||||||
|
showMarkerTitle?: boolean;
|
||||||
|
displayDetails?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) => ({
|
||||||
|
pin: {
|
||||||
|
borderRadius: "50rem",
|
||||||
|
display: "inline-block",
|
||||||
|
borderBottomRightRadius: "0",
|
||||||
|
transform: "rotate(45deg)",
|
||||||
|
cursor: "pointer",
|
||||||
|
boxShadow: "4px 4px 10px black"
|
||||||
|
},
|
||||||
|
details: {
|
||||||
|
transform: "rotate(45deg)",
|
||||||
|
background: theme.palette.background.default,
|
||||||
|
opacity: 0.75,
|
||||||
|
padding: 5,
|
||||||
|
borderRadius: 10,
|
||||||
|
paddingLeft: 40,
|
||||||
|
clipPath: "polygon(25% 0, 100% 0%, 100% 100%, 25% 100%, 0% 50%)"
|
||||||
|
},
|
||||||
|
titleDisplay: {
|
||||||
|
textAlign: "center",
|
||||||
|
width: 100,
|
||||||
|
marginLeft: -25,
|
||||||
|
marginTop: 10,
|
||||||
|
backgroundColor: theme.palette.background.default,
|
||||||
|
borderRadius: 30,
|
||||||
|
boxShadow: "4px 4px 10px black"
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function Markers(props: Props) {
|
||||||
|
const { markerData, dragOn, mapZoomLevel, displayDetails, showMarkerTitle, groupSelect } = props;
|
||||||
|
const classes = useStyles();
|
||||||
|
const isMobile = useMobile();
|
||||||
|
const [groupedMarkers, setGroupedMarkers] = useState<Map<number, MarkerData>>(new Map());
|
||||||
|
|
||||||
|
const moveGroup = (oldLong: number, oldLat: number, newLong: number, newLat: number) => {
|
||||||
|
//find the diff of the oldLong (the starting longitude of the marker that was actually dragged) and the newLong
|
||||||
|
const diffLong = newLong - oldLong;
|
||||||
|
//find the diff of the oldLat and new Lat
|
||||||
|
const diffLat = newLat - oldLat;
|
||||||
|
|
||||||
|
let gm = clone(groupedMarkers);
|
||||||
|
|
||||||
|
gm.forEach(marker => {
|
||||||
|
if (marker.updateFunc) {
|
||||||
|
marker.longitude = marker.longitude + diffLong;
|
||||||
|
marker.latitude = marker.latitude + diffLat;
|
||||||
|
marker.updateFunc(
|
||||||
|
pond.Location.create({ longitude: marker.longitude, latitude: marker.latitude })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setGroupedMarkers(gm);
|
||||||
|
};
|
||||||
|
|
||||||
|
//when group select is turned off, clear the grouped markers
|
||||||
|
useEffect(() => {
|
||||||
|
if (!groupSelect) {
|
||||||
|
setGroupedMarkers(new Map());
|
||||||
|
}
|
||||||
|
}, [groupSelect]);
|
||||||
|
|
||||||
|
const renderMarkers = useCallback(() => {
|
||||||
|
let markerList: JSX.Element[] = [];
|
||||||
|
markerData.forEach((marker, index) => {
|
||||||
|
let markerSize = marker.mini ? 20 : 50;
|
||||||
|
if (marker.customSize) {
|
||||||
|
markerSize = marker.customSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
let yOff = 0;
|
||||||
|
if (!marker.centered) {
|
||||||
|
yOff = -markerSize * 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
let vis = "block";
|
||||||
|
if (marker.visibleLevels) {
|
||||||
|
let max = marker.visibleLevels.max;
|
||||||
|
let min = marker.visibleLevels.min;
|
||||||
|
if ((max && mapZoomLevel > max) || (min && mapZoomLevel < min)) {
|
||||||
|
vis = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let gm = groupedMarkers.get(index);
|
||||||
|
if (gm) {
|
||||||
|
marker.longitude = gm.longitude;
|
||||||
|
marker.latitude = gm.latitude;
|
||||||
|
}
|
||||||
|
|
||||||
|
let border = marker.customImage ? "" : "1px solid white";
|
||||||
|
if (gm) {
|
||||||
|
border = "2px solid black";
|
||||||
|
}
|
||||||
|
|
||||||
|
let blah: JSX.Element = (
|
||||||
|
<Marker
|
||||||
|
//style={{border: gm ? "2px solid black" : ""}}
|
||||||
|
key={"marker-" + index}
|
||||||
|
longitude={marker.longitude}
|
||||||
|
latitude={marker.latitude}
|
||||||
|
//offsetTop={yOff}
|
||||||
|
offset={[0, yOff]}
|
||||||
|
draggable={dragOn}
|
||||||
|
onDragEnd={e => {
|
||||||
|
let newLong = e.lngLat.lng;
|
||||||
|
let newLat = e.lngLat.lat;
|
||||||
|
if (groupSelect && groupedMarkers.size > 0) {
|
||||||
|
moveGroup(marker.longitude, marker.latitude, newLong, newLat);
|
||||||
|
} else {
|
||||||
|
if (marker.updateFunc) {
|
||||||
|
marker.longitude = newLong;
|
||||||
|
marker.latitude = newLat;
|
||||||
|
marker.updateFunc(
|
||||||
|
pond.Location.create({ latitude: e.lngLat.lat, longitude: e.lngLat.lng })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<Box id={"marker-" + index} />
|
||||||
|
<Tooltip title={marker.title} placement={"right"} arrow>
|
||||||
|
<Box
|
||||||
|
className={!marker.customImage ? classes.pin : undefined}
|
||||||
|
style={{
|
||||||
|
display: vis,
|
||||||
|
border: border,
|
||||||
|
background: marker.customImage ? "none" : marker.colour,
|
||||||
|
width: markerSize,
|
||||||
|
height: markerSize
|
||||||
|
}}
|
||||||
|
onPointerUp={e => {
|
||||||
|
if (!dragOn && marker.clickFunc) {
|
||||||
|
marker.clickFunc(e, index, isMobile);
|
||||||
|
} else if (groupSelect) {
|
||||||
|
let group = clone(groupedMarkers);
|
||||||
|
group.set(index, marker);
|
||||||
|
setGroupedMarkers(group);
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
{marker.graphPercent !== undefined && (
|
||||||
|
<Box
|
||||||
|
width={"100%"}
|
||||||
|
height={"100%"}
|
||||||
|
style={{ pointerEvents: "none", position: "absolute" }}>
|
||||||
|
<MiniPie
|
||||||
|
max={100}
|
||||||
|
current={marker.graphPercent}
|
||||||
|
colour={marker.colour}
|
||||||
|
size={markerSize}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{marker.customImage ? (
|
||||||
|
marker.customImage
|
||||||
|
) : (
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
transform: "rotate(-45deg)",
|
||||||
|
width: markerSize * 0.6,
|
||||||
|
height: markerSize * 0.6,
|
||||||
|
paddingTop: marker.mini ? "10%" : "30%",
|
||||||
|
pointerEvents: "none"
|
||||||
|
}}>
|
||||||
|
{marker.markerIcon}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{marker.details && displayDetails && (
|
||||||
|
<Box
|
||||||
|
className={classes.details}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: markerSize / 2,
|
||||||
|
top: markerSize / 2,
|
||||||
|
width: 200,
|
||||||
|
marginTop: 80
|
||||||
|
}}>
|
||||||
|
<Grid container direction="column" wrap="nowrap">
|
||||||
|
{marker.details.map((detail, i) => (
|
||||||
|
<Grid key={i}>
|
||||||
|
<Typography noWrap style={{ fontSize: 10, fontWeight: 750, height: 15 }}>
|
||||||
|
{detail}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
{showMarkerTitle && <Box className={classes.titleDisplay}>{marker.title}</Box>}
|
||||||
|
</Marker>
|
||||||
|
);
|
||||||
|
markerList.push(<React.Fragment key={index}>{blah}</React.Fragment>);
|
||||||
|
});
|
||||||
|
return markerList;
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [
|
||||||
|
markerData,
|
||||||
|
classes,
|
||||||
|
displayDetails,
|
||||||
|
dragOn,
|
||||||
|
groupSelect,
|
||||||
|
groupedMarkers,
|
||||||
|
isMobile,
|
||||||
|
showMarkerTitle,
|
||||||
|
mapZoomLevel
|
||||||
|
]);
|
||||||
|
|
||||||
|
return <React.Fragment>{renderMarkers()}</React.Fragment>;
|
||||||
|
}
|
||||||
213
src/maps/mapMarkers/fieldMarkers/FieldMarkerSettings.tsx
Normal file
213
src/maps/mapMarkers/fieldMarkers/FieldMarkerSettings.tsx
Normal file
|
|
@ -0,0 +1,213 @@
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
Grid2 as Grid,
|
||||||
|
MenuItem,
|
||||||
|
Slider,
|
||||||
|
TextField
|
||||||
|
} from "@mui/material";
|
||||||
|
import ResponsiveDialog from "common/ResponsiveDialog";
|
||||||
|
import { FieldMarker } from "models";
|
||||||
|
import { pond } from "protobuf-ts/pond";
|
||||||
|
import { useFieldMarkerAPI, useSnackbar } from "providers";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open: boolean;
|
||||||
|
close: (refresh?: boolean) => void;
|
||||||
|
longitude?: number;
|
||||||
|
latitude?: number;
|
||||||
|
fieldMarker?: FieldMarker;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldMarkerTypes = [
|
||||||
|
{
|
||||||
|
value: pond.FieldMarkerType.FIELD_MARKER_TYPE_OTHER,
|
||||||
|
label: "Other"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: pond.FieldMarkerType.FIELD_MARKER_TYPE_WEEDS,
|
||||||
|
label: "Weeds"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: pond.FieldMarkerType.FIELD_MARKER_TYPE_DISEASE,
|
||||||
|
label: "Disease"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: pond.FieldMarkerType.FIELD_MARKER_TYPE_PESTS,
|
||||||
|
label: "Pests"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: pond.FieldMarkerType.FIELD_MARKER_TYPE_ROCKS,
|
||||||
|
label: "Rocks"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function FieldMarkerSettings(props: Props) {
|
||||||
|
const { open, close, fieldMarker, longitude, latitude } = props;
|
||||||
|
const fieldMarkerAPI = useFieldMarkerAPI();
|
||||||
|
const { openSnack } = useSnackbar();
|
||||||
|
const [title, setTitle] = useState("Field Marker");
|
||||||
|
const [type, setType] = useState<pond.FieldMarkerType>(
|
||||||
|
pond.FieldMarkerType.FIELD_MARKER_TYPE_OTHER
|
||||||
|
);
|
||||||
|
const [severity, setSeverity] = useState(0);
|
||||||
|
const [description, setDescription] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (fieldMarker) {
|
||||||
|
setTitle(fieldMarker.name());
|
||||||
|
setType(fieldMarker.type());
|
||||||
|
setSeverity(fieldMarker.severity());
|
||||||
|
setDescription(fieldMarker.description());
|
||||||
|
}
|
||||||
|
}, [fieldMarker]);
|
||||||
|
|
||||||
|
const saveNewFieldMarker = () => {
|
||||||
|
let newFM: FieldMarker = FieldMarker.create();
|
||||||
|
newFM.settings.name = title;
|
||||||
|
newFM.settings.type = type;
|
||||||
|
newFM.settings.description = description;
|
||||||
|
newFM.settings.latitude = latitude ?? 0;
|
||||||
|
newFM.settings.longitude = longitude ?? 0;
|
||||||
|
newFM.settings.severity = severity;
|
||||||
|
//newFM.settings.affectedArea = fmAffectedArea;
|
||||||
|
fieldMarkerAPI
|
||||||
|
.addFieldMarker(newFM.settings)
|
||||||
|
.then(resp => {
|
||||||
|
openSnack("New marker has been saved");
|
||||||
|
close(true);
|
||||||
|
})
|
||||||
|
.catch(err => openSnack("There was a problem saving your marker"));
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateFieldMarker = (fieldMarker: FieldMarker) => {
|
||||||
|
let fm = fieldMarker;
|
||||||
|
fm.settings.description = description;
|
||||||
|
fm.settings.severity = severity;
|
||||||
|
//fm.settings.affectedArea = fmAffectedArea;
|
||||||
|
fieldMarkerAPI
|
||||||
|
.updateFieldMarker(fm.key(), fm.settings)
|
||||||
|
.then(resp => {
|
||||||
|
openSnack("Updated Field Marker");
|
||||||
|
close(true);
|
||||||
|
})
|
||||||
|
.catch(resp => {
|
||||||
|
openSnack("Failed to update Field Marker");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeFieldMarker = (fieldMarker: FieldMarker) => {
|
||||||
|
fieldMarkerAPI
|
||||||
|
.removeFieldMarker(fieldMarker.key())
|
||||||
|
.then(resp => {
|
||||||
|
openSnack("Field Marker Removed");
|
||||||
|
close(true);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
openSnack("Failed to remove Field Marker");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ResponsiveDialog open={open} onClose={() => close()} fullScreen={false}>
|
||||||
|
<DialogTitle>{fieldMarker ? "Update Marker" : "Place Marker"}</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Box width={300}>
|
||||||
|
<TextField
|
||||||
|
id={"fieldMarkerName"}
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
label="Name"
|
||||||
|
value={title}
|
||||||
|
disabled={fieldMarker !== undefined}
|
||||||
|
onChange={e => setTitle(e.target.value)}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
id={"FieldMarkerType"}
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
label="type"
|
||||||
|
value={type}
|
||||||
|
onChange={e => setType(+e.target.value)}
|
||||||
|
select
|
||||||
|
disabled={fieldMarker !== undefined}>
|
||||||
|
{fieldMarkerTypes.map(option => (
|
||||||
|
<MenuItem key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
Severity
|
||||||
|
<Slider
|
||||||
|
value={severity}
|
||||||
|
min={0}
|
||||||
|
max={4}
|
||||||
|
step={1}
|
||||||
|
onChange={(e, val) => setSeverity(val as number)}
|
||||||
|
/>
|
||||||
|
{/* Not doing affected area until i figure out how to resize with zoom */}
|
||||||
|
{/* Affected Area
|
||||||
|
<Slider
|
||||||
|
value={fmAffectedArea}
|
||||||
|
min={0}
|
||||||
|
max={1000}
|
||||||
|
onChange={(e, val) => setFMAffectedArea(val as number)}
|
||||||
|
/> */}
|
||||||
|
<TextField
|
||||||
|
id={"fieldMarkerDescription"}
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
label="Description"
|
||||||
|
value={description}
|
||||||
|
onChange={e => setDescription(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Grid container direction="row">
|
||||||
|
<Grid>
|
||||||
|
{fieldMarker !== undefined && (
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
style={{ color: "black", backgroundColor: "red", margin: 5 }}
|
||||||
|
onClick={() => removeFieldMarker(fieldMarker)}>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<Button
|
||||||
|
style={{ margin: 5 }}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => close()}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
{fieldMarker !== undefined ? (
|
||||||
|
<Button
|
||||||
|
style={{ margin: 5 }}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => updateFieldMarker(fieldMarker)}>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
style={{ margin: 5 }}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={saveNewFieldMarker}>
|
||||||
|
Plot Field Marker
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</DialogActions>
|
||||||
|
</ResponsiveDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
1904
src/maps/mapObjectControllers/AgMapController.tsx
Normal file
1904
src/maps/mapObjectControllers/AgMapController.tsx
Normal file
File diff suppressed because it is too large
Load diff
419
src/maps/mapObjectTools/agMapTools.tsx
Normal file
419
src/maps/mapObjectTools/agMapTools.tsx
Normal file
|
|
@ -0,0 +1,419 @@
|
||||||
|
import {
|
||||||
|
Grid2 as Grid,
|
||||||
|
IconButton,
|
||||||
|
Theme,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemIcon,
|
||||||
|
Typography,
|
||||||
|
Box,
|
||||||
|
FormControlLabel,
|
||||||
|
Checkbox
|
||||||
|
} from "@mui/material";
|
||||||
|
import AddMarkerIcon from "products/AgIcons/AddMarker";
|
||||||
|
import FieldMapIcon from "products/AgIcons/FieldMap";
|
||||||
|
import FieldNamesIcon from "products/AgIcons/FieldNames";
|
||||||
|
import ScoutIcon from "products/AgIcons/ScoutIcon";
|
||||||
|
import AddBinIcon from "products/Bindapt/AddBinIcon";
|
||||||
|
import BinsIcon from "products/Bindapt/BinsIcon";
|
||||||
|
import { VisibilityOff } from "@mui/icons-material";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import AddFieldIcon from "products/AgIcons/AddField";
|
||||||
|
import EditIcon from "products/AgIcons/Edit";
|
||||||
|
import DeleteIcon from "products/AgIcons/Delete";
|
||||||
|
import FieldListIcon from "products/AgIcons/FieldList";
|
||||||
|
import DeviceIcon from "products/Bindapt/BindaptIcon";
|
||||||
|
import { pond } from "protobuf-ts/pond";
|
||||||
|
//import { useHistory } from "react-router";
|
||||||
|
import { makeStyles } from "@mui/styles";
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) => ({
|
||||||
|
iconButtons: {
|
||||||
|
background: theme.palette.background.default,
|
||||||
|
margin: 5,
|
||||||
|
boxShadow: "4px 4px 10px black"
|
||||||
|
},
|
||||||
|
list: {
|
||||||
|
background: theme.palette.background.default,
|
||||||
|
opacity: 0.75,
|
||||||
|
borderRadius: 15,
|
||||||
|
marginTop: 10,
|
||||||
|
boxShadow: "4px 4px 10px black"
|
||||||
|
},
|
||||||
|
liFont: {
|
||||||
|
fontWeight: 700,
|
||||||
|
marginLeft: -15
|
||||||
|
},
|
||||||
|
listItems: {
|
||||||
|
borderRadius: "3rem"
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
toggleEditorMode: (
|
||||||
|
modeName: "edit" | "drawPolygon" | "drawLine" | "drawPoint" | "none" | "delete" | undefined
|
||||||
|
) => void;
|
||||||
|
//these modes are for the imported editor, could possibly be removed when new editor is made
|
||||||
|
binMarkerDetails: boolean;
|
||||||
|
toggleBinMarkerDetails: (detailsVisible: boolean) => void; //controls whether the details are visible on the
|
||||||
|
toggleIsNewObject: (newObject: boolean) => void; //controls whether creating a new object, in this case bin, when clicking on the map to place a marker
|
||||||
|
toggleMarkerType: (type: pond.ObjectType) => void;
|
||||||
|
toggleFieldTitles: (show: boolean) => void;
|
||||||
|
toggleObjectEditType: (type: pond.ObjectType) => void;
|
||||||
|
toggleMeasurement: (measurement: boolean) => void;
|
||||||
|
//because the draw control for the map actually changes the modes itself internally will need to have the object controller on the outside change its mode as well
|
||||||
|
editorMode: "edit" | "drawPolygon" | "drawLine" | "drawPoint" | "none" | "delete" | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AgMapTools(props: Props) {
|
||||||
|
const classes = useStyles();
|
||||||
|
const {
|
||||||
|
toggleEditorMode,
|
||||||
|
toggleBinMarkerDetails,
|
||||||
|
binMarkerDetails,
|
||||||
|
toggleIsNewObject,
|
||||||
|
toggleMarkerType,
|
||||||
|
toggleFieldTitles,
|
||||||
|
toggleObjectEditType,
|
||||||
|
toggleMeasurement
|
||||||
|
} = props;
|
||||||
|
const [markerType, setMarkerType] = useState<pond.ObjectType | undefined>();
|
||||||
|
const [isNew, setIsNew] = useState(false);
|
||||||
|
const [fieldControlDisplay, setFieldControlDisplay] = useState<"none" | "block">("none");
|
||||||
|
const [binControlDisplay, setBinControlDisplay] = useState<"none" | "block">("none");
|
||||||
|
const [scoutingControlDisplay, setScoutingControlDisplay] = useState<"none" | "block">("none");
|
||||||
|
const [fieldControlHovered, setFieldControlHovered] = useState(false);
|
||||||
|
const [binControlHovered, setBinControlHovered] = useState(false);
|
||||||
|
const [scoutingControlHovered, setScoutingControlHovered] = useState(false);
|
||||||
|
const [showFieldTitle, setShowFieldTitles] = useState(false);
|
||||||
|
//const history = useHistory();
|
||||||
|
|
||||||
|
const goToMyFields = () => {
|
||||||
|
console.log("navigate to my fields page")
|
||||||
|
//history.push("/myFields");
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleControls = (control: "field" | "bin" | "scout") => {
|
||||||
|
if (control === "field") {
|
||||||
|
if (fieldControlDisplay === "none") {
|
||||||
|
setFieldControlDisplay("block");
|
||||||
|
setBinControlDisplay("none");
|
||||||
|
setScoutingControlDisplay("none");
|
||||||
|
} else {
|
||||||
|
setFieldControlDisplay("none");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (control === "bin") {
|
||||||
|
if (binControlDisplay === "none") {
|
||||||
|
setBinControlDisplay("block");
|
||||||
|
setFieldControlDisplay("none");
|
||||||
|
setScoutingControlDisplay("none");
|
||||||
|
} else {
|
||||||
|
setBinControlDisplay("none");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (control === "scout") {
|
||||||
|
if (scoutingControlDisplay === "none") {
|
||||||
|
setScoutingControlDisplay("block");
|
||||||
|
setFieldControlDisplay("none");
|
||||||
|
setBinControlDisplay("none");
|
||||||
|
} else {
|
||||||
|
setScoutingControlDisplay("none");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
toggleEditorMode("none");
|
||||||
|
toggleIsNewObject(false);
|
||||||
|
changeMarkerType(pond.ObjectType.OBJECT_TYPE_UNKNOWN);
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeMarkerType = (type: pond.ObjectType) => {
|
||||||
|
setMarkerType(type);
|
||||||
|
toggleMarkerType(type);
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeNewStatus = (isNew: boolean) => {
|
||||||
|
setIsNew(isNew);
|
||||||
|
toggleIsNewObject(isNew);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Grid container direction="column">
|
||||||
|
<Grid>
|
||||||
|
<Grid container direction="row">
|
||||||
|
<Grid>
|
||||||
|
<IconButton
|
||||||
|
className={classes.iconButtons}
|
||||||
|
style={
|
||||||
|
binControlDisplay === "block"
|
||||||
|
? { background: "yellow" }
|
||||||
|
: binControlHovered
|
||||||
|
? { background: "grey" }
|
||||||
|
: {}
|
||||||
|
}
|
||||||
|
title="Bin Controls"
|
||||||
|
onClick={e => {
|
||||||
|
toggleControls("bin");
|
||||||
|
}}
|
||||||
|
onMouseOver={() => setBinControlHovered(true)}
|
||||||
|
onMouseOut={() => setBinControlHovered(false)}>
|
||||||
|
{binControlDisplay === "block" ? <BinsIcon type="dark" /> : <BinsIcon />}
|
||||||
|
</IconButton>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<IconButton
|
||||||
|
className={classes.iconButtons}
|
||||||
|
style={
|
||||||
|
fieldControlDisplay === "block"
|
||||||
|
? { background: "yellow" }
|
||||||
|
: fieldControlHovered
|
||||||
|
? { background: "grey" }
|
||||||
|
: {}
|
||||||
|
}
|
||||||
|
title="Field Controls"
|
||||||
|
onClick={e => {
|
||||||
|
toggleControls("field");
|
||||||
|
}}
|
||||||
|
onMouseOver={() => setFieldControlHovered(true)}
|
||||||
|
onMouseOut={() => setFieldControlHovered(false)}>
|
||||||
|
{fieldControlDisplay === "block" ? <FieldMapIcon type="dark" /> : <FieldMapIcon />}
|
||||||
|
</IconButton>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<IconButton
|
||||||
|
className={classes.iconButtons}
|
||||||
|
style={
|
||||||
|
scoutingControlDisplay === "block"
|
||||||
|
? { background: "yellow" }
|
||||||
|
: scoutingControlHovered
|
||||||
|
? { background: "grey" }
|
||||||
|
: {}
|
||||||
|
}
|
||||||
|
title="Scouting Controls"
|
||||||
|
onClick={e => {
|
||||||
|
toggleControls("scout");
|
||||||
|
}}
|
||||||
|
onMouseOver={() => setScoutingControlHovered(true)}
|
||||||
|
onMouseOut={() => setScoutingControlHovered(false)}>
|
||||||
|
{scoutingControlDisplay === "block" ? <ScoutIcon type="dark" /> : <ScoutIcon />}
|
||||||
|
</IconButton>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<Grid container direction="row">
|
||||||
|
<Grid style={{ display: binControlDisplay }}>
|
||||||
|
<List className={classes.list}>
|
||||||
|
<ListItem
|
||||||
|
id="createBin"
|
||||||
|
//button
|
||||||
|
onClick={e => {
|
||||||
|
changeNewStatus(true);
|
||||||
|
//setPlaceLine(false);
|
||||||
|
changeMarkerType(pond.ObjectType.OBJECT_TYPE_BIN);
|
||||||
|
}}
|
||||||
|
className={classes.listItems}
|
||||||
|
style={
|
||||||
|
isNew && markerType === pond.ObjectType.OBJECT_TYPE_BIN
|
||||||
|
? { background: "green" }
|
||||||
|
: {}
|
||||||
|
}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<AddBinIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<Typography className={classes.liFont}>Create New Bin</Typography>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
id="addBin"
|
||||||
|
//button
|
||||||
|
onClick={e => {
|
||||||
|
changeNewStatus(false);
|
||||||
|
//setPlaceLine(false);
|
||||||
|
changeMarkerType(pond.ObjectType.OBJECT_TYPE_BIN);
|
||||||
|
}}
|
||||||
|
className={classes.listItems}
|
||||||
|
style={
|
||||||
|
!isNew && markerType === pond.ObjectType.OBJECT_TYPE_BIN
|
||||||
|
? { background: "green" }
|
||||||
|
: {}
|
||||||
|
}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<BinsIcon singleBin />
|
||||||
|
</ListItemIcon>
|
||||||
|
<Typography className={classes.liFont}>Set Bin Location</Typography>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
id="addBinYard"
|
||||||
|
//button
|
||||||
|
onClick={e => {
|
||||||
|
changeNewStatus(false);
|
||||||
|
//setPlaceLine(false);
|
||||||
|
changeMarkerType(pond.ObjectType.OBJECT_TYPE_BINYARD);
|
||||||
|
}}
|
||||||
|
className={classes.listItems}
|
||||||
|
style={
|
||||||
|
markerType === pond.ObjectType.OBJECT_TYPE_BINYARD
|
||||||
|
? { background: "green" }
|
||||||
|
: {}
|
||||||
|
}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<BinsIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<Typography className={classes.liFont}>Set Bin Yard Location</Typography>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem id="binName" className={classes.listItems}>
|
||||||
|
<FormControlLabel
|
||||||
|
label="Bin Details"
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
value={binMarkerDetails}
|
||||||
|
onChange={e => toggleBinMarkerDetails(e.target.checked)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
</Grid>
|
||||||
|
<Grid style={{ display: fieldControlDisplay }}>
|
||||||
|
<Box>
|
||||||
|
<Box className={classes.list}></Box>
|
||||||
|
<List className={classes.list} style={{ marginLeft: 60 }}>
|
||||||
|
<ListItem
|
||||||
|
id="addField"
|
||||||
|
//button
|
||||||
|
onClick={() => {
|
||||||
|
toggleEditorMode("drawPolygon");
|
||||||
|
toggleMeasurement(false);
|
||||||
|
}}
|
||||||
|
className={classes.listItems}
|
||||||
|
style={props.editorMode === "drawPolygon" ? { background: "green" } : {}}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<AddFieldIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<Typography className={classes.liFont}>Add Field/Hole</Typography>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
id="plotGrainBag"
|
||||||
|
//button
|
||||||
|
onClick={() => {
|
||||||
|
toggleEditorMode("drawLine");
|
||||||
|
toggleMeasurement(false);
|
||||||
|
toggleObjectEditType(pond.ObjectType.OBJECT_TYPE_GRAIN_BAG);
|
||||||
|
}}
|
||||||
|
className={classes.listItems}
|
||||||
|
style={props.editorMode === "drawLine" ? { background: "green" } : {}}>
|
||||||
|
<ListItemIcon></ListItemIcon>
|
||||||
|
<Typography className={classes.liFont}>Add Grain Bag</Typography>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
id="editField"
|
||||||
|
//button
|
||||||
|
onClick={() => {
|
||||||
|
toggleEditorMode("edit");
|
||||||
|
toggleMeasurement(false);
|
||||||
|
}}
|
||||||
|
className={classes.listItems}
|
||||||
|
style={props.editorMode === "edit" ? { background: "green" } : {}}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<EditIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<Typography className={classes.liFont}>Edit Field Structures</Typography>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
id="deleteField"
|
||||||
|
//button
|
||||||
|
onClick={() => {
|
||||||
|
toggleEditorMode("delete");
|
||||||
|
toggleMeasurement(false);
|
||||||
|
}}
|
||||||
|
className={classes.listItems}
|
||||||
|
style={props.editorMode === "delete" ? { background: "green" } : {}}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<DeleteIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<Typography className={classes.liFont}>Delete Field</Typography>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
id="fieldList"
|
||||||
|
//button
|
||||||
|
onClick={goToMyFields}
|
||||||
|
className={classes.listItems}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<FieldListIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<Typography className={classes.liFont}>Go to My Fields</Typography>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
id="fieldNames"
|
||||||
|
//button
|
||||||
|
onClick={() => {
|
||||||
|
toggleFieldTitles(!showFieldTitle);
|
||||||
|
setShowFieldTitles(!showFieldTitle);
|
||||||
|
}}
|
||||||
|
className={classes.listItems}>
|
||||||
|
<ListItemIcon>
|
||||||
|
{showFieldTitle ? <VisibilityOff /> : <FieldNamesIcon />}
|
||||||
|
</ListItemIcon>
|
||||||
|
<Typography className={classes.liFont}>View Field Names</Typography>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
id="addDevice"
|
||||||
|
//button
|
||||||
|
onClick={e => {
|
||||||
|
changeMarkerType(pond.ObjectType.OBJECT_TYPE_DEVICE);
|
||||||
|
}}
|
||||||
|
className={classes.listItems}
|
||||||
|
style={
|
||||||
|
markerType === pond.ObjectType.OBJECT_TYPE_DEVICE
|
||||||
|
? { background: "green" }
|
||||||
|
: {}
|
||||||
|
}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<DeviceIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<Typography className={classes.liFont}>Set Device Location</Typography>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
<Grid style={{ display: scoutingControlDisplay }}>
|
||||||
|
<List className={classes.list} style={{ marginLeft: 120 }}>
|
||||||
|
<ListItem
|
||||||
|
id="addCustomMarker"
|
||||||
|
//button
|
||||||
|
onClick={e => {
|
||||||
|
changeMarkerType(pond.ObjectType.OBJECT_TYPE_FIELDMARKER);
|
||||||
|
// if (watching) {
|
||||||
|
// props.newMarker(geoPosition.longitude, geoPosition.latitude, "custom");
|
||||||
|
// }
|
||||||
|
}}
|
||||||
|
className={classes.listItems}
|
||||||
|
style={
|
||||||
|
markerType === pond.ObjectType.OBJECT_TYPE_FIELDMARKER
|
||||||
|
? { background: "green" }
|
||||||
|
: {}
|
||||||
|
}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<AddMarkerIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<Typography className={classes.liFont}>Set Custom Marker</Typography>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
id="addMeasurementLine"
|
||||||
|
//button
|
||||||
|
onClick={e => {
|
||||||
|
toggleEditorMode("drawLine");
|
||||||
|
toggleObjectEditType(pond.ObjectType.OBJECT_TYPE_UNKNOWN); //TODO: make this a measurement object type in the future
|
||||||
|
toggleMeasurement(true);
|
||||||
|
}}
|
||||||
|
className={classes.listItems}
|
||||||
|
style={props.editorMode === "drawLine" ? { background: "green" } : {}}>
|
||||||
|
<ListItemIcon></ListItemIcon>
|
||||||
|
<Typography className={classes.liFont}>Measurement Line</Typography>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
284
src/maps/mapboxStyleOverrides.css
Normal file
284
src/maps/mapboxStyleOverrides.css
Normal file
|
|
@ -0,0 +1,284 @@
|
||||||
|
/* Basics */
|
||||||
|
.mapboxgl-ctrl-geocoder,
|
||||||
|
.mapboxgl-ctrl-geocoder *,
|
||||||
|
.mapboxgl-ctrl-geocoder *:after,
|
||||||
|
.mapboxgl-ctrl-geocoder *:before {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapboxgl-ctrl-top-right {
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
padding-left: 20px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .mapboxgl-ctrl-top-left {
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.mapboxgl-ctrl-top-right {
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.mapboxgl-ctrl-bottom-left {
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.mapboxgl-ctrl-bottom-right {
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
} */
|
||||||
|
|
||||||
|
.mapboxgl-ctrl-geocoder {
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 24px;
|
||||||
|
font-family: "Open Sans", "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||||
|
position: relative;
|
||||||
|
background-color: #424242;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 240px;
|
||||||
|
z-index: 1;
|
||||||
|
border-radius: 20px;
|
||||||
|
transition: width 0.25s, min-width 0.25s;
|
||||||
|
opacity: 0.95;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapboxgl-ctrl-geocoder--input {
|
||||||
|
font: inherit;
|
||||||
|
width: 100%;
|
||||||
|
border: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
margin: 0;
|
||||||
|
height: 50px;
|
||||||
|
color: #fff;
|
||||||
|
padding: 6px 45px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapboxgl-ctrl-geocoder--input::-ms-clear {
|
||||||
|
display: none; /* hide input clear button in IE */
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapboxgl-ctrl-geocoder--input:focus {
|
||||||
|
color: #fff;
|
||||||
|
outline: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
outline: thin dotted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapboxgl-ctrl-geocoder .mapboxgl-ctrl-geocoder--pin-right > * {
|
||||||
|
z-index: 2;
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
top: 7px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapboxgl-ctrl-geocoder,
|
||||||
|
.mapboxgl-ctrl-geocoder .suggestions {
|
||||||
|
box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Collapsed */
|
||||||
|
.mapboxgl-ctrl-geocoder.mapboxgl-ctrl-geocoder--collapsed {
|
||||||
|
width: 50px;
|
||||||
|
min-width: 50px;
|
||||||
|
transition: width 0.25s, min-width 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Suggestions */
|
||||||
|
.mapboxgl-ctrl-geocoder .suggestions {
|
||||||
|
background-color: #424242;
|
||||||
|
opacity: 0.95;
|
||||||
|
border-radius: 4px;
|
||||||
|
left: 0;
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
top: 110%; /* fallback */
|
||||||
|
top: calc(100% + 6px);
|
||||||
|
z-index: 1000;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapboxgl-ctrl-bottom-left .suggestions,
|
||||||
|
.mapboxgl-ctrl-bottom-right .suggestions {
|
||||||
|
top: auto;
|
||||||
|
bottom: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapboxgl-ctrl-geocoder .suggestions > li > a {
|
||||||
|
cursor: default;
|
||||||
|
display: block;
|
||||||
|
padding: 6px 12px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapboxgl-ctrl-geocoder .suggestions > .active > a,
|
||||||
|
.mapboxgl-ctrl-geocoder .suggestions > li > a:hover {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #303030;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapboxgl-ctrl-geocoder--suggestion-title {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapboxgl-ctrl-geocoder--suggestion-title,
|
||||||
|
.mapboxgl-ctrl-geocoder--suggestion-address {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Icons */
|
||||||
|
.mapboxgl-ctrl-geocoder--icon {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
fill: #757575;
|
||||||
|
top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapboxgl-ctrl-geocoder--icon-search {
|
||||||
|
position: absolute;
|
||||||
|
top: 13px;
|
||||||
|
left: 12px;
|
||||||
|
width: 23px;
|
||||||
|
height: 23px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapboxgl-ctrl-geocoder--button {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #424242;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapboxgl-ctrl-geocoder--icon-close {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapboxgl-ctrl-geocoder--button:hover .mapboxgl-ctrl-geocoder--icon-close {
|
||||||
|
fill: #909090;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapboxgl-ctrl-geocoder--icon-loading {
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-right: 0px;
|
||||||
|
-moz-animation: rotate 0.8s infinite cubic-bezier(0.45, 0.05, 0.55, 0.95);
|
||||||
|
-webkit-animation: rotate 0.8s infinite cubic-bezier(0.45, 0.05, 0.55, 0.95);
|
||||||
|
animation: rotate 0.8s infinite cubic-bezier(0.45, 0.05, 0.55, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation */
|
||||||
|
@-webkit-keyframes rotate {
|
||||||
|
from {
|
||||||
|
-webkit-transform: rotate(0);
|
||||||
|
transform: rotate(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
-webkit-transform: rotate(360deg);
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotate {
|
||||||
|
from {
|
||||||
|
-webkit-transform: rotate(0);
|
||||||
|
transform: rotate(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
-webkit-transform: rotate(360deg);
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Media queries*/
|
||||||
|
@media screen and (min-width: 640px) {
|
||||||
|
.mapboxgl-ctrl-geocoder.mapboxgl-ctrl-geocoder--collapsed {
|
||||||
|
width: 36px;
|
||||||
|
min-width: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapboxgl-ctrl-geocoder {
|
||||||
|
width: 33.3333%;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 20px;
|
||||||
|
max-width: 360px;
|
||||||
|
}
|
||||||
|
.mapboxgl-ctrl-geocoder .suggestions {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapboxgl-ctrl-geocoder--icon {
|
||||||
|
top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapboxgl-ctrl-geocoder--icon-close {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin-top: 3px;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapboxgl-ctrl-geocoder--icon-search {
|
||||||
|
left: 7px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapboxgl-ctrl-geocoder--input {
|
||||||
|
height: 36px;
|
||||||
|
padding: 6px 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapboxgl-ctrl-geocoder--icon-loading {
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
margin-top: -2px;
|
||||||
|
margin-right: -5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapbox-gl-geocoder--error {
|
||||||
|
color: #909090;
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* popup styles */
|
||||||
|
.mapboxgl-popup-content {
|
||||||
|
background: #1f1e1d !important;
|
||||||
|
box-shadow: 5px 5px 20px 4px rgb(0, 0, 0) !important;
|
||||||
|
border-radius: 3 3 10px 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapboxgl-popup-tip {
|
||||||
|
border-bottom-color: #1f1e1d !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapboxgl-popup-close-button {
|
||||||
|
background-color: #e8dd40 !important;
|
||||||
|
border-radius: 3px !important;
|
||||||
|
position: absolute !important;
|
||||||
|
top: -5px !important;
|
||||||
|
right: -5px !important;
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import GrainDescriber from "grain/GrainDescriber";
|
import GrainDescriber from "grain/GrainDescriber";
|
||||||
import { cloneDeep } from "lodash";
|
import { cloneDeep } from "lodash";
|
||||||
// import { MarkerData } from "Maps/mapMarkers/Markers";
|
import { MarkerData } from "maps/mapMarkers/Markers";
|
||||||
import { pond } from "protobuf-ts/pond";
|
import { pond } from "protobuf-ts/pond";
|
||||||
import { stringToMaterialColour } from "utils/strings";
|
import { stringToMaterialColour } from "utils/strings";
|
||||||
import { or } from "utils/types";
|
import { or } from "utils/types";
|
||||||
|
|
@ -176,25 +176,25 @@ export class Bin {
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
// public getMarkerData(
|
public getMarkerData(
|
||||||
// clickFunc?: (event: React.PointerEvent<HTMLElement>, index: number, isMobile: boolean) => void,
|
clickFunc?: (event: React.PointerEvent<HTMLElement>, index: number, isMobile: boolean) => void,
|
||||||
// updateFunc?: (location: pond.Location) => void
|
updateFunc?: (location: pond.Location) => void
|
||||||
// ): MarkerData {
|
): MarkerData {
|
||||||
// let m: MarkerData = {
|
let m: MarkerData = {
|
||||||
// centered: true,
|
centered: true,
|
||||||
// longitude: this.location()?.longitude ?? 0,
|
longitude: this.location()?.longitude ?? 0,
|
||||||
// latitude: this.location()?.latitude ?? 0,
|
latitude: this.location()?.latitude ?? 0,
|
||||||
// title: this.name(),
|
title: this.name(),
|
||||||
// colour: this.grainColour(),
|
colour: this.grainColour(),
|
||||||
// visibleLevels: { min: 17 },
|
visibleLevels: { min: 17 },
|
||||||
// graphPercent: this.fillPercent(),
|
graphPercent: this.fillPercent(),
|
||||||
// customSize: this.settings.theme?.height,
|
customSize: this.settings.theme?.height,
|
||||||
// details: [this.name() + ", " + this.binFillCap()],
|
details: [this.name() + ", " + this.binFillCap()],
|
||||||
// clickFunc: clickFunc,
|
clickFunc: clickFunc,
|
||||||
// updateFunc: updateFunc
|
updateFunc: updateFunc
|
||||||
// };
|
};
|
||||||
// return m;
|
return m;
|
||||||
// }
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* returns the bushels per tonne set in the bins settings, if not set will return 1
|
* returns the bushels per tonne set in the bins settings, if not set will return 1
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { pond } from "protobuf-ts/pond";
|
import { pond } from "protobuf-ts/pond";
|
||||||
import { or } from "utils/types";
|
import { or } from "utils/types";
|
||||||
import { cloneDeep } from "lodash";
|
import { cloneDeep } from "lodash";
|
||||||
// import { MarkerData } from "Maps/mapMarkers/Markers";
|
import { MarkerData } from "maps/mapMarkers/Markers";
|
||||||
|
|
||||||
interface FeatureVersionByPlatform {
|
interface FeatureVersionByPlatform {
|
||||||
photon: string;
|
photon: string;
|
||||||
|
|
@ -65,20 +65,20 @@ export class Device {
|
||||||
return loc;
|
return loc;
|
||||||
}
|
}
|
||||||
|
|
||||||
// public getMarkerData(
|
public getMarkerData(
|
||||||
// clickFunc?: (event: React.PointerEvent<HTMLElement>, index: number, isMobile: boolean) => void,
|
clickFunc?: (event: React.PointerEvent<HTMLElement>, index: number, isMobile: boolean) => void,
|
||||||
// updateFunc?: (location: pond.Location) => void
|
updateFunc?: (location: pond.Location) => void
|
||||||
// ): MarkerData {
|
): MarkerData {
|
||||||
// let m: MarkerData = {
|
let m: MarkerData = {
|
||||||
// longitude: this.location()?.longitude ?? 0,
|
longitude: this.location()?.longitude ?? 0,
|
||||||
// latitude: this.location()?.latitude ?? 0,
|
latitude: this.location()?.latitude ?? 0,
|
||||||
// title: this.name(),
|
title: this.name(),
|
||||||
// colour: this.settings.theme?.color ?? "blue",
|
colour: this.settings.theme?.color ?? "blue",
|
||||||
// clickFunc: clickFunc,
|
clickFunc: clickFunc,
|
||||||
// updateFunc: updateFunc
|
updateFunc: updateFunc
|
||||||
// };
|
};
|
||||||
// return m;
|
return m;
|
||||||
// }
|
}
|
||||||
|
|
||||||
public featureSupported(feature: string): boolean {
|
public featureSupported(feature: string): boolean {
|
||||||
let versions = featureVersions.get(feature);
|
let versions = featureVersions.get(feature);
|
||||||
|
|
|
||||||
127
src/models/FieldMarker.ts
Normal file
127
src/models/FieldMarker.ts
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
import { cloneDeep } from "lodash";
|
||||||
|
import { pond } from "protobuf-ts/pond";
|
||||||
|
import { or } from "utils/types";
|
||||||
|
|
||||||
|
export class FieldMarker {
|
||||||
|
public settings: pond.FieldMarkerSettings = pond.FieldMarkerSettings.create();
|
||||||
|
public status: pond.FieldMarkerStatus = pond.FieldMarkerStatus.create();
|
||||||
|
|
||||||
|
public static create(hm?: pond.FieldMarker): FieldMarker {
|
||||||
|
let my = new FieldMarker();
|
||||||
|
if (hm) {
|
||||||
|
my.settings = pond.FieldMarkerSettings.fromObject(cloneDeep(or(hm.settings, {})));
|
||||||
|
my.status = pond.FieldMarkerStatus.fromObject(cloneDeep(or(hm.status, {})));
|
||||||
|
}
|
||||||
|
return my;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static clone(other?: FieldMarker): FieldMarker {
|
||||||
|
if (other) {
|
||||||
|
return FieldMarker.create(
|
||||||
|
pond.FieldMarker.fromObject({
|
||||||
|
settings: cloneDeep(other.settings),
|
||||||
|
status: cloneDeep(other.status)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return FieldMarker.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static any(data: any): FieldMarker {
|
||||||
|
return FieldMarker.create(pond.FieldMarker.fromObject(cloneDeep(data)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public key(): string {
|
||||||
|
return this.settings.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long(): number {
|
||||||
|
return this.settings.longitude;
|
||||||
|
}
|
||||||
|
|
||||||
|
public lat(): number {
|
||||||
|
return this.settings.latitude;
|
||||||
|
}
|
||||||
|
|
||||||
|
public description(): string {
|
||||||
|
return this.settings.description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public name(): string {
|
||||||
|
return this.settings.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public severity(): number {
|
||||||
|
return this.settings.severity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public severityColour(): string {
|
||||||
|
switch (this.settings.severity) {
|
||||||
|
case 1:
|
||||||
|
return "#58d68d";
|
||||||
|
case 2:
|
||||||
|
return "#FFC300";
|
||||||
|
case 3:
|
||||||
|
return "#FF5733";
|
||||||
|
case 4:
|
||||||
|
return "#FF0000";
|
||||||
|
default:
|
||||||
|
return "#229954";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public severityString(): string {
|
||||||
|
switch (this.settings.severity) {
|
||||||
|
case 1:
|
||||||
|
return "Mild";
|
||||||
|
case 2:
|
||||||
|
return "Moderate";
|
||||||
|
case 3:
|
||||||
|
return "High";
|
||||||
|
case 4:
|
||||||
|
return "Severe";
|
||||||
|
default:
|
||||||
|
return "Low";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public area(): number {
|
||||||
|
return this.settings.affectedArea;
|
||||||
|
}
|
||||||
|
|
||||||
|
public type(): pond.FieldMarkerType {
|
||||||
|
return this.settings.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public typeString(): string {
|
||||||
|
switch (this.settings.type) {
|
||||||
|
case pond.FieldMarkerType.FIELD_MARKER_TYPE_WEEDS:
|
||||||
|
return "Weeds";
|
||||||
|
case pond.FieldMarkerType.FIELD_MARKER_TYPE_PESTS:
|
||||||
|
return "Pests";
|
||||||
|
case pond.FieldMarkerType.FIELD_MARKER_TYPE_DISEASE:
|
||||||
|
return "Disease";
|
||||||
|
case pond.FieldMarkerType.FIELD_MARKER_TYPE_OTHER:
|
||||||
|
return "Other";
|
||||||
|
case pond.FieldMarkerType.FIELD_MARKER_TYPE_ROCKS:
|
||||||
|
return "Rocks";
|
||||||
|
default:
|
||||||
|
return "Invalid Type";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public defaultColor(): string {
|
||||||
|
switch (this.settings.type) {
|
||||||
|
case pond.FieldMarkerType.FIELD_MARKER_TYPE_WEEDS:
|
||||||
|
return "green";
|
||||||
|
case pond.FieldMarkerType.FIELD_MARKER_TYPE_PESTS:
|
||||||
|
return "blue";
|
||||||
|
case pond.FieldMarkerType.FIELD_MARKER_TYPE_DISEASE:
|
||||||
|
return "yellow";
|
||||||
|
case pond.FieldMarkerType.FIELD_MARKER_TYPE_ROCKS:
|
||||||
|
return "grey";
|
||||||
|
default:
|
||||||
|
return "red";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -115,3 +115,19 @@ export function shapeFromCoords(coordinates: number[][]): pond.Shape {
|
||||||
});
|
});
|
||||||
return shape;
|
return shape;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function coordDistance(lat1: number, long1: number, lat2: number, long2: number) {
|
||||||
|
var R = 6371; // Radius of the earth in km
|
||||||
|
var dLat = deg2rad(lat2 - lat1); // deg2rad below
|
||||||
|
var dLon = deg2rad(long2 - long1);
|
||||||
|
var a =
|
||||||
|
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||||
|
Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||||
|
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||||
|
var d = R * c; // Distance in km
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deg2rad(deg: number) {
|
||||||
|
return deg * (Math.PI / 180);
|
||||||
|
}
|
||||||
|
|
|
||||||
45
src/models/HomeMarker.ts
Normal file
45
src/models/HomeMarker.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { cloneDeep } from "lodash";
|
||||||
|
import { pond } from "protobuf-ts/pond";
|
||||||
|
import { or } from "utils/types";
|
||||||
|
|
||||||
|
export class HomeMarker {
|
||||||
|
public settings: pond.HomeMarkerSettings = pond.HomeMarkerSettings.create();
|
||||||
|
public status: pond.HomeMarkerStatus = pond.HomeMarkerStatus.create();
|
||||||
|
|
||||||
|
public static create(hm?: pond.HomeMarker): HomeMarker {
|
||||||
|
let my = new HomeMarker();
|
||||||
|
if (hm) {
|
||||||
|
my.settings = pond.HomeMarkerSettings.fromObject(cloneDeep(or(hm.settings, {})));
|
||||||
|
my.status = pond.HomeMarkerStatus.fromObject(cloneDeep(or(hm.status, {})));
|
||||||
|
}
|
||||||
|
return my;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static clone(other?: HomeMarker): HomeMarker {
|
||||||
|
if (other) {
|
||||||
|
return HomeMarker.create(
|
||||||
|
pond.HomeMarker.fromObject({
|
||||||
|
settings: cloneDeep(other.settings),
|
||||||
|
status: cloneDeep(other.status)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return HomeMarker.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static any(data: any): HomeMarker {
|
||||||
|
return HomeMarker.create(pond.HomeMarker.fromObject(cloneDeep(data)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public key(): string {
|
||||||
|
return this.settings.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long(): number {
|
||||||
|
return this.settings.longitude;
|
||||||
|
}
|
||||||
|
|
||||||
|
public lat(): number {
|
||||||
|
return this.settings.latitude;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,6 +21,6 @@ export * from "./team";
|
||||||
// export * from "./HarvestYear";
|
// export * from "./HarvestYear";
|
||||||
export * from "./Note";
|
export * from "./Note";
|
||||||
// export * from "./FieldMapping";
|
// export * from "./FieldMapping";
|
||||||
// export * from "./FieldMarker";
|
export * from "./FieldMarker";
|
||||||
export * from "./GrainBag";
|
export * from "./GrainBag";
|
||||||
export * from "./Contract";
|
export * from "./Contract";
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import Logout from "pages/Logout";
|
||||||
import { ErrorBoundary } from "react-error-boundary";
|
import { ErrorBoundary } from "react-error-boundary";
|
||||||
import { getWhitelabel } from "services/whiteLabel";
|
import { getWhitelabel } from "services/whiteLabel";
|
||||||
import Ventilation from "pages/VentEditor";
|
import Ventilation from "pages/VentEditor";
|
||||||
|
import FieldMap from "pages/FieldMap";
|
||||||
|
|
||||||
const DeviceHistory = lazy(() => import("pages/DeviceHistory"));
|
const DeviceHistory = lazy(() => import("pages/DeviceHistory"));
|
||||||
const DevicePage = lazy(() => import("pages/Device"));
|
const DevicePage = lazy(() => import("pages/Device"));
|
||||||
|
|
@ -219,6 +220,7 @@ export default function Router(props: Props) {
|
||||||
{/* Page routes */}
|
{/* Page routes */}
|
||||||
{/* <Route index element={<Typography>Hello!</Typography>} /> */}
|
{/* <Route index element={<Typography>Hello!</Typography>} /> */}
|
||||||
<Route path="users" element={<Users/>} />
|
<Route path="users" element={<Users/>} />
|
||||||
|
<Route path="visualFarm" element={<FieldMap />} />
|
||||||
<Route path="/logout" element={<Logout />} />
|
<Route path="/logout" element={<Logout />} />
|
||||||
{/*
|
{/*
|
||||||
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import BinsIcon from "products/Bindapt/BinsIcon";
|
import BinsIcon from "products/Bindapt/BinsIcon";
|
||||||
import { useGlobalState } from "providers";
|
import { useGlobalState } from "providers";
|
||||||
import {
|
import {
|
||||||
|
IsAdaptiveAgriculture,
|
||||||
// hasTutorialPlaylist,
|
// hasTutorialPlaylist,
|
||||||
// IsAdaptiveAgriculture,
|
// IsAdaptiveAgriculture,
|
||||||
// IsAdCon,
|
// IsAdCon,
|
||||||
|
|
@ -33,6 +34,7 @@ import {
|
||||||
} from "services/whiteLabel";
|
} from "services/whiteLabel";
|
||||||
import MiningIcon from "products/ventilation/MiningIcon";
|
import MiningIcon from "products/ventilation/MiningIcon";
|
||||||
import { useAuth0 } from "@auth0/auth0-react";
|
import { useAuth0 } from "@auth0/auth0-react";
|
||||||
|
import FieldsIcon from "products/AgIcons/FieldsIcon";
|
||||||
|
|
||||||
const drawerWidth = 230;
|
const drawerWidth = 230;
|
||||||
|
|
||||||
|
|
@ -137,8 +139,10 @@ export default function SideNavigator(props: Props) {
|
||||||
|
|
||||||
const authenticatedSideMenu = () => {
|
const authenticatedSideMenu = () => {
|
||||||
const isMiVent = IsMiVent();
|
const isMiVent = IsMiVent();
|
||||||
|
const isAg = IsAdaptiveAgriculture()
|
||||||
return (
|
return (
|
||||||
<List className={classes.list} component="nav">
|
<List className={classes.list} component="nav">
|
||||||
|
{(isAg || user.hasFeature("admin")) && (
|
||||||
<Tooltip title="Bins" placement="right">
|
<Tooltip title="Bins" placement="right">
|
||||||
<ListItemButton
|
<ListItemButton
|
||||||
id="tour-dashboard"
|
id="tour-dashboard"
|
||||||
|
|
@ -151,6 +155,7 @@ export default function SideNavigator(props: Props) {
|
||||||
{open && <ListItemText primary="Bins" />}
|
{open && <ListItemText primary="Bins" />}
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
)}
|
||||||
<Tooltip title="Devices" placement="right">
|
<Tooltip title="Devices" placement="right">
|
||||||
<ListItemButton
|
<ListItemButton
|
||||||
id="tour-dashboard"
|
id="tour-dashboard"
|
||||||
|
|
@ -201,6 +206,20 @@ export default function SideNavigator(props: Props) {
|
||||||
{open && <ListItemText primary="Users" />}
|
{open && <ListItemText primary="Users" />}
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
{(isAg || user.hasFeature("admin")) && (
|
||||||
|
<Tooltip title="Visual Farm" placement="right">
|
||||||
|
<ListItemButton
|
||||||
|
id="tour-dashboard"
|
||||||
|
onClick={() => goTo("/visualFarm")}
|
||||||
|
classes={getClasses("/visualFarm")}
|
||||||
|
>
|
||||||
|
<ListItemIcon>
|
||||||
|
<FieldsIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
{open && <ListItemText primary="Visual Farm" />}
|
||||||
|
</ListItemButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
</List>
|
</List>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -566,7 +566,7 @@ export default function Bin(props: Props) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const goToYard = (bin: IBin) => {
|
const goToYard = (bin: IBin) => {
|
||||||
navigate("/fields", { state: {
|
navigate("/visualFarm", { state: {
|
||||||
long: bin.getLocation()?.longitude,
|
long: bin.getLocation()?.longitude,
|
||||||
lat: bin.getLocation()?.latitude
|
lat: bin.getLocation()?.latitude
|
||||||
}
|
}
|
||||||
|
|
|
||||||
26
src/pages/FieldMap.tsx
Normal file
26
src/pages/FieldMap.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import AgMapController from "maps/mapObjectControllers/AgMapController";
|
||||||
|
import PageContainer from "./PageContainer";
|
||||||
|
import { useLocation } from "react-router";
|
||||||
|
|
||||||
|
export default function FieldMap() {
|
||||||
|
const location = useLocation();
|
||||||
|
return (
|
||||||
|
<PageContainer>
|
||||||
|
{location.state !== undefined &&
|
||||||
|
location.state !== null &&
|
||||||
|
location.state.long !== undefined &&
|
||||||
|
location.state.lat !== undefined ? (
|
||||||
|
<AgMapController
|
||||||
|
startingView={{
|
||||||
|
longitude: location.state.long,
|
||||||
|
latitude: location.state.lat,
|
||||||
|
zoom: 13,
|
||||||
|
transitionDuration: 0
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<AgMapController />
|
||||||
|
)}
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,6 @@ import FieldsIconLight from "assets/products/Ag/FieldsIconLight.png";
|
||||||
import FieldsIconDark from "assets/products/Ag/FieldsIconDark.png";
|
import FieldsIconDark from "assets/products/Ag/FieldsIconDark.png";
|
||||||
import { ImgIcon } from "common/ImgIcon";
|
import { ImgIcon } from "common/ImgIcon";
|
||||||
import { useThemeType } from "hooks";
|
import { useThemeType } from "hooks";
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
type?: "light" | "dark";
|
type?: "light" | "dark";
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,12 @@ export {
|
||||||
useDeviceAPI,
|
useDeviceAPI,
|
||||||
// useDeviceWebsocket,
|
// useDeviceWebsocket,
|
||||||
useFieldAPI,
|
useFieldAPI,
|
||||||
// useFieldMarkerAPI,
|
useFieldMarkerAPI,
|
||||||
useFirmwareAPI,
|
useFirmwareAPI,
|
||||||
useGroupAPI,
|
useGroupAPI,
|
||||||
// useHarvestPlanAPI,
|
// useHarvestPlanAPI,
|
||||||
// useHarvestYearAPI,
|
// useHarvestYearAPI,
|
||||||
// useHomeMarkerAPI,
|
useHomeMarkerAPI,
|
||||||
useInteractionsAPI,
|
useInteractionsAPI,
|
||||||
// useMeasurementsWebsocket,
|
// useMeasurementsWebsocket,
|
||||||
// useMetricAPI,
|
// useMetricAPI,
|
||||||
|
|
|
||||||
113
src/providers/pond/fieldMarkerAPI.tsx
Normal file
113
src/providers/pond/fieldMarkerAPI.tsx
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
import { AxiosResponse } from "axios";
|
||||||
|
import { useHTTP } from "hooks";
|
||||||
|
import { pond } from "protobuf-ts/pond";
|
||||||
|
import { useGlobalState } from "providers";
|
||||||
|
import { createContext, PropsWithChildren, useContext } from "react";
|
||||||
|
import { pondURL } from "./pond";
|
||||||
|
|
||||||
|
export interface IFieldMarkerAPIContext {
|
||||||
|
addFieldMarker: (
|
||||||
|
fieldMarker: pond.FieldMarkerSettings
|
||||||
|
) => Promise<AxiosResponse<pond.AddFieldMarkerResponse>>;
|
||||||
|
getFieldMarker: (fieldMarkerID: string) => Promise<AxiosResponse<pond.FieldMarker>>;
|
||||||
|
listFieldMarkers: (
|
||||||
|
limit: number,
|
||||||
|
offset: number,
|
||||||
|
order?: "asc" | "desc",
|
||||||
|
orderBy?: string,
|
||||||
|
search?: string,
|
||||||
|
asRoot?: boolean,
|
||||||
|
as?: string
|
||||||
|
) => Promise<AxiosResponse<pond.ListFieldMarkersResponse>>;
|
||||||
|
removeFieldMarker: (
|
||||||
|
fieldMarkerID: string
|
||||||
|
) => Promise<AxiosResponse<pond.RemoveFieldMarkerResponse>>;
|
||||||
|
updateFieldMarker: (
|
||||||
|
key: string,
|
||||||
|
fieldMarker: pond.FieldMarkerSettings,
|
||||||
|
asRoot?: true
|
||||||
|
) => Promise<AxiosResponse<pond.UpdateFieldMarkerResponse>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FieldMarkersAPIContext = createContext<IFieldMarkerAPIContext>(
|
||||||
|
{} as IFieldMarkerAPIContext
|
||||||
|
);
|
||||||
|
|
||||||
|
interface Props {}
|
||||||
|
|
||||||
|
export default function FieldMarkerProvider(props: PropsWithChildren<Props>) {
|
||||||
|
const { children } = props;
|
||||||
|
const { get, del, post, put } = useHTTP();
|
||||||
|
const [{ as }] = useGlobalState();
|
||||||
|
|
||||||
|
const addFieldMarker = (fieldMarker: pond.FieldMarkerSettings) => {
|
||||||
|
if (as)
|
||||||
|
return post<pond.AddFieldMarkerResponse>(pondURL("/fieldMarkers?as=" + as), fieldMarker);
|
||||||
|
return post<pond.AddFieldMarkerResponse>(pondURL("/fieldMarkers"), fieldMarker);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFieldMarker = (fieldMarkerId: string) => {
|
||||||
|
if (as) return get<pond.FieldMarker>(pondURL("/fieldMarkers/" + fieldMarkerId + "?as=" + as));
|
||||||
|
return get<pond.FieldMarker>(pondURL("/fieldMarkers/" + fieldMarkerId));
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeFieldMarker = (key: string) => {
|
||||||
|
if (as)
|
||||||
|
return del<pond.RemoveFieldMarkerResponse>(pondURL("/fieldMarkers/" + key + "?as=" + as));
|
||||||
|
return del<pond.RemoveFieldMarkerResponse>(pondURL("/fieldMarkers/" + key));
|
||||||
|
};
|
||||||
|
|
||||||
|
const listFieldMarkers = (
|
||||||
|
limit: number,
|
||||||
|
offset: number,
|
||||||
|
order?: "asc" | "desc",
|
||||||
|
orderBy?: string,
|
||||||
|
search?: string,
|
||||||
|
asRoot?: boolean,
|
||||||
|
as?: string
|
||||||
|
) => {
|
||||||
|
return get<pond.ListFieldMarkersResponse>(
|
||||||
|
pondURL(
|
||||||
|
"/fieldMarkers" +
|
||||||
|
"?limit=" +
|
||||||
|
limit +
|
||||||
|
"&offset=" +
|
||||||
|
offset +
|
||||||
|
("&order=" + (order ? order : "asc")) +
|
||||||
|
("&by=" + (orderBy ? orderBy : "key")) +
|
||||||
|
(search ? "&search=" + search : "") +
|
||||||
|
(asRoot ? "&asRoot=" + asRoot.toString() : "") +
|
||||||
|
(as ? "&as=" + as : "")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateFieldMarker = (key: string, hm: pond.FieldMarkerSettings, asRoot?: boolean) => {
|
||||||
|
if (as)
|
||||||
|
return put<pond.UpdateFieldMarkerResponse>(
|
||||||
|
pondURL(
|
||||||
|
"/fieldMarkers/" + key + "?as=" + as + (asRoot ? "&asRoot=" + asRoot.toString() : "")
|
||||||
|
),
|
||||||
|
hm
|
||||||
|
);
|
||||||
|
return put<pond.UpdateFieldMarkerResponse>(
|
||||||
|
pondURL("/fieldMarkers/" + key + (asRoot ? "?asRoot=" + asRoot.toString() : "")),
|
||||||
|
hm
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldMarkersAPIContext.Provider
|
||||||
|
value={{
|
||||||
|
addFieldMarker,
|
||||||
|
getFieldMarker,
|
||||||
|
removeFieldMarker,
|
||||||
|
listFieldMarkers,
|
||||||
|
updateFieldMarker
|
||||||
|
}}>
|
||||||
|
{children}
|
||||||
|
</FieldMarkersAPIContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useFieldMarkerAPI = () => useContext(FieldMarkersAPIContext);
|
||||||
107
src/providers/pond/homeMarkerAPI.tsx
Normal file
107
src/providers/pond/homeMarkerAPI.tsx
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
import { AxiosResponse } from "axios";
|
||||||
|
import { useHTTP } from "hooks";
|
||||||
|
import { pond } from "protobuf-ts/pond";
|
||||||
|
import { useGlobalState } from "providers";
|
||||||
|
import { createContext, PropsWithChildren, useContext } from "react";
|
||||||
|
import { pondURL } from "./pond";
|
||||||
|
|
||||||
|
export interface IHomeMarkerAPIContext {
|
||||||
|
addHomeMarker: (
|
||||||
|
homeMarker: pond.HomeMarkerSettings
|
||||||
|
) => Promise<AxiosResponse<pond.AddHomeMarkerResponse>>;
|
||||||
|
getHomeMarker: (homeMarkerID: string) => Promise<AxiosResponse<pond.HomeMarker>>;
|
||||||
|
listHomeMarkers: (
|
||||||
|
limit: number,
|
||||||
|
offset: number,
|
||||||
|
order?: "asc" | "desc",
|
||||||
|
orderBy?: string,
|
||||||
|
search?: string,
|
||||||
|
asRoot?: boolean,
|
||||||
|
as?: string
|
||||||
|
) => Promise<AxiosResponse<pond.ListHomeMarkersResponse>>;
|
||||||
|
removeHomeMarker: (homeMarkerID: string) => Promise<AxiosResponse<pond.RemoveHomeMarkerResponse>>;
|
||||||
|
updateHomeMarker: (
|
||||||
|
key: string,
|
||||||
|
homeMarker: pond.HomeMarkerSettings,
|
||||||
|
asRoot?: true
|
||||||
|
) => Promise<AxiosResponse<pond.UpdateHomeMarkerResponse>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HomeMarkersAPIContext = createContext<IHomeMarkerAPIContext>(
|
||||||
|
{} as IHomeMarkerAPIContext
|
||||||
|
);
|
||||||
|
|
||||||
|
interface Props {}
|
||||||
|
|
||||||
|
export default function HomeMarkerProvider(props: PropsWithChildren<Props>) {
|
||||||
|
const { children } = props;
|
||||||
|
const { get, del, post, put } = useHTTP();
|
||||||
|
const [{ as }] = useGlobalState();
|
||||||
|
|
||||||
|
const addHomeMarker = (homeMarker: pond.HomeMarkerSettings) => {
|
||||||
|
if (as) return post<pond.AddHomeMarkerResponse>(pondURL("/homeMarkers?as=" + as), homeMarker);
|
||||||
|
return post<pond.AddHomeMarkerResponse>(pondURL("/homeMarkers"), homeMarker);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHomeMarker = (homeMarkerId: string) => {
|
||||||
|
return get<pond.HomeMarker>(pondURL("/homeMarkers/" + homeMarkerId));
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeHomeMarker = (key: string) => {
|
||||||
|
return del<pond.RemoveHomeMarkerResponse>(pondURL("/homeMarkers/" + key));
|
||||||
|
};
|
||||||
|
|
||||||
|
const listHomeMarkers = (
|
||||||
|
limit: number,
|
||||||
|
offset: number,
|
||||||
|
order?: "asc" | "desc",
|
||||||
|
orderBy?: string,
|
||||||
|
search?: string,
|
||||||
|
asRoot?: boolean,
|
||||||
|
as?: string
|
||||||
|
) => {
|
||||||
|
return get<pond.ListHomeMarkersResponse>(
|
||||||
|
pondURL(
|
||||||
|
"/homeMarkers" +
|
||||||
|
"?limit=" +
|
||||||
|
limit +
|
||||||
|
"&offset=" +
|
||||||
|
offset +
|
||||||
|
("&order=" + (order ? order : "asc")) +
|
||||||
|
("&by=" + (orderBy ? orderBy : "key")) +
|
||||||
|
(search ? "&search=" + search : "") +
|
||||||
|
(asRoot ? "&asRoot=" + asRoot.toString() : "") +
|
||||||
|
(as ? "&as=" + as : "")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateHomeMarker = (key: string, hm: pond.HomeMarkerSettings, asRoot?: boolean) => {
|
||||||
|
if (as)
|
||||||
|
return put<pond.UpdateHomeMarkerResponse>(
|
||||||
|
pondURL(
|
||||||
|
"/homeMarkers/" + key + "?as=" + as + (asRoot ? "&asRoot=" + asRoot.toString() : "")
|
||||||
|
),
|
||||||
|
hm
|
||||||
|
);
|
||||||
|
return put<pond.UpdateHomeMarkerResponse>(
|
||||||
|
pondURL("/homeMarkers/" + key + (asRoot ? "?asRoot=" + asRoot.toString() : "")),
|
||||||
|
hm
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HomeMarkersAPIContext.Provider
|
||||||
|
value={{
|
||||||
|
addHomeMarker,
|
||||||
|
getHomeMarker,
|
||||||
|
removeHomeMarker,
|
||||||
|
listHomeMarkers,
|
||||||
|
updateHomeMarker
|
||||||
|
}}>
|
||||||
|
{children}
|
||||||
|
</HomeMarkersAPIContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useHomeMarkerAPI = () => useContext(HomeMarkersAPIContext);
|
||||||
|
|
@ -21,6 +21,8 @@ import FieldProvider, { useFieldAPI } from "./fieldAPI";
|
||||||
import TransactionProvider, { useTransactionAPI } from "./transactionAPI";
|
import TransactionProvider, { useTransactionAPI } from "./transactionAPI";
|
||||||
import FileControllerProvider, { useFileControllerAPI } from "./fileControllerAPI";
|
import FileControllerProvider, { useFileControllerAPI } from "./fileControllerAPI";
|
||||||
import MineProvider, { useMineAPI } from "./mineAPI";
|
import MineProvider, { useMineAPI } from "./mineAPI";
|
||||||
|
import FieldMarkerProvider, {useFieldMarkerAPI} from "./fieldMarkerAPI";
|
||||||
|
import HomeMarkerProvider, {useHomeMarkerAPI} from "./homeMarkerAPI";
|
||||||
// import NoteProvider from "providers/noteAPI";
|
// import NoteProvider from "providers/noteAPI";
|
||||||
|
|
||||||
export const pondURL = (partial: string, demo: boolean = false): string => {
|
export const pondURL = (partial: string, demo: boolean = false): string => {
|
||||||
|
|
@ -58,7 +60,11 @@ export default function PondProvider(props: PropsWithChildren<any>) {
|
||||||
<TransactionProvider>
|
<TransactionProvider>
|
||||||
<FileControllerProvider>
|
<FileControllerProvider>
|
||||||
<MineProvider>
|
<MineProvider>
|
||||||
|
<FieldMarkerProvider>
|
||||||
|
<HomeMarkerProvider>
|
||||||
{children}
|
{children}
|
||||||
|
</HomeMarkerProvider>
|
||||||
|
</FieldMarkerProvider>
|
||||||
</MineProvider>
|
</MineProvider>
|
||||||
</FileControllerProvider>
|
</FileControllerProvider>
|
||||||
</TransactionProvider>
|
</TransactionProvider>
|
||||||
|
|
@ -104,4 +110,6 @@ export {
|
||||||
useTransactionAPI,
|
useTransactionAPI,
|
||||||
useFileControllerAPI,
|
useFileControllerAPI,
|
||||||
useMineAPI,
|
useMineAPI,
|
||||||
|
useFieldMarkerAPI,
|
||||||
|
useHomeMarkerAPI
|
||||||
};
|
};
|
||||||
|
|
|
||||||
314
src/weather/weather.tsx
Normal file
314
src/weather/weather.tsx
Normal file
|
|
@ -0,0 +1,314 @@
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Grid2 as Grid,
|
||||||
|
LinearProgress,
|
||||||
|
Typography,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { useState } from "react";
|
||||||
|
import OpenWeatherMap from "openweathermap-ts";
|
||||||
|
import { CurrentResponse, ThreeHourResponse } from "openweathermap-ts/dist/types";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
//import WeatherIcon from "./weatherIcon";
|
||||||
|
import { getThemeType } from "theme";
|
||||||
|
import { useMobile, useThemeType } from "hooks";
|
||||||
|
import WindSpeedDark from "assets/components/windSpeedDark.png";
|
||||||
|
import WindSpeedLight from "assets/components/windSpeedLight.png";
|
||||||
|
import { makeStyles } from "@mui/styles";
|
||||||
|
//import { WiHumidity } from "weather-icons-react";
|
||||||
|
|
||||||
|
const openWeather = new OpenWeatherMap({
|
||||||
|
//apiKey: process.env.REACT_APP_OPEN_WEATHERMAP ? process.env.REACT_APP_OPEN_WEATHERMAP : ""
|
||||||
|
apiKey: import.meta.env.REACT_APP_OPEN_WEATHERMAP ? import.meta.env.REACT_APP_OPEN_WEATHERMAP : ""
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
longitude?: number;
|
||||||
|
latitude?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles(() => ({
|
||||||
|
currentBox: {
|
||||||
|
boxShadow: "0px 0px 10px black",
|
||||||
|
height: "110px",
|
||||||
|
borderRadius: 10,
|
||||||
|
margin: 7
|
||||||
|
},
|
||||||
|
upperSquare: {
|
||||||
|
height: "75%",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center"
|
||||||
|
},
|
||||||
|
lowerSquare: {
|
||||||
|
height: "25%",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center"
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
backgroundColor: getThemeType() === "light" ? "rgb(245, 245, 245)" : "rgb(50, 50, 50)",
|
||||||
|
padding: 5,
|
||||||
|
opacity: 1
|
||||||
|
},
|
||||||
|
light: {
|
||||||
|
backgroundColor: getThemeType() === "light" ? "rgb(235, 235, 235)" : "rgb(60, 60, 60)",
|
||||||
|
padding: 5,
|
||||||
|
opacity: 1
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
export default function Weather(props: Props) {
|
||||||
|
const [currentConditions, setCurrentConditions] = useState<CurrentResponse>();
|
||||||
|
const [forecast, setForecast] = useState<ThreeHourResponse>();
|
||||||
|
const classes = useStyles();
|
||||||
|
const isMobile = useMobile();
|
||||||
|
const themeType = useThemeType();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.longitude && props.latitude) {
|
||||||
|
openWeather
|
||||||
|
.getThreeHourForecastByGeoCoordinates(props.latitude, props.longitude)
|
||||||
|
.then(forecast => {
|
||||||
|
setForecast(forecast);
|
||||||
|
});
|
||||||
|
openWeather
|
||||||
|
.getCurrentWeatherByGeoCoordinates(props.latitude, props.longitude)
|
||||||
|
.then(weather => {
|
||||||
|
setCurrentConditions(weather);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [props.latitude, props.longitude]);
|
||||||
|
|
||||||
|
const celsiusConverter = (temp: number) => {
|
||||||
|
return (temp - 32) * (5 / 9);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* gets the high and low of the first 8 readings in the 5 day forecast, is technically getting the high and low for tomorrow
|
||||||
|
* but is usually close to the same temperatures, for actual current high/low must use a paid plan for open weather
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const getHighLow = () => {
|
||||||
|
if (!forecast) return;
|
||||||
|
let loopCounter = 0;
|
||||||
|
let currentHigh = -1000;
|
||||||
|
let currentLow = 1000;
|
||||||
|
while (loopCounter <= 7) {
|
||||||
|
let temp = forecast.list[loopCounter].main.temp;
|
||||||
|
if (temp < currentLow) {
|
||||||
|
currentLow = temp;
|
||||||
|
}
|
||||||
|
if (temp > currentHigh) {
|
||||||
|
currentHigh = temp;
|
||||||
|
}
|
||||||
|
loopCounter++;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Typography variant="h5" style={{ fontSize: 25 }}>
|
||||||
|
{Math.round(celsiusConverter(currentHigh))}° /{" "}
|
||||||
|
{Math.round(celsiusConverter(currentLow))}°
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDirection = (current: CurrentResponse) => {
|
||||||
|
if (!current) return;
|
||||||
|
let windDegrees = current.wind.deg;
|
||||||
|
let direction = "";
|
||||||
|
|
||||||
|
if (windDegrees > 338 || windDegrees <= 23) {
|
||||||
|
direction = "N";
|
||||||
|
}
|
||||||
|
if (windDegrees > 23 && windDegrees <= 67) {
|
||||||
|
direction = "NE";
|
||||||
|
}
|
||||||
|
if (windDegrees > 67 && windDegrees <= 112) {
|
||||||
|
direction = "E";
|
||||||
|
}
|
||||||
|
if (windDegrees > 112 && windDegrees <= 158) {
|
||||||
|
direction = "SE";
|
||||||
|
}
|
||||||
|
if (windDegrees > 158 && windDegrees <= 203) {
|
||||||
|
direction = "S";
|
||||||
|
}
|
||||||
|
if (windDegrees > 203 && windDegrees <= 248) {
|
||||||
|
direction = "SW";
|
||||||
|
}
|
||||||
|
if (windDegrees > 248 && windDegrees <= 293) {
|
||||||
|
direction = "W";
|
||||||
|
}
|
||||||
|
if (windDegrees > 293 && windDegrees <= 338) {
|
||||||
|
direction = "NW";
|
||||||
|
}
|
||||||
|
return direction;
|
||||||
|
};
|
||||||
|
|
||||||
|
const dayOfWeek = (day: number) => {
|
||||||
|
switch (day) {
|
||||||
|
case 1:
|
||||||
|
return "Monday";
|
||||||
|
case 2:
|
||||||
|
return "Tuesday";
|
||||||
|
case 3:
|
||||||
|
return "Wednesday";
|
||||||
|
case 4:
|
||||||
|
return "Thursday";
|
||||||
|
case 5:
|
||||||
|
return "Friday";
|
||||||
|
case 6:
|
||||||
|
return "Saturday";
|
||||||
|
default:
|
||||||
|
return "Sunday";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Three hour response contains a list of 40 items for the weather, each 8 is a 24 hour period
|
||||||
|
* loop through 8 items to get the forecast for that 24 hour period
|
||||||
|
* @param forecast the three hour response that was retrieved
|
||||||
|
* @param day the day we are getting the forecast for as a number
|
||||||
|
*/
|
||||||
|
const getForeCast = (day: number) => {
|
||||||
|
if (!forecast) return;
|
||||||
|
let startingPoint = day * 8;
|
||||||
|
let loopCount = 0;
|
||||||
|
let high = -1000;
|
||||||
|
let low = 1000;
|
||||||
|
let d = forecast.list[startingPoint + 4].dt;
|
||||||
|
let date = new Date(0);
|
||||||
|
date.setUTCSeconds(d);
|
||||||
|
|
||||||
|
while (loopCount < 8) {
|
||||||
|
let temp = forecast.list[startingPoint].main.temp;
|
||||||
|
if (temp > high) {
|
||||||
|
high = temp;
|
||||||
|
}
|
||||||
|
if (temp < low) {
|
||||||
|
low = temp;
|
||||||
|
}
|
||||||
|
startingPoint++;
|
||||||
|
loopCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box className={day % 2 === 0 ? classes.dark : classes.light}>
|
||||||
|
<Box style={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
|
<Typography variant="subtitle1" style={{ fontWeight: 600 }}>
|
||||||
|
{dayOfWeek(date.getDay())}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="subtitle1" style={{ fontWeight: 600 }}>
|
||||||
|
{Math.round(celsiusConverter(high))}° / {Math.round(celsiusConverter(low))}°
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box padding="25px">
|
||||||
|
{currentConditions && forecast ? (
|
||||||
|
<Box>
|
||||||
|
<Box component="span">
|
||||||
|
<Typography style={{ fontSize: 20, fontWeight: 750 }}>
|
||||||
|
Current Weather - {dayOfWeek(new Date().getDay())}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Grid container direction="row">
|
||||||
|
<Grid size={6}>
|
||||||
|
<Box className={classes.currentBox}>
|
||||||
|
<Box
|
||||||
|
padding="5%"
|
||||||
|
className={classes.upperSquare}
|
||||||
|
style={{ flexDirection: isMobile ? "column" : "row" }}>
|
||||||
|
{/* <WeatherIcon
|
||||||
|
openweatherIconId={currentConditions.weather[0].id}
|
||||||
|
size={isMobile ? 35 : 50}
|
||||||
|
/> */}
|
||||||
|
<Typography variant="body1" style={{ fontSize: 25 }}>
|
||||||
|
{Math.round(celsiusConverter(currentConditions.main.temp))}℃
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box className={classes.lowerSquare}>
|
||||||
|
<Typography variant="subtitle1" style={{ fontSize: isMobile ? 10 : 15 }}>
|
||||||
|
Feels like {Math.round(celsiusConverter(currentConditions.main.feels_like))}
|
||||||
|
℃
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={6}>
|
||||||
|
<Box className={classes.currentBox}>
|
||||||
|
<Box
|
||||||
|
className={classes.upperSquare}
|
||||||
|
style={{ flexDirection: isMobile ? "column" : "row" }}>
|
||||||
|
{/* <WiHumidity size={isMobile ? 35 : 50} /> */}
|
||||||
|
<Typography variant="body1" style={{ fontSize: 25, display: "inline" }}>
|
||||||
|
{currentConditions.main.humidity}%
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box className={classes.lowerSquare}>
|
||||||
|
<Typography variant="subtitle1" style={{ fontSize: isMobile ? 10 : 15 }}>
|
||||||
|
Humidity
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={6}>
|
||||||
|
<Box className={classes.currentBox}>
|
||||||
|
<Box
|
||||||
|
className={classes.upperSquare}
|
||||||
|
style={{ flexDirection: isMobile ? "column" : "row" }}>
|
||||||
|
<img
|
||||||
|
src={themeType === "light" ? WindSpeedDark : WindSpeedLight}
|
||||||
|
alt="Wind Speed"
|
||||||
|
height={isMobile ? 35 : 50}
|
||||||
|
width={isMobile ? 35 : 50}
|
||||||
|
/>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="body1" style={{ fontSize: 25, display: "inline" }}>
|
||||||
|
{Math.round(currentConditions.wind.speed * 3.6)}/
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="subtitle1" style={{ fontSize: 15, display: "inline" }}>
|
||||||
|
km/h
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box className={classes.lowerSquare}>
|
||||||
|
<Typography variant="subtitle1" style={{ fontSize: isMobile ? 10 : 15 }}>
|
||||||
|
Direction {getDirection(currentConditions)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={6}>
|
||||||
|
<Box className={classes.currentBox}>
|
||||||
|
<Box className={classes.upperSquare} style={{ paddingTop: isMobile ? 35 : 0 }}>
|
||||||
|
{getHighLow()}
|
||||||
|
</Box>
|
||||||
|
<Box className={classes.lowerSquare}>
|
||||||
|
<Typography variant="subtitle1" style={{ fontSize: isMobile ? 10 : 15 }}>
|
||||||
|
Today's High/Low
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<Box component="span">
|
||||||
|
<Typography style={{ fontSize: 20, fontWeight: 750 }}>5 - Day Forecast</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
{getForeCast(0)}
|
||||||
|
{getForeCast(1)}
|
||||||
|
{getForeCast(2)}
|
||||||
|
{getForeCast(3)}
|
||||||
|
{getForeCast(4)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Box paddingTop="20px">
|
||||||
|
<LinearProgress color="primary" />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
114
src/weather/weatherIcon.tsx
Normal file
114
src/weather/weatherIcon.tsx
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
import { AcUnit, Cloud, LightMode, Thunderstorm, WaterDrop } from "@mui/icons-material";
|
||||||
|
// import {
|
||||||
|
// WiDaySunny,
|
||||||
|
// WiDayCloudy,
|
||||||
|
// WiDaySunnyOvercast,
|
||||||
|
// WiDayLightning,
|
||||||
|
// WiDayStormShowers,
|
||||||
|
// WiDayThunderstorm,
|
||||||
|
// WiDaySprinkle,
|
||||||
|
// WiDayRainMix,
|
||||||
|
// WiDayRain,
|
||||||
|
// WiDayShowers,
|
||||||
|
// WiDayRainWind,
|
||||||
|
// WiDaySleet,
|
||||||
|
// WiDaySnow,
|
||||||
|
// WiDayFog,
|
||||||
|
// WiDayHaze,
|
||||||
|
// WiDust,
|
||||||
|
// WiSmoke,
|
||||||
|
// WiVolcano,
|
||||||
|
// WiTornado
|
||||||
|
// } from "weather-icons-react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
openweatherIconId: number;
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
//need to get Dustin/Sandaru to make some custom weather icons
|
||||||
|
export default function WeatherIcon(props: Props) {
|
||||||
|
const size = props.size ? props.size : 25;
|
||||||
|
const iconId = props.openweatherIconId;
|
||||||
|
|
||||||
|
switch (iconId) {
|
||||||
|
case 200:
|
||||||
|
case 210:
|
||||||
|
case 221:
|
||||||
|
case 230:
|
||||||
|
//return <WiDayLightning size={size} />;
|
||||||
|
case 201:
|
||||||
|
case 211:
|
||||||
|
case 231:
|
||||||
|
//return <WiDayStormShowers size={size} />;
|
||||||
|
case 202:
|
||||||
|
case 212:
|
||||||
|
case 232:
|
||||||
|
return <Thunderstorm />
|
||||||
|
//return <WiDayThunderstorm size={size} />;
|
||||||
|
case 300:
|
||||||
|
case 301:
|
||||||
|
case 302:
|
||||||
|
case 500:
|
||||||
|
//return <WiDaySprinkle size={size} />;
|
||||||
|
case 310:
|
||||||
|
case 311:
|
||||||
|
case 312:
|
||||||
|
case 501:
|
||||||
|
//return <WiDayRainMix size={size} />;
|
||||||
|
case 502:
|
||||||
|
case 503:
|
||||||
|
case 522:
|
||||||
|
//return <WiDayRain size={size} />;
|
||||||
|
case 314:
|
||||||
|
case 321:
|
||||||
|
case 520:
|
||||||
|
case 521:
|
||||||
|
case 531:
|
||||||
|
//return <WiDayShowers size={size} />;
|
||||||
|
case 504:
|
||||||
|
//return <WiDayRainWind size={size} />;
|
||||||
|
case 511:
|
||||||
|
case 611:
|
||||||
|
case 612:
|
||||||
|
case 613:
|
||||||
|
case 615:
|
||||||
|
case 616:
|
||||||
|
case 620:
|
||||||
|
case 621:
|
||||||
|
case 622:
|
||||||
|
return <WaterDrop />
|
||||||
|
//return <WiDaySleet size={size} />;
|
||||||
|
case 600:
|
||||||
|
case 601:
|
||||||
|
case 602:
|
||||||
|
return <AcUnit />
|
||||||
|
//return <WiDaySnow size={size} />;
|
||||||
|
// case 701:
|
||||||
|
// case 741:
|
||||||
|
// return <WiDayFog size={size} />;
|
||||||
|
// case 711:
|
||||||
|
// return <WiSmoke size={size} />;
|
||||||
|
// case 721:
|
||||||
|
// return <WiDayHaze size={size} />;
|
||||||
|
// case 731:
|
||||||
|
// case 751:
|
||||||
|
// case 761:
|
||||||
|
// return <WiDust size={size} />;
|
||||||
|
// case 762:
|
||||||
|
// return <WiVolcano size={size} />;
|
||||||
|
// case 771:
|
||||||
|
// case 781:
|
||||||
|
// return <WiTornado size={size} />;
|
||||||
|
case 801:
|
||||||
|
case 802:
|
||||||
|
//return <WiDaySunnyOvercast size={size} />;
|
||||||
|
case 803:
|
||||||
|
case 804:
|
||||||
|
return <Cloud />
|
||||||
|
//return <WiDayCloudy size={size} />;
|
||||||
|
default:
|
||||||
|
return <LightMode />
|
||||||
|
//return <WiDaySunny size={size} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue