frontend/src/maps/mapObjectControllers/ConstructionMapController.tsx
2025-10-21 10:44:46 -06:00

759 lines
27 KiB
TypeScript

import {
TextField,
DialogContent,
DialogActions,
Button,
Box,
DialogTitle,
Autocomplete
} from "@mui/material";
import React, { useCallback, useState } from "react";
import "mapbox-gl/dist/mapbox-gl.css";
import { pond } from "protobuf-ts/pond";
import { useDeviceAPI, useGroupAPI, useMobile, useSnackbar } from "hooks";
import { useEffect } from "react";
import { useGlobalState, useSiteAPI } from "providers";
import ResponsiveDialog from "common/ResponsiveDialog";
import { Device, Group, Site } from "models";
// import GroupSettingsIcon from "@material-ui/icons/Settings";
import MapBase, { ViewData } from "maps/MapBase";
import ConstructionMapTools from "maps/mapObjectTools/constructionMapTools";
import { MarkerData } from "maps/mapMarkers/Markers";
import { clone } from "lodash";
import NexusSTIcon from "products/Construction/NexusSTIcon";
import DeviceDrawer from "maps/mapDrawers/DeviceDrawer";
//import { Autocomplete } from "@material-ui/lab";
import JobsiteIcon from "products/Construction/JobSiteIcon";
import SiteDrawer from "maps/mapDrawers/SiteDrawer";
import DeviceGroupIcon from "products/Construction/DeviceGroupIcon";
import GroupDrawer from "maps/mapDrawers/GroupDrawer";
import { Result } from "@mapbox/mapbox-gl-geocoder";
//import { GeocoderObject } from "maps/mapControllers/Geocoder";
interface Props {
setSiteCallback?: React.Dispatch<React.SetStateAction<pond.SiteSettings | undefined>>;
disableDrawer?: boolean;
startingView?: ViewData;
}
export default function ConstructionMapController(props: Props) {
const transDuration = 3000;
const [{ user, as }] = useGlobalState();
const [currentView, setCurrentView] = useState<ViewData>(
props.startingView
? props.startingView
: {
latitude: 52.1579,
longitude: -106.6702,
zoom: user.settings.mapZoom
}
);
const [newLong, setNewLong] = useState(0);
const [newLat, setNewLat] = useState(0);
const [siteName, setSiteName] = useState("");
const siteAPI = useSiteAPI();
const [openSiteDrawer, setOpenSiteDrawer] = useState(false);
const [SiteDialog, setSiteDialog] = useState(false);
const [sites, setSites] = useState<Map<string, Site>>(new Map());
const { openSnack } = useSnackbar();
const deviceAPI = useDeviceAPI();
const [loadingSites, setLoadingSites] = useState(false);
const [loadingDevices, setLoadingDevices] = useState(false);
//map that is the site id as the key with and array of heater data objects (heaters for that site)
const [heaterMap, setHeaterMap] = useState<Map<string, pond.ObjectHeaterData[]>>(
new Map<string, pond.ObjectHeaterData[]>()
);
const groupAPI = useGroupAPI();
const [groups, setGroups] = useState<Map<string, Group>>(new Map());
const [openMarkerDialog, setOpenMarkerDialog] = useState(false);
const [selectKey, setSelectKey] = useState("");
const [endTime, setEndTime] = useState(0);
const [devOps, setDevOps] = useState<Device[]>([]);
const [groupOps, setGroupOps] = useState<Group[]>([]);
const [devices, setDevices] = useState<Map<string, Device>>(new Map<string, Device>());
const [openDevDrawer, setOpenDevDrawer] = useState(false);
const [openGroupDrawer, setOpenGroupDrawer] = useState(false);
const [markerType, setMarkerType] = useState<pond.ObjectType>(0);
const [devMarkerData, setDevMarkerData] = useState<Map<string, MarkerData>>(new Map());
const [siteMarkerData, setSiteMarkerData] = useState<Map<string, MarkerData>>(new Map());
const [groupMarkerData, setGroupMarkerData] = useState<Map<string, MarkerData>>(new Map());
const [objectKey, setObjectKey] = useState<string>("");
const isMobile = useMobile();
const [markers, setMarkers] = useState<MarkerData[]>([]);
const [displayMarkerTitle, setDisplayMarkerTitle] = useState(false);
const zoomLevels = {
far: 14,
close: 18
};
const [customGeoSearchEntries, setCustomGeoSearchEntries] = useState<Result[]>([]);
const [siteSearchEntries, setSiteSearchEntries] = useState<Result[]>([]);
const [deviceSearchEntries, setDeviceSearchEntries] = useState<Result[]>([]);
const [groupSearchEntries, setGroupSearchEntries] = useState<Result[]>([]);
//this click delay is implemented to fix an issue with touch screens that caused the drawer to close immediately after opening
const clickDelay = () => {
let endTime = new Date().valueOf() + 1000;
setEndTime(endTime);
};
const closeDrawers = () => {
setOpenDevDrawer(false);
setOpenSiteDrawer(false);
};
//makes sure all of the Fields are filled out on the first form of the dialog otherwise the next button is disabled
const formComplete = () => {
if (siteName.length < 1) {
return false;
} else {
return true;
}
};
//save the new site to the database
const saveSite = () => {
let newSite = Site.create();
newSite.settings.userId = user.id();
newSite.settings.siteName = siteName;
newSite.settings.objectKey = user.id();
newSite.settings.longitude = newLong;
newSite.settings.latitude = newLat;
siteAPI
.addSite(newSite.settings, as)
.then(resp => {
openSnack("Added new Site Location");
loadSites();
})
.catch(err => {
openSnack("Failed to add site");
});
setSiteDialog(false);
};
//load the sites from the database and set the markers for each of them
const loadSites = useCallback(() => {
if (loadingSites) return;
setLoadingSites(true);
let s: Map<string, Site> = new Map();
let sm: Map<string, MarkerData> = new Map();
let hMap: Map<string, pond.ObjectHeaterData[]> = new Map<string, pond.ObjectHeaterData[]>();
let searchEntries: Result[] = [];
siteAPI
.listSitesPageData(50, 0, "asc", "key", undefined, as)
.then(resp => {
if(resp.data.sites){
resp.data.sites.forEach(site => {
if (!site) return;
let newSite: Site = Site.any(site.site);
s.set(newSite.key(), newSite);
//if there is no long/lat on the site give it a default of saskatoon
if (!newSite.longitude() || !newSite.latitude()) {
newSite.settings.latitude = 52.1579;
newSite.settings.longitude = -106.6702;
}
let siteMarker: MarkerData = {
colour: newSite.settings.theme?.color ?? "blue",
longitude: newSite.longitude(),
latitude: newSite.latitude(),
title: newSite.siteName(),
markerIcon: <JobsiteIcon type="light" size={50 * 0.6} />,
details: [newSite.siteName()],
clickFunc: (e, i, isMobile) => {
closeDrawers();
setObjectKey(newSite.key());
setOpenSiteDrawer(true);
moveMap(newSite.latitude(), newSite.longitude(), zoomLevels.far, isMobile);
},
updateFunc: location => {
if (newSite.settings) {
updateSiteMarkers(newSite, location.longitude, location.latitude);
}
}
};
sm.set(newSite.key(), siteMarker);
hMap.set(newSite.key(), site.heaterData);
searchEntries.push({
id: newSite.key(),
center: [newSite.longitude(), newSite.latitude()],
place_name: newSite.siteName(),
place_type: ["site"]
} as Result);
});
}
setSiteMarkerData(sm);
setSites(s);
setLoadingSites(false);
setHeaterMap(hMap);
setSiteSearchEntries(searchEntries);
})
.catch(err => {
openSnack("Failed to load sites");
});
}, [siteAPI, openSnack, as]); // eslint-disable-line react-hooks/exhaustive-deps
const loadDevices = useCallback(() => {
if (loadingDevices) return;
setLoadingDevices(true);
deviceAPI
.list(500, 0, "asc", "name", undefined, undefined, undefined, undefined, undefined, true, undefined, undefined,undefined,undefined,undefined,as)
.then(resp => {
let devOps: Device[] = []; //only devices that are not mapped
let deviceMap: Map<string, Device> = new Map<string, Device>(); // all devices
let m = clone(devMarkerData);
let searchEntries: Result[] = [];
resp.data.comprehensiveDevices.forEach(cd => {
if (cd.device?.settings?.deviceId) {
let dev = Device.any(cd.device);
deviceMap.set(dev.id().toString(), dev);
let devLoc = dev.location();
if (
devLoc.longitude &&
devLoc.longitude !== 0 &&
devLoc.latitude &&
devLoc.latitude !== 0
) {
let devMarker: MarkerData = {
colour: dev.settings.theme?.color || "blue",
title: dev.name(),
longitude: devLoc.longitude,
latitude: devLoc.latitude,
markerIcon: <NexusSTIcon size={50 * 0.6} type="light" />,
details: [dev.name()],
clickFunc: (e, i, isMobile) => {
closeDrawers();
clickDelay();
setObjectKey(dev.id().toString());
setOpenDevDrawer(true);
moveMap(devLoc.latitude, devLoc.longitude, zoomLevels.far, isMobile);
},
updateFunc: location => {
if (dev.settings) {
updateDeviceMarkers(dev, location.longitude, location.latitude);
}
}
};
m.set(dev.id().toString(), devMarker);
searchEntries.push({
id: dev.id().toString(),
center: [devLoc.longitude, devLoc.latitude],
place_name: dev.name(),
place_type: ["device"]
} as Result);
} else {
devOps.push(dev);
}
}
});
setDevices(deviceMap);
setDevOps(devOps);
setLoadingDevices(false);
setDevMarkerData(m);
setDeviceSearchEntries(searchEntries);
})
.catch(err => {
openSnack("Failed to Load Devices");
});
}, [deviceAPI, openSnack, as]); // eslint-disable-line react-hooks/exhaustive-deps
const loadGroups = useCallback(() => {
groupAPI
.listGroups(50, 0)
.then(resp => {
let gMap: Map<string, Group> = new Map();
let groupMarkers: Map<string, MarkerData> = new Map();
let groupOps: Group[] = [];
let searchEntries: Result[] = [];
resp.data.groups.forEach((g: pond.Group) => {
let group = Group.any(g);
gMap.set(group.id().toString(), group);
if (
group.settings.longitude &&
group.settings.longitude !== 0 &&
group.settings.latitude &&
group.settings.latitude !== 0
) {
let marker: MarkerData = {
colour: "blue",
longitude: group.settings.longitude,
latitude: group.settings.latitude,
title: group.name(),
markerIcon: <DeviceGroupIcon type="light" size={50 * 0.6} />,
details: [group.name()],
clickFunc: (e, i, isMobile) => {
closeDrawers();
clickDelay();
setObjectKey(group.id().toString());
setOpenGroupDrawer(true);
moveMap(
group.settings.latitude,
group.settings.longitude,
zoomLevels.far,
isMobile
);
},
updateFunc: location => {
if (group) {
if (group.settings) {
let dataClone = clone(groupMarkerData);
updateGroupMarkers(group, location.longitude, location.latitude);
marker.longitude = location.longitude;
marker.latitude = location.latitude;
dataClone.set(group.id().toString(), marker);
setGroupMarkerData(dataClone);
}
}
}
};
groupMarkers.set(group.id().toString(), marker);
searchEntries.push({
id: group.id().toString(),
center: [group.settings.longitude, group.settings.latitude],
place_name: group.name(),
place_type: ["group"]
} as Result);
} else {
groupOps.push(group);
}
});
setGroups(gMap);
setGroupMarkerData(groupMarkers);
setGroupOps(groupOps);
setGroupSearchEntries(searchEntries);
})
.catch(err => {
//openSnack("Failed to load Groups"); //backend function errors when there are no groups
});
}, [groupAPI]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
loadSites();
loadDevices();
loadGroups();
}, [loadSites, loadDevices, loadGroups, as]);
//assemble the different objects markerdata into a single array to pass to the map
useEffect(() => {
let dArray = Array.from(devMarkerData.values());
let sArray = Array.from(siteMarkerData.values());
let gArray = Array.from(groupMarkerData.values());
setMarkers(dArray.concat(sArray, gArray));
}, [devMarkerData, siteMarkerData, groupMarkerData]);
//assemble the different object search entries into one array to pass to the map
useEffect(() => {
setCustomGeoSearchEntries(siteSearchEntries.concat(deviceSearchEntries, groupSearchEntries));
}, [deviceSearchEntries, siteSearchEntries, groupSearchEntries]);
//the entry fields for the first part of the stepper
const siteDetailsForm = () => {
return (
<Box>
<TextField
value={siteName}
color="primary"
autoFocus
margin="dense"
id="name"
label="Site Name"
type="text"
fullWidth
onChange={e => setSiteName(e.target.value)}></TextField>
</Box>
);
};
//dialog the renders the forms for creating a new site
const dialogForSite = () => {
return (
<ResponsiveDialog
fullWidth
fullScreen={false}
open={SiteDialog}
onClose={() => {
setSiteDialog(false);
}}>
<DialogTitle>Enter Name of Site</DialogTitle>
<DialogContent>{siteDetailsForm()}</DialogContent>
<DialogActions>
<Button
onClick={() => {
setSiteDialog(false);
}}
color="primary">
Cancel
</Button>
<Button onClick={saveSite} color="primary" disabled={!formComplete()}>
Add Site
</Button>
</DialogActions>
</ResponsiveDialog>
);
};
const markerDialog = () => {
return (
<ResponsiveDialog
open={openMarkerDialog}
onClose={() => {
setOpenMarkerDialog(false);
}}>
<DialogTitle>Place Marker</DialogTitle>
<DialogContent>
<Box width={300}>
{markerType === pond.ObjectType.OBJECT_TYPE_DEVICE && (
<Autocomplete
id="autoYardList"
options={devOps}
getOptionLabel={(option: Device) => option.name()}
fullWidth
onChange={(e, val) => {
val && setSelectKey(val.id().toString());
}}
renderInput={params => <TextField {...params} label="Device" />}
/>
)}
{markerType === pond.ObjectType.OBJECT_TYPE_GROUP && (
<Autocomplete
id="autoYardList"
options={groupOps}
getOptionLabel={(option: Group) => option.name()}
fullWidth
onChange={(e, val) => {
val && setSelectKey(val.id().toString());
}}
renderInput={params => <TextField {...params} label="Group" />}
/>
)}
</Box>
</DialogContent>
<DialogActions>
<Box>
<Button onClick={closeNewMarkerDialog}>Cancel</Button>
<Button onClick={setMarker}>Plot New Marker</Button>
</Box>
</DialogActions>
</ResponsiveDialog>
);
};
const setMarker = () => {
if (markerType === pond.ObjectType.OBJECT_TYPE_DEVICE) {
let dev = devices.get(selectKey);
if (dev) {
let key = dev.id().toString();
let location = dev.location();
let newDevMarker: MarkerData = {
colour: dev.settings.theme?.color || "blue",
title: dev.name(),
longitude: newLong,
latitude: newLat,
markerIcon: <NexusSTIcon size={50 * 0.6} type="light" />,
details: [dev.name()],
clickFunc: (e, i, isMobile) => {
closeDrawers();
clickDelay();
setObjectKey(key);
setOpenDevDrawer(true);
moveMap(location.latitude, location.longitude, zoomLevels.far, isMobile);
},
updateFunc: location => {
if (dev) {
if (dev.settings) {
let dataClone = clone(devMarkerData);
updateDeviceMarkers(dev, location.longitude, location.latitude);
newDevMarker.longitude = location.longitude;
newDevMarker.latitude = location.latitude;
dataClone.set(key, newDevMarker);
setDevMarkerData(dataClone);
}
}
}
};
let devMarkers = clone(devMarkerData);
devMarkers.set(key, newDevMarker);
setDevMarkerData(devMarkers);
updateDeviceMarkers(dev, newLong, newLat);
}
}
if (markerType === pond.ObjectType.OBJECT_TYPE_GROUP) {
let group = groups.get(selectKey);
if (group) {
let key = group.id().toString();
let long = group.settings.longitude;
let lat = group.settings.latitude;
let newMarker: MarkerData = {
colour: "blue",
title: group.name(),
longitude: newLong,
latitude: newLat,
markerIcon: <DeviceGroupIcon type="light" size={50 * 0.6} />,
details: [group.name()],
clickFunc: (e, i, isMobile) => {
closeDrawers();
clickDelay();
setObjectKey(key);
setOpenGroupDrawer(true);
moveMap(lat, long, zoomLevels.far, isMobile);
},
updateFunc: location => {
if (group) {
if (group.settings) {
let dataClone = clone(groupMarkerData);
updateGroupMarkers(group, location.longitude, location.latitude);
newMarker.longitude = location.longitude;
newMarker.latitude = location.latitude;
dataClone.set(key, newMarker);
setGroupMarkerData(dataClone);
}
}
}
};
let groupMarkers = clone(groupMarkerData);
groupMarkers.set(key, newMarker);
setGroupMarkerData(groupMarkers);
updateGroupMarkers(group, newLong, newLat);
}
}
closeNewMarkerDialog();
};
const closeNewMarkerDialog = () => {
if (new Date().valueOf() > endTime) {
setOpenMarkerDialog(false);
setSelectKey("");
}
};
const updateDeviceMarkers = (device: Device, long: number, lat: number) => {
device.settings.longitude = long;
device.settings.latitude = lat;
deviceAPI
.update(device.id(), device.settings, as)
.then(resp => {
openSnack("Device location Updated");
if (devOps.includes(device)) {
devOps.splice(devOps.indexOf(device), 1);
}
})
.catch(err => {
openSnack("Could not update Device Location");
});
};
const updateGroupMarkers = (group: Group, long: number, lat: number) => {
group.settings.longitude = long;
group.settings.latitude = lat;
groupAPI
.updateGroup(group.id(), group.settings)
.then(resp => {
openSnack("Group location Updated");
if (groupOps.includes(group)) {
groupOps.splice(groupOps.indexOf(group), 1);
}
})
.catch(err => {
openSnack("Could not update Group Location");
});
};
const updateSiteMarkers = (site: Site, long: number, lat: number) => {
site.settings.longitude = long;
site.settings.latitude = lat;
siteAPI
.updateSite(site.key(), site.settings, undefined, as)
.then(resp => {
openSnack("Site location updated");
})
.catch(err => {
openSnack("Could not update site location");
});
};
const moveMap = (
lat: number,
long: number,
zoom: number,
mobileOffset?: boolean,
center?: boolean
) => {
let xOff = !mobileOffset ? -250 : undefined;
let yOff = mobileOffset ? -150 : undefined;
setCurrentView({
latitude: lat,
longitude: long,
zoom: zoom,
transitionDuration: transDuration,
xOffset: center ? 0 : xOff,
yOffset: center ? 0 : yOff
});
};
return (
<Box style={{ height: "100%" }}>
<MapBase
placingMarker={markerType !== pond.ObjectType.OBJECT_TYPE_UNKNOWN}
displayMarkerTitles={displayMarkerTitle}
customSearchEntries={customGeoSearchEntries}
geocoderResultFunction={result => {
//open drawer
setObjectKey(result.id?.toString() ?? "");
if (result.place_type.includes("site")) {
setOpenSiteDrawer(true);
} else if (result.place_type.includes("device")) {
setOpenDevDrawer(true);
} else if (result.place_type.includes("group")) {
setOpenGroupDrawer(true);
}
}}
geocoderTransitionFunction={result => {
//override the geocoders transition with our own
let long = result.center[0];
let lat = result.center[1];
if (long && lat) {
let centered = true;
let zoom = zoomLevels.far;
if (
result.place_type.includes("site") ||
result.place_type.includes("device") ||
result.place_type.includes("group")
) {
centered = false;
}
moveMap(lat, long, zoom, isMobile, centered);
}
}}
mapTools={
<ConstructionMapTools
toggleMarkerType={setMarkerType}
toggleMarkerDisplay={setDisplayMarkerTitle}
/>
}
mapClick={e => {
if (markerType !== pond.ObjectType.OBJECT_TYPE_UNKNOWN) {
setNewLong(e.lngLat.lng);
setNewLat(e.lngLat.lat);
if (markerType === pond.ObjectType.OBJECT_TYPE_SITE) {
setSiteDialog(true);
} else {
setOpenMarkerDialog(true);
}
}
}}
currentView={currentView}
markerData={markers}
/>
{dialogForSite()}
{markerDialog()}
{objectKey !== "" && (
<React.Fragment>
<DeviceDrawer
open={openDevDrawer}
onClose={() => {
if (new Date().valueOf() > endTime) {
setOpenDevDrawer(false);
setObjectKey("");
}
}}
selectedDevice={objectKey}
devices={devices}
removeMarker={key => {
if (new Date().valueOf() > endTime) {
//use the key to remove it from the map of yard markers which should cause the markerData array to update
let newMarkers = clone(devMarkerData);
newMarkers.delete(key);
setDevMarkerData(newMarkers);
//adds it back to the options
let dev = devices.get(key);
if (dev) {
devOps.push(dev);
}
}
}}
updateMarker={(key, objectSettings) => {
if (new Date().valueOf() > endTime) {
let newMarkers = clone(devMarkerData);
let toupdate = newMarkers.get(key);
if (toupdate) {
if (objectSettings.theme) {
toupdate.colour = objectSettings.theme.color;
}
}
setDevMarkerData(newMarkers);
}
}}
moveMap={(long, lat) => {
moveMap(lat, long, zoomLevels.far, isMobile);
}}
/>
<SiteDrawer
open={openSiteDrawer}
sites={sites}
heaters={heaterMap}
selectedSite={objectKey}
onClose={() => {
if (new Date().valueOf() > endTime) {
setOpenSiteDrawer(false);
setObjectKey("");
}
}}
moveMap={(long, lat) => {
moveMap(lat, long, zoomLevels.far, isMobile);
}}
removeMarker={key => {
if (new Date().valueOf() > endTime) {
//use the key to remove it from the map of yard markers which should cause the markerData array to update
let newMarkers = clone(siteMarkerData);
newMarkers.delete(key);
setSiteMarkerData(newMarkers);
}
}}
updateMarker={(key, objectSettings) => {
if (new Date().valueOf() > endTime) {
let newMarkers = clone(siteMarkerData);
let toupdate = newMarkers.get(key);
if (toupdate) {
if (objectSettings.theme) {
toupdate.colour = objectSettings.theme.color;
}
}
setSiteMarkerData(newMarkers);
}
}}
reLoadSites={() => {
loadSites();
}}
/>
<GroupDrawer
open={openGroupDrawer}
groups={groups}
moveMap={(long, lat) => {
moveMap(lat, long, zoomLevels.far, isMobile);
}}
onClose={() => {
if (new Date().valueOf() > endTime) {
setOpenGroupDrawer(false);
setObjectKey("");
}
}}
removeMarker={key => {
if (new Date().valueOf() > endTime) {
//use the key to remove it from the map of yard markers which should cause the markerData array to update
let newMarkers = clone(groupMarkerData);
newMarkers.delete(key);
setGroupMarkerData(newMarkers);
//adds it back to the options
let group = groups.get(key);
if (group) {
groupOps.push(group);
}
}
}}
selectedGroup={objectKey}
/>
</React.Fragment>
)}
</Box>
);
}