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>; disableDrawer?: boolean; startingView?: ViewData; } export default function ConstructionMapController(props: Props) { const transDuration = 3000; const [{ user, as }] = useGlobalState(); const [currentView, setCurrentView] = useState( 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>(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>( new Map() ); const groupAPI = useGroupAPI(); const [groups, setGroups] = useState>(new Map()); const [openMarkerDialog, setOpenMarkerDialog] = useState(false); const [selectKey, setSelectKey] = useState(""); const [endTime, setEndTime] = useState(0); const [devOps, setDevOps] = useState([]); const [groupOps, setGroupOps] = useState([]); const [devices, setDevices] = useState>(new Map()); const [openDevDrawer, setOpenDevDrawer] = useState(false); const [openGroupDrawer, setOpenGroupDrawer] = useState(false); const [markerType, setMarkerType] = useState(0); const [devMarkerData, setDevMarkerData] = useState>(new Map()); const [siteMarkerData, setSiteMarkerData] = useState>(new Map()); const [groupMarkerData, setGroupMarkerData] = useState>(new Map()); const [objectKey, setObjectKey] = useState(""); const isMobile = useMobile(); const [markers, setMarkers] = useState([]); const [displayMarkerTitle, setDisplayMarkerTitle] = useState(false); const zoomLevels = { far: 14, close: 18 }; const [customGeoSearchEntries, setCustomGeoSearchEntries] = useState([]); const [siteSearchEntries, setSiteSearchEntries] = useState([]); const [deviceSearchEntries, setDeviceSearchEntries] = useState([]); const [groupSearchEntries, setGroupSearchEntries] = useState([]); //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 = new Map(); let sm: Map = new Map(); let hMap: Map = new Map(); 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: , 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 = new Map(); // 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: , 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 = new Map(); let groupMarkers: Map = 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: , 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 ( setSiteName(e.target.value)}> ); }; //dialog the renders the forms for creating a new site const dialogForSite = () => { return ( { setSiteDialog(false); }}> Enter Name of Site {siteDetailsForm()} ); }; const markerDialog = () => { return ( { setOpenMarkerDialog(false); }}> Place Marker {markerType === pond.ObjectType.OBJECT_TYPE_DEVICE && ( option.name()} fullWidth onChange={(e, val) => { val && setSelectKey(val.id().toString()); }} renderInput={params => } /> )} {markerType === pond.ObjectType.OBJECT_TYPE_GROUP && ( option.name()} fullWidth onChange={(e, val) => { val && setSelectKey(val.id().toString()); }} renderInput={params => } /> )} ); }; 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: , 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: , 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 ( { //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={ } 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 !== "" && ( { 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); }} /> { 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(); }} /> { 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} /> )} ); }