From bcacf761c80fbab7e466e22c8df0e137bbf30091 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Mon, 31 Mar 2025 16:24:44 -0600 Subject: [PATCH] added the construction and aviation map stuff as well as the site api and model --- src/device/DeviceViewer.tsx | 2 +- src/maps/mapDrawers/GateDrawer.tsx | 119 +++ src/maps/mapDrawers/GroupDrawer.tsx | 144 ++++ src/maps/mapDrawers/SiteDrawer.tsx | 221 +++++ src/maps/mapDrawers/TerminalDrawer.tsx | 119 +++ .../AviationMapController.tsx | 468 +++++++++++ .../ConstructionMapController.tsx | 757 ++++++++++++++++++ src/maps/mapObjectTools/aviationMapTools.tsx | 136 ++++ .../mapObjectTools/constructionMapTools.tsx | 238 ++++++ src/models/Site.ts | 80 ++ src/models/index.ts | 2 +- src/navigation/Router.tsx | 7 + src/navigation/SideNavigator.tsx | 15 + src/pages/AviationMap.tsx | 26 + src/pages/ConstructionSiteMap.tsx | 27 + src/providers/index.ts | 2 +- src/providers/pond/pond.tsx | 8 +- src/providers/pond/siteAPI.tsx | 179 +++++ src/weather/weather.tsx | 6 +- src/weather/weatherIcon.tsx | 2 +- 20 files changed, 2549 insertions(+), 9 deletions(-) create mode 100644 src/maps/mapDrawers/GateDrawer.tsx create mode 100644 src/maps/mapDrawers/GroupDrawer.tsx create mode 100644 src/maps/mapDrawers/SiteDrawer.tsx create mode 100644 src/maps/mapDrawers/TerminalDrawer.tsx create mode 100644 src/maps/mapObjectControllers/AviationMapController.tsx create mode 100644 src/maps/mapObjectControllers/ConstructionMapController.tsx create mode 100644 src/maps/mapObjectTools/aviationMapTools.tsx create mode 100644 src/maps/mapObjectTools/constructionMapTools.tsx create mode 100644 src/models/Site.ts create mode 100644 src/pages/AviationMap.tsx create mode 100644 src/pages/ConstructionSiteMap.tsx create mode 100644 src/providers/pond/siteAPI.tsx diff --git a/src/device/DeviceViewer.tsx b/src/device/DeviceViewer.tsx index 2c93d87..830ab02 100644 --- a/src/device/DeviceViewer.tsx +++ b/src/device/DeviceViewer.tsx @@ -172,7 +172,7 @@ export default function Device(props: Props) { }, [load]); const getOrderedComponents = () => { - if (preferences.childDisplayOrder.length === Array.from(components.values()).length) { + if (preferences.childDisplayOrder && preferences.childDisplayOrder.length === Array.from(components.values()).length) { return Array.from(components.values()).sort((a, b: Component) => sortComponents(a, b, preferences.childDisplayOrder) ); diff --git a/src/maps/mapDrawers/GateDrawer.tsx b/src/maps/mapDrawers/GateDrawer.tsx new file mode 100644 index 0000000..c516e8f --- /dev/null +++ b/src/maps/mapDrawers/GateDrawer.tsx @@ -0,0 +1,119 @@ +import DisplayDrawer from "common/DisplayDrawer"; +import { Gate as IGate } from "models/Gate"; +import Gate from "pages/Gate"; +import { useGateAPI, useSnackbar } from "providers"; +import React, { useEffect, useState } from "react"; + +interface Props { + open: boolean; + onClose: () => void; + gates: Map; + selectedGate: string; + moveMap: (longitude: number, latitude: number) => void; + removeMarker: (key: string) => void; +} + +export default function GateDrawer(props: Props) { + const { open, onClose, gates, selectedGate, moveMap, removeMarker } = props; + const [gate, setGate] = useState(IGate.create()); + const gateAPI = useGateAPI(); + const { openSnack } = useSnackbar(); + + useEffect(() => { + let g = gates.get(selectedGate); + if (g) { + setGate(g); + } + }, [selectedGate, gates]); + + const closeDrawer = () => { + onClose(); + }; + + const displayNext = () => { + let gateArr = Array.from(gates.values()); + let index = gateArr.indexOf(gate); + let found = false; + do { + if (index === gateArr.length - 1) { + index = 0; + } else { + index++; + } + let next = gateArr[index]; + if ( + next.longitude() !== 0 && + next.longitude() !== 0 && + next.latitude() !== undefined && + next.longitude() !== undefined + ) { + setGate(next); + moveMap(next.longitude(), next.latitude()); + found = true; + } + } while (!found); + }; + + const displayPrev = () => { + let gateArr = Array.from(gates.values()); + let index = gateArr.indexOf(gate); + let found = false; + do { + if (index === 0) { + index = gateArr.length - 1; + } else { + index--; + } + let next = gateArr[index]; + if ( + next.longitude() !== 0 && + next.latitude() !== 0 && + next.latitude() !== undefined && + next.longitude() !== undefined + ) { + setGate(next); + moveMap(next.longitude(), next.latitude()); + found = true; + } + } while (!found); + }; + + const drawerBody = () => { + return ; + }; + + /** + * 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 = gate.settings; + settings.longitude = 0; + settings.latitude = 0; + gateAPI + .updateGate(gate.key, gate.name, settings) + .then(resp => { + openSnack("Marker Removed"); + //then use the removeMarker prop function to update the markers in the parent map + removeMarker(gate.key); + }) + .catch(err => { + openSnack("Failed to remove marker"); + }); + }; + + return ( + + + + ); +} diff --git a/src/maps/mapDrawers/GroupDrawer.tsx b/src/maps/mapDrawers/GroupDrawer.tsx new file mode 100644 index 0000000..5a66125 --- /dev/null +++ b/src/maps/mapDrawers/GroupDrawer.tsx @@ -0,0 +1,144 @@ +import { Box } from "@mui/material"; +import DisplayDrawer from "common/DisplayDrawer"; +//import DeviceCardList from "device/DeviceCardList"; +import { useGroupAPI, useSnackbar } from "hooks"; +import { Group } from "models"; +import { pond } from "protobuf-ts/pond"; +import React, { useEffect, useState, useCallback } from "react"; +import { or } from "utils"; + +interface Props { + open: boolean; + groups: Map; + selectedGroup: string; + onClose: () => void; + removeMarker: (key: string) => void; + moveMap: (long: number, lat: number) => void; +} + +export default function GroupDrawer(props: Props) { + const { open, groups, selectedGroup, onClose, moveMap, removeMarker } = props; + const [group, setGroup] = useState(Group.create()); + const groupAPI = useGroupAPI(); + const { openSnack } = useSnackbar(); + const [grpDevs, setGrpDevs] = useState([]); + + const loadDevs = useCallback(() => { + groupAPI + .listGroupDevices(group.id(), 100, 0, "asc", undefined, true) + .then(resp => { + const groupDevices: pond.ComprehensiveDevice[] = or( + resp.data.comprehensiveDevices, + [] + ).map(d => pond.ComprehensiveDevice.fromObject(d)); + setGrpDevs(groupDevices); + }) + .catch(err => { + openSnack("Failed to get devices for group"); + }); + }, [group, groupAPI, openSnack]); + + useEffect(() => { + loadDevs(); + }, [loadDevs]); + + useEffect(() => { + let g = groups.get(selectedGroup); + if (g) { + setGroup(g); + } + }, [selectedGroup, groups]); + + const closeDrawer = () => { + onClose(); + }; + + const displayNext = () => { + let groupArr = Array.from(groups.values()); + let index = groupArr.indexOf(group); + let found = false; + do { + if (index === groupArr.length - 1) { + index = 0; + } else { + index++; + } + let next = groupArr[index]; + if ( + next.settings.longitude !== 0 && + next.settings.longitude !== 0 && + next.settings.latitude !== undefined && + next.settings.longitude !== undefined + ) { + setGroup(next); + moveMap(next.settings.longitude, next.settings.latitude); + found = true; + } + } while (!found); + }; + + const displayPrev = () => { + let groupArr = Array.from(groups.values()); + let index = groupArr.indexOf(group); + let found = false; + do { + if (index === 0) { + index = groupArr.length - 1; + } else { + index--; + } + let next = groupArr[index]; + if ( + next.settings.longitude !== 0 && + next.settings.longitude !== 0 && + next.settings.latitude !== undefined && + next.settings.longitude !== undefined + ) { + setGroup(next); + moveMap(next.settings.longitude, next.settings.latitude); + found = true; + } + } while (!found); + }; + + /** + * 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 = group.settings; + settings.longitude = 0; + settings.latitude = 0; + groupAPI + .updateGroup(group.id(), settings) + .then(resp => { + openSnack("Marker Removed"); + //then use the removeMarker prop function to update the markers in the parent map + removeMarker(group.id().toString()); + }) + .catch(err => { + openSnack("Failed to remove marker"); + }); + }; + + const drawerBody = () => { + return (Use the responsive table here) + //return ; + }; + + return ( + + + + ); +} diff --git a/src/maps/mapDrawers/SiteDrawer.tsx b/src/maps/mapDrawers/SiteDrawer.tsx new file mode 100644 index 0000000..f4e6649 --- /dev/null +++ b/src/maps/mapDrawers/SiteDrawer.tsx @@ -0,0 +1,221 @@ +import { Box, Typography } from "@mui/material"; +import DeviceLinkDrawer from "common/DeviceLinkDrawer"; +import DisplayDrawer from "common/DisplayDrawer"; +import MapMarkerSettings from "maps/MapMarkerSettings"; +import { Device, Site } from "models"; +//import { ObjectHeater } from "models/ObjectHeater"; +//import ObjectHeaterCard from "objectHeater/ObjectHeaterCard"; +import { pond } from "protobuf-ts/pond"; +import { /*useObjectHeaterAPI,*/ useSiteAPI, useSnackbar } from "providers"; +import React, { useEffect, useState } from "react"; + +interface Props { + open: boolean; + onClose: () => void; + selectedSite: string; + sites: Map; + heaters: Map; + removeMarker: (key: string) => void; + updateMarker: (key: string, newSettings: pond.SiteSettings) => void; + moveMap: (long: number, lat: number) => void; + reLoadSites: () => void; +} + +export default function SiteDrawer(props: Props) { + const { + open, + onClose, + selectedSite, + sites, + removeMarker, + moveMap, + heaters, + reLoadSites, + updateMarker + } = props; + const [site, setSite] = useState(Site.create()); + const siteAPI = useSiteAPI(); + //const objectHeaterAPI = useObjectHeaterAPI(); + const { openSnack } = useSnackbar(); + const [linkDrawer, setLinkDrawer] = useState(false); + const [linkedDevices, setLinkedDevices] = useState>( + new Map() + ); + //const [selectedHeater, setSelectedHeater] = useState(); + const [openMarkerSettings, setOpenMarkerSettings] = useState(false); + + useEffect(() => { + let s = sites.get(selectedSite); + if (s) { + setSite(s); + } + }, [selectedSite, sites]); + + const closeDrawer = () => { + onClose(); + }; + + const displayNext = () => { + let devArr = Array.from(sites.values()); + let index = devArr.indexOf(site); + let found = false; + do { + if (index === devArr.length - 1) { + index = 0; + } else { + index++; + } + let nextSite = devArr[index]; + if ( + nextSite.location().longitude !== 0 && + nextSite.location().longitude !== 0 && + nextSite.location().latitude !== undefined && + nextSite.location().longitude !== undefined + ) { + setSite(nextSite); + moveMap(nextSite.location().longitude, nextSite.location().latitude); + found = true; + } + } while (!found); + }; + + const displayPrev = () => { + let devArr = Array.from(sites.values()); + let index = devArr.indexOf(site); + let found = false; + do { + if (index === 0) { + index = devArr.length - 1; + } else { + index--; + } + let nextSite = devArr[index]; + if ( + nextSite.location().longitude !== 0 && + nextSite.location().longitude !== 0 && + nextSite.location().latitude !== undefined && + nextSite.location().longitude !== undefined + ) { + setSite(nextSite); + moveMap(nextSite.location().longitude, nextSite.location().latitude); + found = true; + } + } while (!found); + }; + + // const deviceLinkDrawer = () => { + // return ( + // { + // setLinkDrawer(false); + // }} + // linkedDevices={linkedDevices} + // updateLinkedDevices={(dev, linked) => { + // let deviceID = dev.device?.settings?.deviceId; + // if (deviceID) { + // if (linked && selectedHeater) { + // objectHeaterAPI + // .updateLink(selectedHeater.key, "objectHeater", deviceID.toString(), "device", [ + // "read", + // "write", + // "grant", + // "revoke" + // ]) + // .then(resp => { + // setLinkDrawer(false); + // reLoadSites(); + // }); + // } + // } + // }} + // /> + // ); + // }; + + const drawerBody = () => { + //let siteHeaters = heaters.get(site.key()); + return ( + + {/* {siteHeaters ? ( + siteHeaters.map(h => ( + { + setSelectedHeater(heater); + setLinkDrawer(true); + let d = new Map(); + linkedDevices.forEach(compDev => { + let device = Device.any(compDev.device); + d.set(device.id().toString(), compDev); + }); + setLinkedDevices(d); + }} + /> + )) + ) : ( */} + No Heaters Found + {/* )} */} + + ); + }; + + /** + * function to remove the marker and coordinates from the object + */ + const remove = () => { + siteAPI + .removeSite(site.key()) + .then(resp => { + openSnack("Site Deleted"); + //then use the removeMarker prop function to update the markers in the parent map + removeMarker(site.key()); + }) + .catch(err => { + openSnack("Failed to remove marker"); + }); + }; + + const update = () => { + setOpenMarkerSettings(true); + }; + + return ( + + {/* {deviceLinkDrawer()} */} + + { + setOpenMarkerSettings(false); + }} + open={openMarkerSettings} + colourControl + theme={site.settings.theme ?? pond.ObjectTheme.create()} + updateObject={newTheme => { + let settings = site.settings; + settings.theme = newTheme; + siteAPI + .updateSite(site.key(), settings) + .then(resp => { + openSnack("marker settings updated"); + updateMarker(site.key(), settings); + }) + .catch(() => { + openSnack("failed to update marker settings"); + }); + }} + /> + + ); +} diff --git a/src/maps/mapDrawers/TerminalDrawer.tsx b/src/maps/mapDrawers/TerminalDrawer.tsx new file mode 100644 index 0000000..4c1387d --- /dev/null +++ b/src/maps/mapDrawers/TerminalDrawer.tsx @@ -0,0 +1,119 @@ +import DisplayDrawer from "common/DisplayDrawer"; +import { Terminal as ITerminal } from "models/Terminal"; +import Terminal from "pages/Terminals"; +import { useSnackbar, useTerminalAPI } from "providers"; +import React, { useState, useEffect } from "react"; + +interface Props { + open: boolean; + onClose: () => void; + terminals: Map; + selectedKey: string; + moveMap: (longitude: number, latitude: number) => void; + removeMarker: (key: string) => void; +} + +export default function TerminalDrawer(props: Props) { + const { terminals, selectedKey, open, onClose, moveMap, removeMarker } = props; + const [terminal, setTerminal] = useState(ITerminal.create()); + const terminalAPI = useTerminalAPI(); + const { openSnack } = useSnackbar(); + + useEffect(() => { + let t = terminals.get(selectedKey); + if (t) { + setTerminal(t); + } + }, [selectedKey, terminals]); + + const closeDrawer = () => { + onClose(); + }; + + const displayNext = () => { + let termArr = Array.from(terminals.values()); + let index = termArr.indexOf(terminal); + let found = false; + do { + if (index === termArr.length - 1) { + index = 0; + } else { + index++; + } + let next = termArr[index]; + if ( + next.longitude() !== 0 && + next.longitude() !== 0 && + next.latitude() !== undefined && + next.longitude() !== undefined + ) { + setTerminal(next); + moveMap(next.longitude(), next.latitude()); + found = true; + } + } while (!found); + }; + + const displayPrev = () => { + let termArr = Array.from(terminals.values()); + let index = termArr.indexOf(terminal); + let found = false; + do { + if (index === 0) { + index = termArr.length - 1; + } else { + index--; + } + let next = termArr[index]; + if ( + next.longitude() !== 0 && + next.latitude() !== 0 && + next.latitude() !== undefined && + next.longitude() !== undefined + ) { + setTerminal(next); + moveMap(next.longitude(), next.latitude()); + found = true; + } + } while (!found); + }; + + const drawerBody = () => { + return ; + }; + + /** + * 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 = terminal.settings; + settings.longitude = 0; + settings.latitude = 0; + terminalAPI + .updateTerminal(terminal.key, terminal.name, settings) + .then(resp => { + openSnack("Marker Removed"); + //then use the removeMarker prop function to update the markers in the parent map + removeMarker(terminal.key); + }) + .catch(err => { + openSnack("Failed to remove marker"); + }); + }; + + return ( + + + + ); +} diff --git a/src/maps/mapObjectControllers/AviationMapController.tsx b/src/maps/mapObjectControllers/AviationMapController.tsx new file mode 100644 index 0000000..9a83fd9 --- /dev/null +++ b/src/maps/mapObjectControllers/AviationMapController.tsx @@ -0,0 +1,468 @@ +import { + Box, + Button, + DialogActions, + DialogContent, + DialogTitle, + TextField, + Autocomplete +} from "@mui/material"; +//import { Autocomplete } from "@material-ui/lab"; +import ResponsiveDialog from "common/ResponsiveDialog"; +import { useMobile } from "hooks"; +import { clone } from "lodash"; +import MapBase, { ViewData } from "maps/MapBase"; +//import { GeocoderObject } from "maps/mapControllers/Geocoder"; +import { Result } from "@mapbox/mapbox-gl-geocoder"; +import GateDrawer from "maps/mapDrawers/GateDrawer"; +import TerminalDrawer from "maps/mapDrawers/TerminalDrawer"; +import { MarkerData } from "maps/mapMarkers/Markers"; +import AviationMapTools from "maps/mapObjectTools/aviationMapTools"; +import { Gate } from "models/Gate"; +import { Terminal } from "models/Terminal"; +import OmniAirDeviceIcon from "products/AviationIcons/OmniAirDeviceIcon"; +import PlaneIcon from "products/AviationIcons/PlaneIcon"; +import { pond } from "protobuf-ts/pond"; +import { useGateAPI, useGlobalState, useSnackbar, useTerminalAPI } from "providers"; +import React, { useCallback, useEffect, useState } from "react"; + +interface Props { + startingView?: ViewData; +} + +export default function AviationMapController(props: Props) { + const [{ user }] = useGlobalState(); + const transDuration = 3000; + const [currentView, setCurrentView] = useState( + props.startingView + ? props.startingView + : { + latitude: 52.1579, + longitude: -106.6702, + zoom: user.settings.mapZoom + } + ); + const terminalAPI = useTerminalAPI(); + const gateAPI = useGateAPI(); + const [terminals, setTerminals] = useState>(new Map()); + const [gates, setGates] = useState>(new Map()); + const [terminalOptions, setTerminalOptions] = useState([]); + const [gateOptions, setGateOptions] = useState([]); + const [terminalMarkers, setTerminalMarkers] = useState>(new Map()); + const [gateMarkers, setGateMarkers] = useState>(new Map()); + const [markers, setMarkers] = useState([]); + const { openSnack } = useSnackbar(); + const [gateDrawerOpen, setGateDrawerOpen] = useState(false); + const [terminalDrawerOpen, setTerminalDrawerOpen] = useState(false); + const [objectKey, setObjectKey] = useState(""); + const [endTime, setEndTime] = useState(0); + const isMobile = useMobile(); + const [markerType, setMarkerType] = useState(pond.ObjectType.OBJECT_TYPE_UNKNOWN); + const [markerDisplay, setMarkerDisplay] = useState(false); + const [openMarkerDialog, setOpenMarkerDialog] = useState(false); + const [selectKey, setSelectKey] = useState(""); + const [newLong, setNewLong] = useState(0); + const [newLat, setNewLat] = useState(0); + const zoomLevels = { + far: 14, + close: 16 + }; + const [customSearchEntries, setCustomSearchEntries] = useState([]); + const [terminalSearchEntries, setTerminalSearchEntries] = useState([]); + const [gateSearchEntries, setGateSearchEntries] = useState([]); + + const closeDrawers = () => { + setTerminalDrawerOpen(false); + setGateDrawerOpen(false); + }; + + const markerClick = (key: string, long: number, lat: number, isMobile: boolean) => { + closeDrawers(); + clickDelay(); + setObjectKey(key); + moveMap(lat, long, zoomLevels.close, isMobile); + }; + + //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 updateTerminalMarkers = ( + terminal: Terminal, + longitude: number, + latitude: number, + newMarker?: boolean + ) => { + let settings = terminal.settings; + settings.longitude = longitude; + settings.latitude = latitude; + terminalAPI + .updateTerminal(terminal.key, terminal.name, settings) + .then(resp => { + openSnack("Terminal Location Updated"); + if (newMarker) { + //create new marker + let termMarker = terminal.getMarkerData( + (e, i, isMobile) => { + markerClick(terminal.key, terminal.longitude(), terminal.latitude(), isMobile); + setTerminalDrawerOpen(true); + }, + location => { + updateTerminalMarkers(terminal, location.longitude, location.latitude); + } + ); + termMarker.markerIcon = ; + //clone data to update the state with and put the marker into the clone + let cloneData = clone(terminalMarkers); + cloneData.set(terminal.key, termMarker); + setTerminalMarkers(cloneData); + //remove from options + if (terminalOptions.includes(terminal)) { + terminalOptions.splice(terminalOptions.indexOf(terminal), 1); + } + } + }) + .catch(err => { + openSnack("Failed to update location"); + }); + }; + + const updateGateMarkers = ( + gate: Gate, + longitude: number, + latitude: number, + newMarker?: boolean + ) => { + let settings = gate.settings; + settings.longitude = longitude; + settings.latitude = latitude; + gateAPI + .updateGate(gate.key, gate.name, settings) + .then(resp => { + openSnack("Gate Location Updated"); + if (newMarker) { + let marker = gate.getMarkerData( + (e, i, isMobile) => { + markerClick(gate.key, gate.longitude(), gate.latitude(), isMobile); + setGateDrawerOpen(true); + }, + location => { + updateGateMarkers(gate, location.longitude, location.latitude); + } + ); + marker.markerIcon = ; + let cloneData = clone(gateMarkers); + cloneData.set(gate.key, marker); + setGateMarkers(cloneData); + //remove from options + if (gateOptions.includes(gate)) { + gateOptions.splice(gateOptions.indexOf(gate), 1); + } + } else { + //change the click function in the markers data to use the new long and lat + } + }) + .catch(err => { + openSnack("Failed to update location"); + }); + }; + + const loadTerminals = useCallback(() => { + terminalAPI.listTerminals(0, 0, undefined, undefined, undefined).then(resp => { + let terminalMap: Map = new Map(); + let terminalMarkers: Map = new Map(); + let terminalOps: Terminal[] = []; + let searchEntries: Result[] = []; + resp.data.terminals.forEach(term => { + let terminal = Terminal.create(term); + terminalMap.set(terminal.key, terminal); + if ( + terminal.longitude() && + terminal.longitude() !== 0 && + terminal.latitude() && + terminal.latitude() !== 0 + ) { + //make marker + let termMarker = terminal.getMarkerData( + (e, i, isMobile) => { + markerClick(terminal.key, terminal.longitude(), terminal.latitude(), isMobile); + setTerminalDrawerOpen(true); + }, + location => { + updateTerminalMarkers(terminal, location.longitude, location.latitude); + } + ); + termMarker.markerIcon = ; + terminalMarkers.set(terminal.key, termMarker); + searchEntries.push({ + id: terminal.key, + center: [terminal.longitude(), terminal.latitude()], + place_name: terminal.name, + place_type: ["terminal"] + } as Result); + } else { + //no location was set so add to the options + terminalOps.push(terminal); + } + }); + setTerminals(terminalMap); + setTerminalMarkers(terminalMarkers); + setTerminalSearchEntries(searchEntries); + setTerminalOptions(terminalOps); + }); + }, [terminalAPI]); // eslint-disable-line react-hooks/exhaustive-deps + + const loadGates = useCallback(() => { + gateAPI.listGates(0, 0, undefined, undefined, undefined, true).then(resp => { + let gates: Map = new Map(); + let gateOptions: Gate[] = []; + let gateMarkers: Map = new Map(); + let searchEntries: Result[] = []; + resp.data.gates.forEach(g => { + let gate = Gate.create(g); + gates.set(gate.key, gate); + if ( + gate.longitude() && + gate.longitude() !== 0 && + gate.latitude() && + gate.latitude() !== 0 + ) { + let marker = gate.getMarkerData( + (e, i, isMobile) => { + markerClick(gate.key, gate.longitude(), gate.latitude(), isMobile); + setGateDrawerOpen(true); + }, + location => { + updateGateMarkers(gate, location.longitude, location.latitude); + } + ); + marker.markerIcon = ; + gateMarkers.set(gate.key, marker); + searchEntries.push({ + id: gate.key, + center: [gate.longitude(), gate.latitude()], + place_name: gate.name, + place_type: ["gate"] + } as Result); + } else { + gateOptions.push(gate); + } + }); + setGates(gates); + setGateOptions(gateOptions); + setGateMarkers(gateMarkers); + setGateSearchEntries(searchEntries); + }); + }, [gateAPI]); // eslint-disable-line react-hooks/exhaustive-deps + + useEffect(() => { + loadTerminals(); + loadGates(); + }, [loadTerminals, loadGates]); + + useEffect(() => { + let tArr = Array.from(terminalMarkers.values()); + let gArr = Array.from(gateMarkers.values()); + setMarkers(tArr.concat(gArr)); + }, [terminalMarkers, gateMarkers]); + + useEffect(() => { + setCustomSearchEntries(terminalSearchEntries.concat(gateSearchEntries)); + }, [terminalSearchEntries, gateSearchEntries]); + + 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 + }); + }; + + const closeNewMarkerDialog = () => { + if (new Date().valueOf() > endTime) { + setOpenMarkerDialog(false); + setSelectKey(""); + } + }; + + const setMarker = () => { + if (markerType === pond.ObjectType.OBJECT_TYPE_TERMINAL) { + let terminal = terminals.get(selectKey); + if (terminal) { + updateTerminalMarkers(terminal, newLong, newLat, true); + } + } + if (markerType === pond.ObjectType.OBJECT_TYPE_GATE) { + let gate = gates.get(selectKey); + if (gate) { + updateGateMarkers(gate, newLong, newLat, true); + } + } + setOpenMarkerDialog(false); + }; + + const markerDialog = () => { + return ( + { + setOpenMarkerDialog(false); + }}> + Place Marker + + + {markerType === pond.ObjectType.OBJECT_TYPE_TERMINAL && ( + option.name} + fullWidth + onChange={(e, val) => { + val && setSelectKey(val.key); + }} + renderInput={params => } + /> + )} + {markerType === pond.ObjectType.OBJECT_TYPE_GATE && ( + option.name} + fullWidth + onChange={(e, val) => { + val && setSelectKey(val.key); + }} + renderInput={params => } + /> + )} + + + + + + + + + + ); + }; + + return ( + + {markerDialog()} + { + //open drawer + setObjectKey(result.id?.toString() ?? ""); + if (result.place_type.includes("terminal")) { + setTerminalDrawerOpen(true); + } else if (result.place_type.includes("gate")) { + setGateDrawerOpen(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("terminal")) { + centered = false; + } else if (result.place_type.includes("gate")) { + zoom = zoomLevels.close; + centered = false; + } + moveMap(lat, long, zoom, isMobile, centered); + } + }} + markerData={markers} + placingMarker={markerType !== pond.ObjectType.OBJECT_TYPE_UNKNOWN} + mapTools={ + + } + mapClick={e => { + setNewLong(e.lngLat.lng); + setNewLat(e.lngLat.lat); + if (markerType !== pond.ObjectType.OBJECT_TYPE_UNKNOWN) { + setOpenMarkerDialog(true); + } + }} + currentView={currentView} + /> + { + moveMap(lat, long, zoomLevels.close, isMobile); + }} + open={gateDrawerOpen} + onClose={() => { + if (new Date().valueOf() > endTime) { + setObjectKey(""); + setGateDrawerOpen(false); + } + }} + selectedGate={objectKey} + 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(gateMarkers); + newMarkers.delete(key); + setGateMarkers(newMarkers); + //add it back to the options for terminals + let gate = gates.get(key); + if (gate) { + gateOptions.push(gate); + } + } + }} + /> + { + moveMap(lat, long, zoomLevels.close, isMobile); + }} + open={terminalDrawerOpen} + onClose={() => { + if (new Date().valueOf() > endTime) { + setObjectKey(""); + setTerminalDrawerOpen(false); + } + }} + selectedKey={objectKey} + 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(terminalMarkers); + newMarkers.delete(key); + setTerminalMarkers(newMarkers); + //add it back to the options for terminals + let terminal = terminals.get(key); + if (terminal) { + terminalOptions.push(terminal); + } + } + }} + /> + + ); +} diff --git a/src/maps/mapObjectControllers/ConstructionMapController.tsx b/src/maps/mapObjectControllers/ConstructionMapController.tsx new file mode 100644 index 0000000..68e27ba --- /dev/null +++ b/src/maps/mapObjectControllers/ConstructionMapController.tsx @@ -0,0 +1,757 @@ +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) + .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 => { + 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) + .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]); // 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) + .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) + .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} + /> + + )} + + ); +} diff --git a/src/maps/mapObjectTools/aviationMapTools.tsx b/src/maps/mapObjectTools/aviationMapTools.tsx new file mode 100644 index 0000000..17ae028 --- /dev/null +++ b/src/maps/mapObjectTools/aviationMapTools.tsx @@ -0,0 +1,136 @@ +import { + Box, + Grid2 as Grid, + IconButton, + List, + ListItem, + ListItemIcon, + Theme, + Typography, +} from "@mui/material"; +import FieldNamesIcon from "products/AgIcons/FieldNames"; +import AddPlaneIcon from "products/AviationIcons/AddPlaneIcon"; +import OmniAirDeviceIcon from "products/AviationIcons/OmniAirDeviceIcon"; +import VisibilityOffIcon from "@mui/icons-material/VisibilityOff"; +import PlaneIcon from "products/AviationIcons/PlaneIcon"; +import { pond } from "protobuf-ts/pond"; +import React, { useState } from "react"; +import { makeStyles } from "@mui/styles"; + +const useStyles = makeStyles((theme: Theme) => ({ + button: { + background: theme.palette.background.default, + margin: 5, + boxShadow: "4px 4px 10px black", + width: 50, + height: 50 + }, + list: { + background: theme.palette.background.default, + opacity: 0.75, + borderRadius: 15, + marginTop: 10, + boxShadow: "4px 4px 10px black" + }, + listItems: { + borderRadius: "3rem" + }, + liFont: { + fontWeight: 700, + marginLeft: -15 + } +})); + +interface Props { + toggleMarkerType: (type: pond.ObjectType) => void; + toggleMarkerDisplay: (display: boolean) => void; +} + +export default function AviationMapTools(props: Props) { + const classes = useStyles(); + const { toggleMarkerType, toggleMarkerDisplay } = props; + const [airportMenu, setAirportMenu] = useState<"block" | "none">("none"); + const [siteButtonHovered, setSiteButtonHovered] = useState(false); + const [placeTerminal, setPlaceTerminal] = useState(false); + const [placeGate, setPlaceGate] = useState(false); + const [markerDisplay, setMarkerDisplay] = useState(false); + + const toggleAirportMenu = () => { + if (airportMenu === "none") { + setAirportMenu("block"); + } else { + setAirportMenu("none"); + setPlaceTerminal(false); + setPlaceGate(false); + toggleMarkerType(pond.ObjectType.OBJECT_TYPE_UNKNOWN); + } + }; + + return ( + + + + setSiteButtonHovered(true)} + onMouseOut={() => setSiteButtonHovered(false)} + style={ + airportMenu === "block" + ? { background: "#004f9b" } + : siteButtonHovered + ? { background: "grey" } + : {} + }> + + + + + + { + setPlaceTerminal(true); + setPlaceGate(false); + toggleMarkerType(pond.ObjectType.OBJECT_TYPE_TERMINAL); + }} + className={classes.listItems} + style={placeTerminal ? { background: "#004f9b" } : {}}> + + + + Add Terminal + + { + setPlaceGate(true); + setPlaceTerminal(false); + toggleMarkerType(pond.ObjectType.OBJECT_TYPE_GATE); + }} + className={classes.listItems} + style={placeGate ? { background: "#004f9b" } : {}}> + + + + Add Gate + + { + setMarkerDisplay(!markerDisplay); + toggleMarkerDisplay(!markerDisplay); + }} + className={classes.listItems}> + + {markerDisplay ? : } + + Show Marker Names + + + + + + ); +} diff --git a/src/maps/mapObjectTools/constructionMapTools.tsx b/src/maps/mapObjectTools/constructionMapTools.tsx new file mode 100644 index 0000000..a686cef --- /dev/null +++ b/src/maps/mapObjectTools/constructionMapTools.tsx @@ -0,0 +1,238 @@ +import { + //Box, + Grid2 as Grid, + IconButton, + List, + ListItem, + ListItemIcon, + Theme, + Typography +} from "@mui/material"; +import { Device } from "models"; +import FieldNamesIcon from "products/AgIcons/FieldNames"; +import DeviceGroupIcon from "products/Construction/DeviceGroupIcon"; +import JobSiteIcon from "products/Construction/JobSiteIcon"; +import NexusSTIcon from "products/Construction/NexusSTIcon"; +import VisibilityOffIcon from "@mui/icons-material/VisibilityOff"; + +//import { useDeviceAPI, useGlobalState } from "providers"; +// import { downloadJSON } from "utils"; +import { useState } from "react"; +import { pond } from "protobuf-ts/pond"; +import { makeStyles } from "@mui/styles"; + +const useStyles = makeStyles((theme: Theme) => ({ + button: { + background: theme.palette.background.default, + margin: 5, + boxShadow: "4px 4px 10px black", + width: 50, + height: 50 + }, + list: { + background: theme.palette.background.default, + opacity: 0.75, + borderRadius: 15, + marginTop: 10, + boxShadow: "4px 4px 10px black" + }, + listItems: { + borderRadius: "3rem" + }, + liFont: { + fontWeight: 700, + marginLeft: -15 + } +})); + +interface Props { + toggleMarkerType: (type: pond.ObjectType) => void; + devices?: Device[]; + toggleMarkerDisplay: (display: boolean) => void; +} + +export default function ConstructionMapTools(props: Props) { + const { toggleMarkerType, toggleMarkerDisplay } = props; + //const [{ user }] = useGlobalState(); + const classes = useStyles(); + const [siteControls, setSiteControls] = useState<"block" | "none">("none"); + const [deviceControls, setDeviceControls] = useState<"block" | "none">("none"); + const [placeDevice, setPlaceDevice] = useState(false); + const [placeGroup, setPlaceGroup] = useState(false); + const [placeSite, setPlaceSite] = useState(false); + const [siteButtonHovered, setSiteButtonHovered] = useState(false); + const [devButtonHovered, setDevButtonHovered] = useState(false); + const [markerDisplay, setMarkerDisplay] = useState(false); + //const deviceAPI = useDeviceAPI(); + + const toggleSiteControls = () => { + if (siteControls === "none") { + setSiteControls("block"); + setDeviceControls("none"); + setPlaceDevice(false); + setPlaceGroup(false); + } else { + setSiteControls("none"); + setPlaceSite(false); + toggleMarkerType(pond.ObjectType.OBJECT_TYPE_UNKNOWN); + } + }; + + const toggleDeviceControls = () => { + if (deviceControls === "none") { + setDeviceControls("block"); + setSiteControls("none"); + setPlaceSite(false); + } else { + setDeviceControls("none"); + setPlaceDevice(false); + setPlaceGroup(false); + toggleMarkerType(pond.ObjectType.OBJECT_TYPE_UNKNOWN); + } + }; + + // const getDeviceGeoJson = () => { + // let ids: number[] = []; + // if (props.devices) { + // props.devices.forEach(dev => ids.push(dev.id())); + // deviceAPI.getMultiGeoJson(ids).then(resp => { + // downloadJSON(resp.data, "allDevicesGeoJSON.json"); + // }); + // } + // }; + + return ( + + + + {" "} + {/* row that has the buttons for controlling the display of the controls */} + + setSiteButtonHovered(true)} + onMouseOut={() => setSiteButtonHovered(false)} + style={ + siteControls === "block" + ? { background: "blue" } + : siteButtonHovered + ? { background: "grey" } + : {} + }> + {siteControls === "block" ? : } + + + + setDevButtonHovered(true)} + onMouseOut={() => setDevButtonHovered(false)} + style={ + deviceControls === "block" + ? { background: "blue" } + : devButtonHovered + ? { background: "grey" } + : {} + }> + {deviceControls === "block" ? : } + + + + + + + {" "} + {/* row that contains the controls */} + + + { + toggleMarkerType(pond.ObjectType.OBJECT_TYPE_SITE); + setPlaceSite(true); + }} + className={classes.listItems} + style={placeSite ? { background: "blue" } : {}}> + + + + Add Site + + { + setMarkerDisplay(!markerDisplay); + toggleMarkerDisplay(!markerDisplay); + }} + className={classes.listItems}> + + {markerDisplay ? : } + + Show Marker Names + + + + + + { + setPlaceDevice(true); + toggleMarkerType(pond.ObjectType.OBJECT_TYPE_DEVICE); + setPlaceGroup(false); + }} + className={classes.listItems} + style={placeDevice ? { background: "blue" } : {}}> + + + + Add Device + + { + setPlaceGroup(true); + toggleMarkerType(pond.ObjectType.OBJECT_TYPE_GROUP); + setPlaceDevice(false); + }} + className={classes.listItems} + style={placeGroup ? { background: "blue" } : {}}> + + + + Add Group + + { + setMarkerDisplay(!markerDisplay); + toggleMarkerDisplay(!markerDisplay); + }} + className={classes.listItems}> + + {markerDisplay ? : } + + Show Marker Names + + {/* TODO: Create flag for the GeoJson data for device from map */} + {/* {user.hasAdmin() && ( + getDeviceGeoJson()} + className={classes.listItems}> + + Export GeoJSON for devices + + )} */} + + + + + + ); +} diff --git a/src/models/Site.ts b/src/models/Site.ts new file mode 100644 index 0000000..9633418 --- /dev/null +++ b/src/models/Site.ts @@ -0,0 +1,80 @@ +import { cloneDeep } from "lodash"; +import { pond } from "protobuf-ts/pond"; +import { or } from "utils/types"; + +export class Site { + public settings: pond.SiteSettings = pond.SiteSettings.create(); + public status: pond.SiteStatus = pond.SiteStatus.create(); + + public static create(ps?: pond.Site): Site { + let my = new Site(); + if (ps) { + my.settings = pond.SiteSettings.fromObject(cloneDeep(or(ps.settings, {}))); + my.status = pond.SiteStatus.fromObject(cloneDeep(or(ps.status, {}))); + } + return my; + } + + public static clone(other?: Site): Site { + if (other) { + return Site.create( + pond.Site.fromObject({ + settings: cloneDeep(other.settings), + status: cloneDeep(other.status) + }) + ); + } + return Site.create(); + } + + public static any(data: any): Site { + return Site.create(pond.Site.fromObject(cloneDeep(data))); + } + + public key(): string { + return this.settings.key; + } + + public longitude(): number { + return this.settings.longitude; + } + + public latitude(): number { + return this.settings.latitude; + } + + public siteName(): string { + return this.settings.siteName; + } + + public siteAddress(): string { + return this.settings.siteAddress; + } + + public siteDescription(): string { + return this.settings.siteDescription; + } + + public clientName(): string { + return this.settings.clientName; + } + + public clientPhone(): string { + return this.settings.clientPhone; + } + + public jobType(): string { + return this.settings.jobType; + } + + public jobDetails(): string { + return this.settings.jobDetails; + } + + public location(): pond.Location { + let loc = pond.Location.create(); + loc.longitude = this.settings.longitude; + loc.latitude = this.settings.latitude; + return loc; + } +} diff --git a/src/models/index.ts b/src/models/index.ts index f731969..f07e985 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -8,7 +8,7 @@ export * from "./Group"; export * from "./Interaction"; export * from "./Scope"; export * from "./ShareableLink"; -// export * from "./Site"; +export * from "./Site"; export * from "./Tag"; // export * from "./Task"; // export * from "./Upgrade"; diff --git a/src/navigation/Router.tsx b/src/navigation/Router.tsx index af596a0..42683fa 100644 --- a/src/navigation/Router.tsx +++ b/src/navigation/Router.tsx @@ -11,6 +11,8 @@ import FieldMap from "pages/FieldMap"; import GrainBag from "pages/grainBag"; import Terminals from "pages/Terminals"; import Gate from "pages/Gate"; +import AviationMap from "pages/AviationMap"; +import ConstructionSiteMap from "pages/ConstructionSiteMap"; const DeviceHistory = lazy(() => import("pages/DeviceHistory")); const DevicePage = lazy(() => import("pages/Device")); @@ -246,7 +248,12 @@ export default function Router(props: Props) { {/* Page routes */} {/* Hello!} /> */} } /> + + {/* Map pages */} } /> + } /> + } /> + } /> } /> {/* diff --git a/src/navigation/SideNavigator.tsx b/src/navigation/SideNavigator.tsx index 924e359..9f0ddb4 100644 --- a/src/navigation/SideNavigator.tsx +++ b/src/navigation/SideNavigator.tsx @@ -37,6 +37,7 @@ import MiningIcon from "products/ventilation/MiningIcon"; import { useAuth0 } from "@auth0/auth0-react"; import FieldsIcon from "products/AgIcons/FieldsIcon"; import PlaneIcon from "products/AviationIcons/PlaneIcon"; +import AirportMapIcon from "products/AviationIcons/AirportMapIcon"; const drawerWidth = 230; @@ -237,6 +238,20 @@ export default function SideNavigator(props: Props) { )} + {(IsOmniAir || user.hasFeature("admin")) && ( + + goTo("/aviationMap")} + classes={getClasses("/aviationMap")} + > + + + + {open && } + + + )} ) } diff --git a/src/pages/AviationMap.tsx b/src/pages/AviationMap.tsx new file mode 100644 index 0000000..d0a3140 --- /dev/null +++ b/src/pages/AviationMap.tsx @@ -0,0 +1,26 @@ +import AviationMapController from "maps/mapObjectControllers/AviationMapController"; +import { useLocation } from "react-router"; +import PageContainer from "./PageContainer"; + +export default function AviationMap() { + const location = useLocation(); + + return ( + + {location.state !== undefined && + location.state !== null && + location.state.long !== undefined && + location.state.lat !== undefined ? ( + + ) : ( + + )} + + ); +} \ No newline at end of file diff --git a/src/pages/ConstructionSiteMap.tsx b/src/pages/ConstructionSiteMap.tsx new file mode 100644 index 0000000..27eaf46 --- /dev/null +++ b/src/pages/ConstructionSiteMap.tsx @@ -0,0 +1,27 @@ +import PageContainer from "./PageContainer"; +import ConstructionMapController from "maps/mapObjectControllers/ConstructionMapController"; +import { useLocation } from "react-router"; + +export default function ConstructionSiteMap() { + const location = useLocation(); + + return ( + + {location.state !== undefined && + location.state !== null && + location.state.long !== undefined && + location.state.lat !== undefined ? ( + + ) : ( + + )} + + ); +} \ No newline at end of file diff --git a/src/providers/index.ts b/src/providers/index.ts index 864b919..305e7f1 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -25,7 +25,7 @@ export { // useMetricAPI, usePermissionAPI, // usePreferenceAPI, - // useSiteAPI, + useSiteAPI, //TODO: update api with resolve, reject useTagAPI, useTeamAPI, useImagekitAPI, diff --git a/src/providers/pond/pond.tsx b/src/providers/pond/pond.tsx index c882a6f..779b4a0 100644 --- a/src/providers/pond/pond.tsx +++ b/src/providers/pond/pond.tsx @@ -25,6 +25,7 @@ import FieldMarkerProvider, {useFieldMarkerAPI} from "./fieldMarkerAPI"; import HomeMarkerProvider, {useHomeMarkerAPI} from "./homeMarkerAPI"; import HarvestPlanProvider, {useHarvestPlanAPI} from "./harvestPlanAPI"; import TerminalProvider, {useTerminalAPI} from "./terminalAPI" +import SiteProvider, {useSiteAPI} from "./siteAPI"; // import NoteProvider from "providers/noteAPI"; export const pondURL = (partial: string, demo: boolean = false): string => { @@ -66,7 +67,9 @@ export default function PondProvider(props: PropsWithChildren) { - {children} + + {children} + @@ -119,5 +122,6 @@ export { useFieldMarkerAPI, useHomeMarkerAPI, useHarvestPlanAPI, - useTerminalAPI + useTerminalAPI, + useSiteAPI }; diff --git a/src/providers/pond/siteAPI.tsx b/src/providers/pond/siteAPI.tsx new file mode 100644 index 0000000..133d888 --- /dev/null +++ b/src/providers/pond/siteAPI.tsx @@ -0,0 +1,179 @@ +import { AxiosResponse } from "axios"; +import { useHTTP } from "hooks"; +import { pond } from "protobuf-ts/pond"; +import { useGlobalState } from "providers/StateContainer"; +import React, { createContext, PropsWithChildren, useContext } from "react"; +import { or } from "utils"; +import { pondURL } from "./pond"; + +export interface ISiteAPIContext { + addSite: (site: pond.SiteSettings) => Promise; + getSite: (siteID: string) => Promise; + getSitePage: (siteID: string) => Promise; + listSites: ( + limit: number, + offset: number, + order?: "asc" | "desc", + orderBy?: string, + search?: string, + as?: string, + asRoot?: boolean + ) => Promise>; + listSitesPageData: ( + limit: number, + offset: number, + order?: "asc" | "desc", + orderBy?: string, + search?: string, + as?: string, + asRoot?: boolean + ) => Promise>; + removeSite: (siteID: string) => Promise>; + updateSite: ( + key: string, + site: pond.SiteSettings, + asRoot?: true + ) => Promise>; + updateLink: ( + parentKey: string, + parentType: string, + objectID: string, + objectType: string, + permissions: string[] + ) => Promise; +} + +export const SiteAPIContext = createContext({} as ISiteAPIContext); + +interface Props {} + +export default function SiteProvider(props: PropsWithChildren) { + const { children } = props; + const { get, del, post, put } = useHTTP(); + const [{ as }] = useGlobalState(); + + const getSitePage = (siteID: string) => { + if (as) return get(pondURL("/sitePage/" + siteID + "?as=" + as)); + return get(pondURL("/sitePage/" + siteID)); + }; + + const addSite = (site: pond.SiteSettings) => { + if (as) return post(pondURL("/sites?as=" + as), site); + return post(pondURL("/sites"), site); + }; + + const getSite = (siteID: string) => { + if (as) return get(pondURL("/site/" + siteID + "?as=" + as)); + return get(pondURL("/site/" + siteID)); + }; + + const removeSite = (key: string) => { + return del(pondURL("/sites/" + key)); + }; + + const listSites = ( + limit: number, + offset: number, + order?: "asc" | "desc", + orderBy?: string, + search?: string, + as?: string, + asRoot?: boolean + ) => { + return get( + pondURL( + "/sites" + + "?limit=" + + limit + + "&offset=" + + offset + + ("&order=" + or(order, "asc")) + + ("&by=" + or(orderBy, "key")) + + (as ? "&as=" + as : "") + + (asRoot ? "&asRoot=" + asRoot.toString() : "") + + (search ? "&search=" + search : "") + ) + ); + }; + + const listSitesPageData = ( + limit: number, + offset: number, + order?: "asc" | "desc", + orderBy?: string, + search?: string, + as?: string, + asRoot?: boolean + ) => { + return get( + pondURL( + "/sitesWithData" + + "?limit=" + + limit + + "&offset=" + + offset + + ("&order=" + or(order, "asc")) + + ("&by=" + or(orderBy, "key")) + + (as ? "&as=" + as : "") + + (asRoot ? "&asRoot=" + asRoot.toString() : "") + + (search ? "&search=" + search : "") + ) + ); + }; + + const updateSite = (key: string, site: pond.SiteSettings, asRoot?: boolean) => { + if (as) + return put( + pondURL( + "/sites/" + key + (as ? "?as=" + as : "") + (asRoot ? "&asRoot=" + asRoot.toString() : "") + ), + site + ); + return put( + pondURL("/sites/" + key + (asRoot ? "?asRoot=" + asRoot.toString() : "")), + site + ); + }; + + const updateLink = ( + parentID: string, + parentType: string, + objectID: string, + objectType: string, + permissions: string[] + ) => { + if (as) + return post(pondURL(`/sites/` + parentID + `/link?as=${as}`), { + Key: objectID, + Type: objectType, + Parent: parentID, + ParentType: parentType, + Permissions: permissions + }); + return post(pondURL(`/sites/` + parentID + `/link`), { + Key: objectID, + Type: objectType, + Parent: parentID, + ParentType: parentType, + Permissions: permissions + }); + }; + + return ( + + {children} + + ); +} + +export const useSiteAPI = () => useContext(SiteAPIContext); diff --git a/src/weather/weather.tsx b/src/weather/weather.tsx index 082e88e..c3b4251 100644 --- a/src/weather/weather.tsx +++ b/src/weather/weather.tsx @@ -8,7 +8,7 @@ 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 WeatherIcon from "./weatherIcon"; import { getThemeType } from "theme"; import { useMobile, useThemeType } from "hooks"; import WindSpeedDark from "assets/components/windSpeedDark.png"; @@ -220,10 +220,10 @@ export default function Weather(props: Props) { padding="5%" className={classes.upperSquare} style={{ flexDirection: isMobile ? "column" : "row" }}> - {/* */} + /> {Math.round(celsiusConverter(currentConditions.main.temp))}℃ diff --git a/src/weather/weatherIcon.tsx b/src/weather/weatherIcon.tsx index 32e9385..8fd864e 100644 --- a/src/weather/weatherIcon.tsx +++ b/src/weather/weatherIcon.tsx @@ -28,7 +28,7 @@ interface Props { //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 size = props.size ? props.size : 25; const iconId = props.openweatherIconId; switch (iconId) {