first commit
This commit is contained in:
270
app/src/modules/visualization/RealTimeVisulization.tsx
Normal file
270
app/src/modules/visualization/RealTimeVisulization.tsx
Normal file
@@ -0,0 +1,270 @@
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
import { usePlayButtonStore } from "../../store/usePlayButtonStore";
|
||||
import Panel from "./widgets/panel/Panel";
|
||||
import AddButtons from "./widgets/panel/AddButtons";
|
||||
import { useSelectedZoneStore } from "../../store/visualization/useZoneStore";
|
||||
import DisplayZone from "./zone/DisplayZone";
|
||||
import useModuleStore from "../../store/useModuleStore";
|
||||
import { getZone2dData } from "../../services/visulization/zone/getZoneData";
|
||||
import SocketRealTimeViz from "./socket/realTimeVizSocket.dev";
|
||||
import RenderOverlay from "../../components/templates/Overlay";
|
||||
import ConfirmationPopup from "../../components/layout/confirmationPopup/ConfirmationPopup";
|
||||
import DroppedObjects from "./widgets/floating/DroppedFloatingWidgets";
|
||||
import EditWidgetOption from "../../components/ui/menu/EditWidgetOption";
|
||||
import { useEditWidgetOptionsStore, useRightClickSelected, useRightSelected, } from "../../store/visualization/useZone3DWidgetStore";
|
||||
import OuterClick from "../../utils/outerClick";
|
||||
import { useWidgetStore } from "../../store/useWidgetStore";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
type Side = "top" | "bottom" | "left" | "right";
|
||||
|
||||
type FormattedZoneData = Record<
|
||||
string,
|
||||
{
|
||||
activeSides: Side[];
|
||||
panelOrder: Side[];
|
||||
points: [];
|
||||
lockedPanels: Side[];
|
||||
zoneUuid: string;
|
||||
zoneViewPortTarget: number[];
|
||||
zoneViewPortPosition: number[];
|
||||
widgets: Widget[];
|
||||
}
|
||||
>;
|
||||
type Widget = {
|
||||
id: string;
|
||||
type: string;
|
||||
title: string;
|
||||
panel: Side;
|
||||
data: any;
|
||||
};
|
||||
|
||||
// Define the type for HiddenPanels, where keys are zone IDs and values are arrays of hidden sides
|
||||
interface HiddenPanels {
|
||||
[zoneUuid: string]: Side[];
|
||||
}
|
||||
|
||||
const RealTimeVisulization: React.FC = () => {
|
||||
const [hiddenPanels, setHiddenPanels] = React.useState<HiddenPanels>({});
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const { isPlaying } = usePlayButtonStore();
|
||||
const { activeModule } = useModuleStore();
|
||||
const [zonesData, setZonesData] = useState<FormattedZoneData>({});
|
||||
const { selectedZone, setSelectedZone } = useSelectedZoneStore();
|
||||
|
||||
const { setRightSelect } = useRightSelected();
|
||||
const { editWidgetOptions, setEditWidgetOptions } =
|
||||
useEditWidgetOptionsStore();
|
||||
const { rightClickSelected, setRightClickSelected } = useRightClickSelected();
|
||||
const [openConfirmationPopup, setOpenConfirmationPopup] = useState(false);
|
||||
const { setSelectedChartId } = useWidgetStore();
|
||||
const [waitingPanels, setWaitingPanels] = useState(null);
|
||||
const { projectId } = useParams();
|
||||
|
||||
OuterClick({
|
||||
contextClassName: [
|
||||
"chart-container",
|
||||
"floating",
|
||||
"sidebar-right-wrapper",
|
||||
"card",
|
||||
"dropdown-menu",
|
||||
"dropdown-options",
|
||||
],
|
||||
setMenuVisible: () => setSelectedChartId(null),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
async function GetZoneData() {
|
||||
const email = localStorage.getItem("email") ?? "";
|
||||
const organization = email?.split("@")[1]?.split(".")[0];
|
||||
try {
|
||||
const response = await getZone2dData(organization, projectId);
|
||||
// console.log('responseRt: ', response);
|
||||
|
||||
|
||||
if (!Array.isArray(response)) {
|
||||
return;
|
||||
}
|
||||
const formattedData = response.reduce<FormattedZoneData>(
|
||||
(acc, zone) => {
|
||||
|
||||
acc[zone.zoneName] = {
|
||||
activeSides: [],
|
||||
panelOrder: [],
|
||||
lockedPanels: [],
|
||||
points: zone.points,
|
||||
zoneUuid: zone.zoneUuid,
|
||||
zoneViewPortTarget: zone.viewPortCenter,
|
||||
zoneViewPortPosition: zone.viewPortposition,
|
||||
widgets: [],
|
||||
};
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
setZonesData(formattedData);
|
||||
} catch (error) {
|
||||
echo.error("Failed to fetch zone data");
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
GetZoneData();
|
||||
}, [activeModule]); // Removed `zones` from dependencies
|
||||
|
||||
useEffect(() => {
|
||||
setZonesData((prev) => {
|
||||
if (!selectedZone) return prev;
|
||||
return {
|
||||
...prev,
|
||||
[selectedZone.zoneName]: {
|
||||
...prev[selectedZone.zoneName], // Keep existing properties
|
||||
activeSides: selectedZone.activeSides || [],
|
||||
panelOrder: selectedZone.panelOrder || [],
|
||||
lockedPanels: selectedZone.lockedPanels || [],
|
||||
points: selectedZone.points || [],
|
||||
zoneUuid: selectedZone.zoneUuid || "",
|
||||
zoneViewPortTarget: selectedZone.zoneViewPortTarget || [],
|
||||
zoneViewPortPosition: selectedZone.zoneViewPortPosition || [],
|
||||
widgets: selectedZone.widgets || [],
|
||||
},
|
||||
};
|
||||
});
|
||||
}, [selectedZone]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
const editWidgetOptions = document.querySelector(
|
||||
".editWidgetOptions-wrapper"
|
||||
);
|
||||
if (
|
||||
editWidgetOptions &&
|
||||
!editWidgetOptions.contains(event.target as Node)
|
||||
) {
|
||||
setRightClickSelected(null);
|
||||
setRightSelect(null);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, [setRightClickSelected, setRightSelect]);
|
||||
|
||||
const [canvasDimensions, setCanvasDimensions] = useState({
|
||||
width: 0,
|
||||
height: 0,
|
||||
});
|
||||
useEffect(() => {
|
||||
const canvas = document.getElementById("work-space-three-d-canvas");
|
||||
if (!canvas) return;
|
||||
|
||||
const updateCanvasDimensions = () => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
setCanvasDimensions({
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
});
|
||||
};
|
||||
|
||||
updateCanvasDimensions();
|
||||
const resizeObserver = new ResizeObserver(updateCanvasDimensions);
|
||||
resizeObserver.observe(canvas);
|
||||
|
||||
return () => resizeObserver.unobserve(canvas);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>
|
||||
{`
|
||||
:root {
|
||||
--realTimeViz-container-width: ${canvasDimensions.width};
|
||||
--realTimeViz-container-height: ${canvasDimensions.height};
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="realTime-viz"
|
||||
id="real-time-vis-canvas"
|
||||
style={{
|
||||
height: isPlaying || activeModule !== "visualization" ? "100vh" : "",
|
||||
width: isPlaying || activeModule !== "visualization" ? "100vw" : "",
|
||||
left: isPlaying || activeModule !== "visualization" ? "0%" : "",
|
||||
borderRadius:
|
||||
isPlaying || activeModule !== "visualization" ? "" : "6px",
|
||||
}}
|
||||
>
|
||||
<div className="realTime-viz-wrapper">
|
||||
{openConfirmationPopup && (
|
||||
<RenderOverlay>
|
||||
<ConfirmationPopup
|
||||
message={"Are you sure want to delete?"}
|
||||
onConfirm={() => console.log("Confirmed")}
|
||||
onCancel={() => setOpenConfirmationPopup(false)}
|
||||
/>
|
||||
</RenderOverlay>
|
||||
)}
|
||||
{activeModule === "visualization" && selectedZone.zoneName !== "" && (
|
||||
<DroppedObjects />
|
||||
)}
|
||||
{activeModule === "visualization" && <SocketRealTimeViz />}
|
||||
|
||||
{activeModule === "visualization" &&
|
||||
editWidgetOptions &&
|
||||
rightClickSelected && (
|
||||
<EditWidgetOption
|
||||
options={[
|
||||
"Duplicate",
|
||||
"Vertical Move",
|
||||
"Horizontal Move",
|
||||
"RotateX",
|
||||
"RotateY",
|
||||
"Delete",
|
||||
]}
|
||||
onClick={(e) => {
|
||||
setRightSelect(e);
|
||||
setEditWidgetOptions(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeModule === "visualization" && (
|
||||
<>
|
||||
<DisplayZone
|
||||
zonesData={zonesData}
|
||||
selectedZone={selectedZone}
|
||||
setSelectedZone={setSelectedZone}
|
||||
hiddenPanels={hiddenPanels}
|
||||
setHiddenPanels={setHiddenPanels}
|
||||
/>
|
||||
|
||||
{!isPlaying && selectedZone?.zoneName !== "" && (
|
||||
<AddButtons
|
||||
hiddenPanels={hiddenPanels}
|
||||
setHiddenPanels={setHiddenPanels}
|
||||
selectedZone={selectedZone}
|
||||
setSelectedZone={setSelectedZone}
|
||||
waitingPanels={waitingPanels}
|
||||
setWaitingPanels={setWaitingPanels}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Panel
|
||||
selectedZone={selectedZone}
|
||||
setSelectedZone={setSelectedZone}
|
||||
hiddenPanels={hiddenPanels}
|
||||
setZonesData={setZonesData}
|
||||
waitingPanels={waitingPanels}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RealTimeVisulization;
|
||||
@@ -0,0 +1,34 @@
|
||||
import html2canvas from "html2canvas";
|
||||
|
||||
export const captureVisualization = async (): Promise<string | null> => {
|
||||
const container = document.getElementById("work-space-three-d-canvas");
|
||||
if (!container) {
|
||||
console.error("Container element not found");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// Hide any elements you don't want in the screenshot
|
||||
const originalVisibility = container.style.visibility;
|
||||
container.style.visibility = 'visible';
|
||||
|
||||
const canvas = await html2canvas(container, {
|
||||
scale: 2, // Higher scale for better quality
|
||||
logging: false, // Disable console logging
|
||||
useCORS: true, // Handle cross-origin images
|
||||
allowTaint: true, // Allow tainted canvas
|
||||
backgroundColor: '#ffffff', // Set white background
|
||||
removeContainer: true // Clean up temporary containers
|
||||
});
|
||||
|
||||
// Restore original visibility
|
||||
container.style.visibility = originalVisibility;
|
||||
|
||||
// Convert to PNG with highest quality
|
||||
return canvas.toDataURL('image/png', 1.0);
|
||||
} catch (error) {
|
||||
echo.error("Failed to capturing visualization");
|
||||
console.error("Error capturing visualization:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
77
app/src/modules/visualization/functions/determinePosition.ts
Normal file
77
app/src/modules/visualization/functions/determinePosition.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
export function determinePosition(
|
||||
canvasRect: DOMRect,
|
||||
relativeX: number,
|
||||
relativeY: number
|
||||
): {
|
||||
top: number | "auto";
|
||||
left: number | "auto";
|
||||
right: number | "auto";
|
||||
bottom: number | "auto";
|
||||
} {
|
||||
const centerX = canvasRect.width / 2;
|
||||
const centerY = canvasRect.height / 2;
|
||||
|
||||
// Define a threshold for considering a point as "centered"
|
||||
const centerThreshold = 10; // Adjust this value as needed
|
||||
|
||||
// Check if the point is within the center threshold
|
||||
const isCenterX = Math.abs(relativeX - centerX) <= centerThreshold;
|
||||
const isCenterY = Math.abs(relativeY - centerY) <= centerThreshold;
|
||||
|
||||
// If the point is centered, return a special "centered" position
|
||||
if (isCenterX && isCenterY) {
|
||||
return {
|
||||
top: "auto",
|
||||
left: "auto",
|
||||
right: "auto",
|
||||
bottom: "auto",
|
||||
};
|
||||
}
|
||||
|
||||
let position: {
|
||||
top: number | "auto";
|
||||
left: number | "auto";
|
||||
right: number | "auto";
|
||||
bottom: number | "auto";
|
||||
};
|
||||
|
||||
if (relativeY < centerY) {
|
||||
if (relativeX < centerX) {
|
||||
// Top-left quadrant
|
||||
position = {
|
||||
top: relativeY - 41.5,
|
||||
left: relativeX - 125,
|
||||
right: "auto",
|
||||
bottom: "auto",
|
||||
};
|
||||
} else {
|
||||
// Top-right quadrant
|
||||
position = {
|
||||
top: relativeY - 41.5,
|
||||
right: canvasRect.width - relativeX - 125,
|
||||
left: "auto",
|
||||
bottom: "auto",
|
||||
};
|
||||
}
|
||||
} else {
|
||||
if (relativeX < centerX) {
|
||||
// Bottom-left quadrant
|
||||
position = {
|
||||
bottom: canvasRect.height - relativeY - 41.5,
|
||||
left: relativeX - 125,
|
||||
right: "auto",
|
||||
top: "auto",
|
||||
};
|
||||
} else {
|
||||
// Bottom-right quadrant
|
||||
position = {
|
||||
bottom: canvasRect.height - relativeY - 41.5,
|
||||
right: canvasRect.width - relativeX - 125,
|
||||
left: "auto",
|
||||
top: "auto",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return position;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export function getActiveProperties(position: any): [string, string] {
|
||||
if (position.top !== "auto" && position.left !== "auto") {
|
||||
return ["top", "left"]; // Top-left
|
||||
} else if (position.top !== "auto" && position.right !== "auto") {
|
||||
return ["top", "right"]; // Top-right
|
||||
} else if (position.bottom !== "auto" && position.left !== "auto") {
|
||||
return ["bottom", "left"]; // Bottom-left
|
||||
} else {
|
||||
return ["bottom", "right"]; // Bottom-right
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { Template } from "../../../store/useTemplateStore";
|
||||
import { captureVisualization } from "./captureVisualization";
|
||||
|
||||
type HandleSaveTemplateProps = {
|
||||
addTemplate: (template: Template) => void;
|
||||
floatingWidget: []; // Updated type from `[]` to `any[]` for clarity
|
||||
widgets3D: []; // Updated type from `[]` to `any[]` for clarity
|
||||
selectedZone: {
|
||||
panelOrder: string[];
|
||||
widgets: any[];
|
||||
};
|
||||
templates?: Template[];
|
||||
visualizationSocket: any;
|
||||
projectId?:string
|
||||
};
|
||||
|
||||
// Generate a unique ID
|
||||
const generateUniqueId = (): string => {
|
||||
return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
||||
};
|
||||
|
||||
export const handleSaveTemplate = async ({
|
||||
addTemplate,
|
||||
floatingWidget,
|
||||
widgets3D,
|
||||
selectedZone,
|
||||
templates = [],
|
||||
visualizationSocket,
|
||||
projectId
|
||||
}: HandleSaveTemplateProps): Promise<void> => {
|
||||
try {
|
||||
// Check if the selected zone has any widgets
|
||||
if (!selectedZone.panelOrder || selectedZone.panelOrder.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the template already exists
|
||||
const isDuplicate = templates.some(
|
||||
(template) =>
|
||||
JSON.stringify(template.panelOrder) ===
|
||||
JSON.stringify(selectedZone.panelOrder) &&
|
||||
JSON.stringify(template.widgets) ===
|
||||
JSON.stringify(selectedZone.widgets)
|
||||
);
|
||||
|
||||
if (isDuplicate) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Capture visualization snapshot
|
||||
const snapshot = await captureVisualization();
|
||||
|
||||
if (!snapshot) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a new template
|
||||
const newTemplate: Template = {
|
||||
id: generateUniqueId(),
|
||||
name: `Template ${new Date().toISOString()}`, // Better name formatting
|
||||
panelOrder: selectedZone.panelOrder,
|
||||
widgets: selectedZone.widgets,
|
||||
snapshot,
|
||||
floatingWidget,
|
||||
widgets3D,
|
||||
};
|
||||
|
||||
// Extract organization from email
|
||||
const email = localStorage.getItem("email") || "";
|
||||
const organization = email.includes("@")
|
||||
? email.split("@")[1]?.split(".")[0]
|
||||
: "";
|
||||
const userId = localStorage.getItem("userId");
|
||||
if (!organization) {
|
||||
return;
|
||||
}
|
||||
let saveTemplate = {
|
||||
organization: organization,
|
||||
template: newTemplate,
|
||||
userId,projectId
|
||||
};
|
||||
if (visualizationSocket) {
|
||||
visualizationSocket.emit("v1:viz-template:add", saveTemplate);
|
||||
}
|
||||
|
||||
// Save the template
|
||||
try {
|
||||
addTemplate(newTemplate);
|
||||
// const response = await saveTemplateApi(organization, newTemplate);
|
||||
//
|
||||
|
||||
// Add template only if API call succeeds
|
||||
} catch (apiError) {
|
||||
//
|
||||
}
|
||||
} catch (error) {
|
||||
echo.error("Failed to save template");
|
||||
}
|
||||
};
|
||||
130
app/src/modules/visualization/functions/handleUiDrop.ts
Normal file
130
app/src/modules/visualization/functions/handleUiDrop.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
|
||||
import { generateUniqueId } from "../../../functions/generateUniqueId";
|
||||
import { useDroppedObjectsStore } from "../../../store/visualization/useDroppedObjectsStore";
|
||||
import { determinePosition } from "./determinePosition";
|
||||
import { getActiveProperties } from "./getActiveProperties";
|
||||
|
||||
interface HandleDropProps {
|
||||
widgetSubOption: any;
|
||||
visualizationSocket: any;
|
||||
selectedZone: any;
|
||||
setFloatingWidget: (value: any) => void;
|
||||
event: React.DragEvent<HTMLDivElement>;
|
||||
projectId?:string
|
||||
}
|
||||
|
||||
export const createHandleDrop = ({
|
||||
widgetSubOption,
|
||||
visualizationSocket,
|
||||
selectedZone,
|
||||
setFloatingWidget,
|
||||
event,projectId
|
||||
}: HandleDropProps) => {
|
||||
|
||||
event.preventDefault();
|
||||
try {
|
||||
const email = localStorage.getItem("email") ?? "";
|
||||
const organization = email?.split("@")[1]?.split(".")[0];
|
||||
const userId = localStorage.getItem("userId");
|
||||
|
||||
const data = event.dataTransfer.getData("text/plain");
|
||||
if (widgetSubOption === "3D") return;
|
||||
if (!data || selectedZone.zoneName === "") return;
|
||||
|
||||
const droppedData = JSON.parse(data);
|
||||
const canvasElement = document.getElementById("work-space-three-d-canvas");
|
||||
if (!canvasElement) throw new Error("Canvas element not found");
|
||||
|
||||
const rect = canvasElement.getBoundingClientRect();
|
||||
const relativeX = event.clientX - rect.left;
|
||||
const relativeY = event.clientY - rect.top;
|
||||
|
||||
// Widget dimensions
|
||||
const widgetWidth = droppedData.width ?? 125;
|
||||
const widgetHeight = droppedData.height ?? 100;
|
||||
|
||||
// Center the widget at cursor
|
||||
const centerOffsetX = widgetWidth / 2;
|
||||
const centerOffsetY = widgetHeight / 2;
|
||||
|
||||
const adjustedX = relativeX - centerOffsetX;
|
||||
const adjustedY = relativeY - centerOffsetY;
|
||||
|
||||
const finalPosition = determinePosition(rect, adjustedX, adjustedY);
|
||||
const [activeProp1, activeProp2] = getActiveProperties(finalPosition);
|
||||
|
||||
let finalY = 0;
|
||||
let finalX = 0;
|
||||
|
||||
if (activeProp1 === "top") {
|
||||
finalY = adjustedY;
|
||||
} else {
|
||||
finalY = rect.height - (adjustedY + widgetHeight);
|
||||
}
|
||||
|
||||
if (activeProp2 === "left") {
|
||||
finalX = adjustedX;
|
||||
} else {
|
||||
finalX = rect.width - (adjustedX + widgetWidth);
|
||||
}
|
||||
|
||||
// Clamp to boundaries
|
||||
finalX = Math.max(0, Math.min(rect.width - widgetWidth, finalX));
|
||||
finalY = Math.max(0, Math.min(rect.height - widgetHeight, finalY));
|
||||
|
||||
const boundedPosition = {
|
||||
...finalPosition,
|
||||
[activeProp1]: finalY,
|
||||
[activeProp2]: finalX,
|
||||
[activeProp1 === "top" ? "bottom" : "top"]: "auto",
|
||||
[activeProp2 === "left" ? "right" : "left"]: "auto",
|
||||
};
|
||||
|
||||
const newObject = {
|
||||
...droppedData,
|
||||
id: generateUniqueId(),
|
||||
position: boundedPosition,
|
||||
};
|
||||
|
||||
const existingZone =
|
||||
useDroppedObjectsStore.getState().zones[selectedZone.zoneName];
|
||||
if (!existingZone) {
|
||||
useDroppedObjectsStore
|
||||
.getState()
|
||||
.setZone(selectedZone.zoneName, selectedZone.zoneUuid);
|
||||
}
|
||||
|
||||
const addFloatingWidget = {
|
||||
organization,
|
||||
widget: newObject,
|
||||
zoneUuid: selectedZone.zoneUuid,
|
||||
projectId,userId
|
||||
};
|
||||
console.log('addFloatingWidget: ', addFloatingWidget);
|
||||
|
||||
console.log('visualizationSocket: ', visualizationSocket);
|
||||
if (visualizationSocket) {
|
||||
visualizationSocket.emit("v1:viz-float:add", addFloatingWidget);
|
||||
}
|
||||
|
||||
useDroppedObjectsStore
|
||||
.getState()
|
||||
.addObject(selectedZone.zoneName, newObject);
|
||||
|
||||
const droppedObjectsStore = useDroppedObjectsStore.getState();
|
||||
const currentZone = droppedObjectsStore.zones[selectedZone.zoneName];
|
||||
|
||||
if (currentZone && currentZone.zoneUuid === selectedZone.zoneUuid) {
|
||||
// console.log(
|
||||
// `Objects for Zone ${selectedZone.zoneUuid}:`,
|
||||
// currentZone.objects
|
||||
// );
|
||||
setFloatingWidget(currentZone.objects);
|
||||
} else {
|
||||
console.warn("Zone not found or zoneUuid mismatch");
|
||||
}
|
||||
} catch (error) {
|
||||
echo.error("Failed to drop widget");
|
||||
console.error("Error in handleDrop:", error);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
export const useClickOutside = (
|
||||
ref: React.RefObject<HTMLElement>,
|
||||
callback: () => void
|
||||
) => {
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (ref.current && !event.composedPath().includes(ref.current)) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, [ref, callback]);
|
||||
};
|
||||
109
app/src/modules/visualization/mqttTemp/drieHtmlTemp.tsx
Normal file
109
app/src/modules/visualization/mqttTemp/drieHtmlTemp.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import { Html } from "@react-three/drei";
|
||||
import * as THREE from "three";
|
||||
import * as Types from "../../../types/world/worldTypes";
|
||||
import { useDrieTemp, useDrieUIValue } from "../../../store/builder/store"
|
||||
import UI from "./ui";
|
||||
import { useEffect } from "react";
|
||||
import { useThree } from "@react-three/fiber";
|
||||
|
||||
export default function DrieHtmlTemp() {
|
||||
const { drieTemp, setDrieTemp } = useDrieTemp();
|
||||
const { drieUIValue, setDrieUIValue } = useDrieUIValue();
|
||||
const state = useThree();
|
||||
const { camera, raycaster, scene } = state;
|
||||
|
||||
useEffect(() => {
|
||||
const canvasElement = state.gl.domElement;
|
||||
let drag = false;
|
||||
let isLeftMouseDown = false;
|
||||
|
||||
const onMouseDown = (evt: any) => {
|
||||
if (evt.button === 0) {
|
||||
isLeftMouseDown = true;
|
||||
drag = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseMove = () => {
|
||||
if (isLeftMouseDown) {
|
||||
drag = true;
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseUp = (evt: any) => {
|
||||
if (evt.button === 0) {
|
||||
isLeftMouseDown = false;
|
||||
if (drag) return;
|
||||
if (!scene) return
|
||||
let intersects = raycaster.intersectObjects(scene.children, true);
|
||||
if (intersects.length > 0) {
|
||||
let currentObject = intersects[0].object;
|
||||
|
||||
while (currentObject) {
|
||||
if (currentObject.name === "Scene") {
|
||||
break;
|
||||
}
|
||||
currentObject = currentObject.parent as THREE.Object3D;
|
||||
}
|
||||
if (currentObject && (currentObject.userData.name === "SV2 Controll pannel" || currentObject.userData.name === "forklift")) {
|
||||
const worldPos = new THREE.Vector3();
|
||||
currentObject.getWorldPosition(worldPos);
|
||||
|
||||
const rightOffset = new THREE.Vector3(1, 0, 0);
|
||||
const upOffset = new THREE.Vector3(0, 1, 0);
|
||||
|
||||
currentObject.localToWorld(rightOffset);
|
||||
currentObject.localToWorld(upOffset);
|
||||
|
||||
const finalPosition = worldPos.clone().addScaledVector(rightOffset.sub(currentObject.position).normalize(), 2.5).addScaledVector(upOffset.sub(currentObject.position).normalize(), 2.3);
|
||||
|
||||
setDrieTemp(finalPosition);
|
||||
} else {
|
||||
setDrieTemp(undefined);
|
||||
}
|
||||
}
|
||||
else {
|
||||
setDrieTemp(undefined);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
canvasElement.addEventListener("mousedown", onMouseDown);
|
||||
canvasElement.addEventListener("mouseup", onMouseUp);
|
||||
canvasElement.addEventListener("mousemove", onMouseMove);
|
||||
|
||||
return () => {
|
||||
canvasElement.removeEventListener("mousedown", onMouseDown);
|
||||
canvasElement.removeEventListener("mouseup", onMouseUp);
|
||||
canvasElement.removeEventListener("mousemove", onMouseMove);
|
||||
};
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
{drieTemp &&
|
||||
<mesh position={[drieTemp.x, drieTemp.y, drieTemp.z]}>
|
||||
<Html
|
||||
as="div"
|
||||
center
|
||||
zIndexRange={[1, 0]}
|
||||
transform
|
||||
sprite
|
||||
style={{
|
||||
padding: "10px",
|
||||
color: "white",
|
||||
borderRadius: "8px",
|
||||
textAlign: "center",
|
||||
fontFamily: "Arial, sans-serif",
|
||||
}}
|
||||
scale={[0.3, 0.3, 0.3]}
|
||||
// occlude
|
||||
>
|
||||
<UI temperature={drieUIValue.temperature} humidity={drieUIValue.humidity} touch={drieUIValue.touch} header={""} />
|
||||
</Html>
|
||||
</mesh>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
141
app/src/modules/visualization/mqttTemp/ui.jsx
Normal file
141
app/src/modules/visualization/mqttTemp/ui.jsx
Normal file
@@ -0,0 +1,141 @@
|
||||
export default function UI({ temperature, humidity, touch, header }) {
|
||||
return (
|
||||
<div
|
||||
className="temp-visualization-wrapper"
|
||||
style={{
|
||||
padding: "24px",
|
||||
width: "fit-content",
|
||||
background: "white",
|
||||
borderRadius: "20px",
|
||||
color: "#282829",
|
||||
// transform: "translate(0, -100%)"
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="header"
|
||||
style={{ paddingBottom: "22px", fontWeight: "600" }}
|
||||
>
|
||||
{header ? header : "Sensor Details"}
|
||||
</div>
|
||||
<div className="container-1" style={{ display: "flex", gap: "24px" }}>
|
||||
<div
|
||||
className="temperature-container"
|
||||
style={{
|
||||
padding: "12px",
|
||||
borderRadius: "12px",
|
||||
background: "white",
|
||||
boxShadow: "7px 7px 14px #e3e3e3, -7px -7px 14px #f4f4f4",
|
||||
display: "flex",
|
||||
gap: "6px",
|
||||
flexDirection: "column",
|
||||
width: "92px",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", borderRadius: "12px" }}>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8.73109 11.6758L9 11.5357V11.2324V4C9 2.61929 10.1193 1.5 11.5 1.5C12.8807 1.5 14 2.61929 14 4V11.2324V11.5357L14.2689 11.6758C16.1901 12.6771 17.5 14.6861 17.5 17.0002C17.5 20.3139 14.8137 23.0002 11.5 23.0002C8.18629 23.0002 5.5 20.3139 5.5 17.0002C5.5 14.6861 6.80994 12.6771 8.73109 11.6758Z"
|
||||
stroke="#FE4519"
|
||||
/>
|
||||
<path d="M11.5 7V16" stroke="#FE4519" strokeLinecap="round" />
|
||||
<circle cx="11.5" cy="17" r="3" fill="#FE4519" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="key" style={{ fontSize: "12px" }}>
|
||||
Temperature
|
||||
</div>
|
||||
<div
|
||||
className="value"
|
||||
style={{ fontSize: "18px", fontWeight: "600" }}
|
||||
>
|
||||
{temperature}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="humidity-container"
|
||||
style={{
|
||||
padding: "12px",
|
||||
borderRadius: "12px",
|
||||
background: "white",
|
||||
boxShadow: "7px 7px 14px #e3e3e3, -7px -7px 14px #f4f4f4",
|
||||
display: "flex",
|
||||
gap: "6px",
|
||||
flexDirection: "column",
|
||||
width: "92px",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", borderRadius: "12px" }}>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M14.8041 19.1765C15.2714 17.4843 14.6826 15.891 12.6962 13.7257C12.3217 13.3175 11.6786 13.3192 11.305 13.7284C9.1738 16.0628 8.77326 17.5784 9.16555 19.0737C9.32805 19.6931 9.79837 20.1765 10.3593 20.4854C11.742 21.2468 12.2655 21.3361 13.7514 20.4639C14.2463 20.1734 14.6514 19.7296 14.8041 19.1765Z"
|
||||
stroke="#0F96F5"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
<path
|
||||
d="M20.8104 9.0293C21.2043 7.39129 20.5932 5.82808 18.6645 3.72574C18.2899 3.31747 17.6469 3.3192 17.2733 3.72838C15.1959 6.00386 14.7629 7.50129 15.1056 8.96027C15.2684 9.65314 15.8159 10.18 16.4679 10.4655C17.7279 11.0173 18.291 11.0385 19.5446 10.4598C20.1511 10.1799 20.6542 9.6787 20.8104 9.0293Z"
|
||||
stroke="#0F96F5"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
<path
|
||||
d="M8.81041 9.0293C9.20431 7.39129 8.59319 5.82808 6.66448 3.72574C6.28992 3.31747 5.64687 3.3192 5.27331 3.72838C3.19591 6.00386 2.76287 7.50129 3.1056 8.96027C3.26837 9.65314 3.81593 10.18 4.46789 10.4655C5.72785 11.0173 6.29105 11.0385 7.54464 10.4598C8.15106 10.1799 8.65424 9.6787 8.81041 9.0293Z"
|
||||
stroke="#0F96F5"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="key" style={{ fontSize: "12px" }}>
|
||||
Humidity
|
||||
</div>
|
||||
<div
|
||||
className="value"
|
||||
style={{ fontSize: "18px", fontWeight: "600" }}
|
||||
>
|
||||
{humidity}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="container-2">
|
||||
<div
|
||||
className="touch-container"
|
||||
style={{
|
||||
display: "flex",
|
||||
borderRadius: "12px",
|
||||
background: "white",
|
||||
boxShadow: "7px 7px 14px #e3e3e3, -7px -7px 14px #f4f4f4",
|
||||
padding: "16px",
|
||||
marginTop: "16px",
|
||||
gap: "18px",
|
||||
alignItems: "center",
|
||||
fontWeight: "600",
|
||||
}}
|
||||
>
|
||||
<div className="key" style={{ fontSize: "14px" }}>
|
||||
Touch Sensor
|
||||
</div>
|
||||
<div
|
||||
className="value"
|
||||
style={
|
||||
touch === "True"
|
||||
? { color: "#2AA553", fontWeight: 500 }
|
||||
: { color: "#FE4519", fontWeight: 500 }
|
||||
}
|
||||
>
|
||||
{touch === "True" ? "Active" : "In active"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
297
app/src/modules/visualization/socket/realTimeVizSocket.dev.tsx
Normal file
297
app/src/modules/visualization/socket/realTimeVizSocket.dev.tsx
Normal file
@@ -0,0 +1,297 @@
|
||||
import { useEffect } from "react";
|
||||
import { useSocketStore } from "../../../store/builder/store";
|
||||
import { useSelectedZoneStore } from "../../../store/visualization/useZoneStore";
|
||||
import { useDroppedObjectsStore } from "../../../store/visualization/useDroppedObjectsStore";
|
||||
import { useZoneWidgetStore } from "../../../store/visualization/useZone3DWidgetStore";
|
||||
import useTemplateStore from "../../../store/useTemplateStore";
|
||||
|
||||
type WidgetData = {
|
||||
id: string;
|
||||
type: string;
|
||||
position: [number, number, number];
|
||||
rotation?: [number, number, number];
|
||||
tempPosition?: [number, number, number];
|
||||
};
|
||||
|
||||
export default function SocketRealTimeViz() {
|
||||
const { visualizationSocket } = useSocketStore();
|
||||
const { setSelectedZone } = useSelectedZoneStore();
|
||||
const deleteObject = useDroppedObjectsStore((state) => state.deleteObject);
|
||||
const updateObjectPosition = useDroppedObjectsStore(
|
||||
(state) => state.updateObjectPosition
|
||||
);
|
||||
const { addWidget } = useZoneWidgetStore();
|
||||
const { removeTemplate } = useTemplateStore();
|
||||
const { setTemplates } = useTemplateStore();
|
||||
const {
|
||||
zoneWidgetData,
|
||||
setZoneWidgetData,
|
||||
updateWidgetPosition,
|
||||
updateWidgetRotation,
|
||||
} = useZoneWidgetStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (visualizationSocket) {
|
||||
//add panel response
|
||||
visualizationSocket.on("v1:viz-panel:response:add", (addPanel: any) => {
|
||||
if (addPanel.success) {
|
||||
let addPanelData = addPanel.data.data;
|
||||
setSelectedZone(addPanelData);
|
||||
}
|
||||
});
|
||||
//delete panel response
|
||||
visualizationSocket.on(
|
||||
"v1:viz-panel:response:delete",
|
||||
(deletePanel: any) => {
|
||||
if (deletePanel.success) {
|
||||
let deletePanelData = deletePanel.data.data;
|
||||
setSelectedZone(deletePanelData);
|
||||
}
|
||||
}
|
||||
);
|
||||
//clear Panel response
|
||||
visualizationSocket.on("v1:viz-panel:response:clear", (clearPanel: any) => {
|
||||
if (
|
||||
clearPanel.success &&
|
||||
clearPanel.message === "PanelWidgets cleared successfully"
|
||||
) {
|
||||
let clearPanelData = clearPanel.data.data;
|
||||
setSelectedZone(clearPanelData);
|
||||
}
|
||||
});
|
||||
//lock Panel response
|
||||
visualizationSocket.on("v1:viz-panel:response:locked", (lockPanel: any) => {
|
||||
if (
|
||||
lockPanel.success &&
|
||||
lockPanel.message === "locked panel updated successfully"
|
||||
) {
|
||||
let lockPanelData = lockPanel.data.data;
|
||||
setSelectedZone(lockPanelData);
|
||||
}
|
||||
});
|
||||
// add 2dWidget response
|
||||
visualizationSocket.on(
|
||||
"v1:viz-widget:response:updates",
|
||||
(add2dWidget: any) => {
|
||||
if (add2dWidget.success && add2dWidget.data) {
|
||||
setSelectedZone((prev) => {
|
||||
const isWidgetAlreadyAdded = prev.widgets.some(
|
||||
(widget) => widget.id === add2dWidget.data.widgetData.id
|
||||
);
|
||||
if (isWidgetAlreadyAdded) return prev; // Prevent duplicate addition
|
||||
return {
|
||||
...prev,
|
||||
zoneUuid: add2dWidget.data.zoneUuid,
|
||||
zoneName: add2dWidget.data.zoneName,
|
||||
widgets: [...prev.widgets, add2dWidget.data.widgetData], // Append new widget
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
//delete 2D Widget response
|
||||
visualizationSocket.on(
|
||||
"v1:viz-widget:response:delete",
|
||||
(deleteWidget: any) => {
|
||||
if (deleteWidget?.success && deleteWidget.data) {
|
||||
setSelectedZone((prevZone: any) => ({
|
||||
...prevZone,
|
||||
zoneUuid: deleteWidget.data.zoneUuid,
|
||||
zoneName: deleteWidget.data.zoneName,
|
||||
widgets: deleteWidget.data.widgetDeleteDatas, // Replace with new widget list
|
||||
}));
|
||||
}
|
||||
}
|
||||
);
|
||||
//add Floating Widget response
|
||||
visualizationSocket.on(
|
||||
"v1:viz-float:response:updates",
|
||||
(addFloatingWidget: any) => {
|
||||
console.log('addFloatingWidget: ', addFloatingWidget);
|
||||
if (addFloatingWidget.success) {
|
||||
if (
|
||||
addFloatingWidget.success &&
|
||||
addFloatingWidget.message === "FloatWidget created successfully"
|
||||
) {
|
||||
const state = useDroppedObjectsStore.getState();
|
||||
const zone = state.zones[addFloatingWidget.data.zoneName];
|
||||
if (!zone) {
|
||||
state.setZone(
|
||||
addFloatingWidget.data.zoneName,
|
||||
addFloatingWidget.data.zoneUuid
|
||||
);
|
||||
}
|
||||
const existingObjects = zone ? zone.objects : [];
|
||||
const newWidget = addFloatingWidget.data.widget;
|
||||
// ✅ Check if the widget ID already exists before adding
|
||||
const isAlreadyAdded = existingObjects.some(
|
||||
(obj) => obj.id === newWidget.id
|
||||
);
|
||||
if (isAlreadyAdded) {
|
||||
return; // Don't add the widget if it already exists
|
||||
}
|
||||
// Add widget only if it doesn't exist
|
||||
state.addObject(addFloatingWidget.data.zoneName, newWidget);
|
||||
}
|
||||
if (addFloatingWidget.message === "Widget updated successfully") {
|
||||
updateObjectPosition(
|
||||
addFloatingWidget.data.zoneName,
|
||||
addFloatingWidget.data.index,
|
||||
addFloatingWidget.data.position
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
//duplicate Floating Widget response
|
||||
visualizationSocket.on(
|
||||
"v1:viz-float:response:addDuplicate",
|
||||
(duplicateFloatingWidget: any) => {
|
||||
if (
|
||||
duplicateFloatingWidget.success &&
|
||||
duplicateFloatingWidget.message ===
|
||||
"duplicate FloatWidget created successfully"
|
||||
) {
|
||||
useDroppedObjectsStore.setState((state) => {
|
||||
const zone = state.zones[duplicateFloatingWidget.data.zoneName];
|
||||
if (!zone) return state; // Zone doesn't exist, return state as is
|
||||
const existingObjects = zone.objects;
|
||||
const newWidget = duplicateFloatingWidget.data.widget;
|
||||
// ✅ Check if the object with the same ID already exists
|
||||
const isAlreadyAdded = existingObjects.some(
|
||||
(obj) => obj.id === newWidget.id
|
||||
);
|
||||
if (isAlreadyAdded) {
|
||||
return state; // Don't update state if it's already there
|
||||
}
|
||||
return {
|
||||
zones: {
|
||||
...state.zones,
|
||||
[duplicateFloatingWidget.data.zoneName]: {
|
||||
...zone,
|
||||
objects: [...existingObjects, newWidget], // Append only if it's not a duplicate
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
//delete Floating Widget response
|
||||
visualizationSocket.on(
|
||||
"v1:viz-float:response:delete",
|
||||
(deleteFloatingWidget: any) => {
|
||||
if (deleteFloatingWidget.success) {
|
||||
deleteObject(
|
||||
deleteFloatingWidget.data.zoneName,
|
||||
deleteFloatingWidget.data.floatWidgetID
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
//add 3D Widget response
|
||||
visualizationSocket.on(
|
||||
"v1:viz-widget3D:response:add",
|
||||
(add3DWidget: any) => {
|
||||
if (add3DWidget.success) {
|
||||
if (add3DWidget.message === "Widget created successfully") {
|
||||
addWidget(add3DWidget.data.zoneUuid, add3DWidget.data.widget);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
//delete 3D Widget response
|
||||
visualizationSocket.on(
|
||||
"v1:viz-widget3D:response:delete",
|
||||
(delete3DWidget: any) => {
|
||||
// "3DWidget delete unsuccessfull"
|
||||
if (
|
||||
delete3DWidget.success &&
|
||||
delete3DWidget.message === "3DWidget delete successfull"
|
||||
) {
|
||||
const activeZoneWidgets =
|
||||
zoneWidgetData[delete3DWidget.data.zoneUuid] || [];
|
||||
setZoneWidgetData(
|
||||
delete3DWidget.data.zoneUuid,
|
||||
activeZoneWidgets.filter(
|
||||
(w: WidgetData) => w.id !== delete3DWidget.data.id
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
//update3D widget response
|
||||
visualizationSocket.on(
|
||||
"v1:viz-widget3D:response:modifyPositionRotation",
|
||||
(update3DWidget: any) => {
|
||||
if (
|
||||
update3DWidget.success &&
|
||||
update3DWidget.message === "widget update successfully"
|
||||
) {
|
||||
updateWidgetPosition(
|
||||
update3DWidget.data.zoneUuid,
|
||||
update3DWidget.data.widget.id,
|
||||
update3DWidget.data.widget.position
|
||||
);
|
||||
updateWidgetRotation(
|
||||
update3DWidget.data.zoneUuid,
|
||||
update3DWidget.data.widget.id,
|
||||
update3DWidget.data.widget.rotation
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
// add Template response
|
||||
visualizationSocket.on(
|
||||
"v1:viz-template:response:add",
|
||||
(addingTemplate: any) => {
|
||||
if (addingTemplate.success) {
|
||||
if (addingTemplate.message === "Template saved successfully") {
|
||||
setTemplates(addingTemplate.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
//load Template response
|
||||
visualizationSocket.on(
|
||||
"v1:viz-template:response:addTemplateZone",
|
||||
(loadTemplate: any) => {
|
||||
if (loadTemplate.success) {
|
||||
if (loadTemplate.message === "Template placed in Zone") {
|
||||
let template = loadTemplate.data.template;
|
||||
setSelectedZone({
|
||||
panelOrder: template.panelOrder,
|
||||
activeSides: Array.from(new Set(template.panelOrder)), // No merging with previous `activeSides`
|
||||
widgets: template.widgets,
|
||||
});
|
||||
useDroppedObjectsStore
|
||||
.getState()
|
||||
.setZone(template.zoneName, template.zoneUuid);
|
||||
|
||||
if (Array.isArray(template.floatingWidget)) {
|
||||
template.floatingWidget.forEach((val: any) => {
|
||||
useDroppedObjectsStore
|
||||
.getState()
|
||||
.addObject(template.zoneName, val);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
//delete Template response
|
||||
visualizationSocket.on(
|
||||
"v1:viz-template:response:delete",
|
||||
(deleteTemplate: any) => {
|
||||
if (deleteTemplate.success) {
|
||||
if (deleteTemplate.message === "Template deleted successfully") {
|
||||
removeTemplate(deleteTemplate.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}, [visualizationSocket]);
|
||||
|
||||
return <></>;
|
||||
}
|
||||
142
app/src/modules/visualization/template/Templates.tsx
Normal file
142
app/src/modules/visualization/template/Templates.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import { useEffect } from "react";
|
||||
import useTemplateStore from "../../../store/useTemplateStore";
|
||||
import { useSelectedZoneStore } from "../../../store/visualization/useZoneStore";
|
||||
import { useSocketStore } from "../../../store/builder/store";
|
||||
import { getTemplateData } from "../../../services/visulization/zone/getTemplate";
|
||||
import { useDroppedObjectsStore } from "../../../store/visualization/useDroppedObjectsStore";
|
||||
import RenameInput from "../../../components/ui/inputs/RenameInput";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
|
||||
const Templates = () => {
|
||||
const { templates, removeTemplate, setTemplates } = useTemplateStore();
|
||||
const { setSelectedZone, selectedZone } = useSelectedZoneStore();
|
||||
const { visualizationSocket } = useSocketStore();
|
||||
const { projectId } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
async function templateData() {
|
||||
try {
|
||||
const email = localStorage.getItem("email") ?? "";
|
||||
const organization = email?.split("@")[1]?.split(".")[0];
|
||||
let response = await getTemplateData(organization,projectId);
|
||||
setTemplates(response);
|
||||
} catch (error) {
|
||||
echo.error("Failed to fetching template data");
|
||||
console.error("Error fetching template data:", error);
|
||||
}
|
||||
}
|
||||
|
||||
templateData();
|
||||
}, []);
|
||||
|
||||
const handleDeleteTemplate = async (
|
||||
e: React.MouseEvent<HTMLButtonElement>,
|
||||
id: string
|
||||
) => {
|
||||
try {
|
||||
e.stopPropagation();
|
||||
const email = localStorage.getItem("email") ?? "";
|
||||
const organization = email?.split("@")[1]?.split(".")[0];
|
||||
const userId = localStorage.getItem("userId");
|
||||
let deleteTemplate = {
|
||||
organization: organization,
|
||||
templateID: id,
|
||||
userId,projectId
|
||||
};
|
||||
if (visualizationSocket) {
|
||||
visualizationSocket.emit(
|
||||
"v1:viz-template:deleteTemplate",
|
||||
deleteTemplate
|
||||
);
|
||||
}
|
||||
removeTemplate(id);
|
||||
} catch (error) {
|
||||
echo.error("Failed to delete template");
|
||||
console.error("Error deleting template:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoadTemplate = async (template: any) => {
|
||||
try {
|
||||
if (selectedZone.zoneName === "") return;
|
||||
const email = localStorage.getItem("email") ?? "";
|
||||
const organization = email?.split("@")[1]?.split(".")[0];
|
||||
const userId = localStorage.getItem("userId");
|
||||
|
||||
let loadingTemplate = {
|
||||
organization: organization,
|
||||
zoneUuid: selectedZone.zoneUuid,
|
||||
templateID: template.id,
|
||||
projectId,userId
|
||||
};
|
||||
|
||||
if (visualizationSocket) {
|
||||
visualizationSocket.emit("v1:viz-template:addToZone", loadingTemplate);
|
||||
}
|
||||
|
||||
setSelectedZone({
|
||||
panelOrder: template.panelOrder,
|
||||
activeSides: Array.from(new Set(template.panelOrder)), // No merging with previous `activeSides`
|
||||
widgets: template.widgets,
|
||||
});
|
||||
|
||||
useDroppedObjectsStore
|
||||
.getState()
|
||||
.setZone(selectedZone.zoneName, selectedZone.zoneUuid);
|
||||
|
||||
if (Array.isArray(template.floatingWidget)) {
|
||||
template.floatingWidget.forEach((val: any) => {
|
||||
useDroppedObjectsStore
|
||||
.getState()
|
||||
.addObject(selectedZone.zoneName, val);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
echo.error("Failed to load template");
|
||||
console.error("Error loading template:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="template-list">
|
||||
{templates.map((template, index) => (
|
||||
<div key={template.id} className="template-item">
|
||||
{template?.snapshot && (
|
||||
<div
|
||||
className="template-image-container"
|
||||
onClick={() => handleLoadTemplate(template)}
|
||||
>
|
||||
<img
|
||||
src={template.snapshot}
|
||||
alt={`${template.name} preview`}
|
||||
className="template-image"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="template-details">
|
||||
<div className="template-name">
|
||||
<RenameInput value={`Template ${index + 1}`} />
|
||||
</div>
|
||||
<button
|
||||
id="template-delete-button"
|
||||
onClick={(e) => handleDeleteTemplate(e, template.id)}
|
||||
className="delete-button"
|
||||
aria-label="Delete template"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{templates.length === 0 && (
|
||||
<div className="no-templates">
|
||||
<h2>No saved templates yet.</h2>
|
||||
<div className="content">Create one in the visualization view!</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Templates;
|
||||
25
app/src/modules/visualization/visualization.tsx
Normal file
25
app/src/modules/visualization/visualization.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from 'react'
|
||||
import Dropped3dWidgets from './widgets/3d/Dropped3dWidget'
|
||||
import ZoneCentreTarget from './zone/zoneCameraTarget'
|
||||
import ZoneAssets from './zone/zoneAssets'
|
||||
import MqttEvents from '../../services/factoryBuilder/mqtt/mqttEvents'
|
||||
|
||||
const Visualization:React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
|
||||
<Dropped3dWidgets />
|
||||
|
||||
<ZoneCentreTarget />
|
||||
|
||||
<ZoneAssets />
|
||||
|
||||
<MqttEvents />
|
||||
|
||||
{/* <DrieHtmlTemp /> */}
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Visualization;
|
||||
411
app/src/modules/visualization/widgets/2d/DraggableWidget.tsx
Normal file
411
app/src/modules/visualization/widgets/2d/DraggableWidget.tsx
Normal file
@@ -0,0 +1,411 @@
|
||||
import { useWidgetStore } from "../../../../store/useWidgetStore";
|
||||
import ProgressCard from "../2d/charts/ProgressCard";
|
||||
import PieGraphComponent from "../2d/charts/PieGraphComponent";
|
||||
import BarGraphComponent from "../2d/charts/BarGraphComponent";
|
||||
import LineGraphComponent from "../2d/charts/LineGraphComponent";
|
||||
import DoughnutGraphComponent from "../2d/charts/DoughnutGraphComponent";
|
||||
import PolarAreaGraphComponent from "../2d/charts/PolarAreaGraphComponent";
|
||||
import ProgressCard1 from "../2d/charts/ProgressCard1";
|
||||
import ProgressCard2 from "../2d/charts/ProgressCard2";
|
||||
import {
|
||||
DeleteIcon,
|
||||
DublicateIcon,
|
||||
KebabIcon,
|
||||
} from "../../../../components/icons/ExportCommonIcons";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useClickOutside } from "../../functions/handleWidgetsOuterClick";
|
||||
import { useSocketStore } from "../../../../store/builder/store";
|
||||
import { usePlayButtonStore } from "../../../../store/usePlayButtonStore";
|
||||
import OuterClick from "../../../../utils/outerClick";
|
||||
import useChartStore from "../../../../store/visualization/useChartStore";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
type Side = "top" | "bottom" | "left" | "right";
|
||||
|
||||
interface Widget {
|
||||
id: string;
|
||||
type: string;
|
||||
title: string;
|
||||
panel: Side;
|
||||
data: any;
|
||||
}
|
||||
|
||||
export const DraggableWidget = ({
|
||||
widget,
|
||||
hiddenPanels,
|
||||
index,
|
||||
onReorder,
|
||||
openKebabId,
|
||||
setOpenKebabId,
|
||||
selectedZone,
|
||||
setSelectedZone,
|
||||
}: {
|
||||
selectedZone: {
|
||||
zoneName: string;
|
||||
zoneUuid: string;
|
||||
activeSides: Side[];
|
||||
points: [];
|
||||
panelOrder: Side[];
|
||||
lockedPanels: Side[];
|
||||
widgets: Widget[];
|
||||
};
|
||||
setSelectedZone: React.Dispatch<
|
||||
React.SetStateAction<{
|
||||
zoneName: string;
|
||||
activeSides: Side[];
|
||||
panelOrder: Side[];
|
||||
points: [];
|
||||
lockedPanels: Side[];
|
||||
zoneUuid: string;
|
||||
zoneViewPortTarget: number[];
|
||||
zoneViewPortPosition: number[];
|
||||
widgets: {
|
||||
id: string;
|
||||
type: string;
|
||||
title: string;
|
||||
panel: Side;
|
||||
data: any;
|
||||
}[];
|
||||
}>
|
||||
>;
|
||||
|
||||
widget: any;
|
||||
hiddenPanels: string[];
|
||||
index: number;
|
||||
onReorder: (fromIndex: number, toIndex: number) => void;
|
||||
openKebabId: string | null;
|
||||
setOpenKebabId: (id: string | null) => void;
|
||||
}) => {
|
||||
const { visualizationSocket } = useSocketStore();
|
||||
const { selectedChartId, setSelectedChartId } = useWidgetStore();
|
||||
const [panelDimensions, setPanelDimensions] = useState<{
|
||||
[side in Side]?: { width: number; height: number };
|
||||
}>({});
|
||||
const { measurements, duration, name } = useChartStore();
|
||||
const { isPlaying } = usePlayButtonStore();
|
||||
|
||||
const [canvasDimensions, setCanvasDimensions] = useState({
|
||||
width: 0,
|
||||
height: 0,
|
||||
});
|
||||
const { projectId } = useParams();
|
||||
useEffect(() => {}, [measurements, duration, name]);
|
||||
const handlePointerDown = () => {
|
||||
if (selectedChartId?.id !== widget.id) {
|
||||
setSelectedChartId(widget);
|
||||
}
|
||||
};
|
||||
|
||||
const chartWidget = useRef<HTMLDivElement>(null);
|
||||
|
||||
const deleteSelectedChart = async () => {
|
||||
try {
|
||||
const email = localStorage.getItem("email") || "";
|
||||
const organization = email?.split("@")[1]?.split(".")[0];
|
||||
const userId = localStorage.getItem("userId");
|
||||
let deleteWidget = {
|
||||
zoneUuid: selectedZone.zoneUuid,
|
||||
widgetID: widget.id,
|
||||
organization: organization,
|
||||
projectId,userId
|
||||
};
|
||||
|
||||
if (visualizationSocket) {
|
||||
setSelectedChartId(null);
|
||||
visualizationSocket.emit("v1:viz-widget:delete", deleteWidget);
|
||||
}
|
||||
const updatedWidgets = selectedZone.widgets.filter(
|
||||
(w: Widget) => w.id !== widget.id
|
||||
);
|
||||
|
||||
setSelectedZone((prevZone: any) => ({
|
||||
...prevZone,
|
||||
widgets: updatedWidgets,
|
||||
}));
|
||||
setOpenKebabId(null);
|
||||
// const response = await deleteWidgetApi(widget.id, organization);
|
||||
// if (response?.message === "Widget deleted successfully") {
|
||||
// const updatedWidgets = selectedZone.widgets.filter(
|
||||
// (w: Widget) => w.id !== widget.id
|
||||
// );
|
||||
// setSelectedZone((prevZone: any) => ({
|
||||
// ...prevZone,
|
||||
// widgets: updatedWidgets,
|
||||
// }));
|
||||
// }
|
||||
} catch (error) {
|
||||
echo.error("Failued to dublicate widgeet");
|
||||
} finally {
|
||||
setOpenKebabId(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Calculate panel size
|
||||
const panelSize = Math.max(
|
||||
Math.min(canvasDimensions.width * 0.25, canvasDimensions.height * 0.25),
|
||||
170 // Min 170px
|
||||
);
|
||||
|
||||
const getCurrentWidgetCount = (panel: Side) =>
|
||||
selectedZone.widgets.filter((w) => w.panel === panel).length;
|
||||
// Calculate panel capacity
|
||||
|
||||
const calculatePanelCapacity = (panel: Side) => {
|
||||
const CHART_WIDTH = panelSize;
|
||||
const CHART_HEIGHT = panelSize;
|
||||
|
||||
const dimensions = panelDimensions[panel];
|
||||
if (!dimensions) {
|
||||
return panel === "top" || panel === "bottom" ? 5 : 3; // Fallback capacities
|
||||
}
|
||||
|
||||
return panel === "top" || panel === "bottom"
|
||||
? Math.max(1, Math.floor(dimensions.width / CHART_WIDTH))
|
||||
: Math.max(1, Math.floor(dimensions.height / CHART_HEIGHT));
|
||||
};
|
||||
|
||||
const isPanelFull = (panel: Side) => {
|
||||
const currentWidgetCount = getCurrentWidgetCount(panel);
|
||||
const panelCapacity = calculatePanelCapacity(panel);
|
||||
|
||||
return currentWidgetCount > panelCapacity;
|
||||
};
|
||||
|
||||
const duplicateWidget = async () => {
|
||||
try {
|
||||
const email = localStorage.getItem("email") || "";
|
||||
const organization = email?.split("@")[1]?.split(".")[0];
|
||||
const userId = localStorage.getItem("userId");
|
||||
|
||||
const duplicatedWidget: Widget = {
|
||||
...widget,
|
||||
title: name === "" ? widget.title : name,
|
||||
Data: {
|
||||
duration: duration,
|
||||
measurements: { ...measurements },
|
||||
},
|
||||
id: `${widget.id}-copy-${Date.now()}`,
|
||||
};
|
||||
|
||||
let duplicateWidget = {
|
||||
organization: organization,
|
||||
zoneUuid: selectedZone.zoneUuid,
|
||||
widget: duplicatedWidget,
|
||||
projectId,userId
|
||||
};
|
||||
|
||||
if (visualizationSocket) {
|
||||
visualizationSocket.emit("v1:viz-widget:add", duplicateWidget);
|
||||
}
|
||||
setSelectedZone((prevZone: any) => ({
|
||||
...prevZone,
|
||||
widgets: [...prevZone.widgets, duplicatedWidget],
|
||||
}));
|
||||
|
||||
// const response = await duplicateWidgetApi(selectedZone.zoneUuid, organization, duplicatedWidget);
|
||||
|
||||
// if (response?.message === "Widget created successfully") {
|
||||
// setSelectedZone((prevZone: any) => ({
|
||||
// ...prevZone,
|
||||
// widgets: [...prevZone.widgets, duplicatedWidget],
|
||||
// }));
|
||||
// }
|
||||
} catch (error) {
|
||||
echo.error("Failued to dublicate widgeet");
|
||||
} finally {
|
||||
setOpenKebabId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKebabClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
event.stopPropagation();
|
||||
if (openKebabId === widget.id) {
|
||||
setOpenKebabId(null);
|
||||
} else {
|
||||
setOpenKebabId(widget.id);
|
||||
}
|
||||
};
|
||||
|
||||
const widgetRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
widgetRef.current &&
|
||||
!widgetRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setOpenKebabId(null);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, [setOpenKebabId]);
|
||||
|
||||
const handleDragStart = (event: React.DragEvent<HTMLDivElement>) => {
|
||||
event.dataTransfer.setData("text/plain", index.toString()); // Store the index of the dragged widget
|
||||
};
|
||||
const handleDragEnter = (event: React.DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault(); // Allow drop
|
||||
};
|
||||
|
||||
const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault(); // Allow drop
|
||||
};
|
||||
|
||||
const handleDrop = (event: React.DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
const fromIndex = parseInt(event.dataTransfer.getData("text/plain"), 10); // Get the dragged widget's index
|
||||
const toIndex = index; // The index of the widget where the drop occurred
|
||||
if (fromIndex !== toIndex) {
|
||||
onReorder(fromIndex, toIndex); // Call the reorder function passed as a prop
|
||||
}
|
||||
};
|
||||
|
||||
// useClickOutside(chartWidget, () => {
|
||||
// setSelectedChartId(null);
|
||||
// });
|
||||
|
||||
// Track canvas dimensions
|
||||
|
||||
// Current: Two identical useEffect hooks for canvas dimensions
|
||||
// Remove the duplicate and keep only one
|
||||
useEffect(() => {
|
||||
const canvas = document.getElementById("real-time-vis-canvas");
|
||||
if (!canvas) return;
|
||||
|
||||
const updateCanvasDimensions = () => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
setCanvasDimensions({
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
});
|
||||
};
|
||||
|
||||
updateCanvasDimensions();
|
||||
const resizeObserver = new ResizeObserver(updateCanvasDimensions);
|
||||
resizeObserver.observe(canvas);
|
||||
|
||||
return () => resizeObserver.unobserve(canvas);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
draggable
|
||||
key={widget.id}
|
||||
className={`chart-container ${
|
||||
selectedChartId?.id === widget.id && !isPlaying && "activeChart"
|
||||
}`}
|
||||
onPointerDown={handlePointerDown}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={handleDrop}
|
||||
style={{
|
||||
width: ["top", "bottom"].includes(widget.panel)
|
||||
? `calc(${canvasDimensions.width}px / 6)`
|
||||
: undefined,
|
||||
height: ["left", "right"].includes(widget.panel)
|
||||
? `calc(${canvasDimensions.height - 15}px / 4)`
|
||||
: undefined,
|
||||
}}
|
||||
ref={chartWidget}
|
||||
onClick={() => {
|
||||
setSelectedChartId(widget);
|
||||
}}
|
||||
>
|
||||
{/* Kebab Icon */}
|
||||
<div className="icon kebab" onClick={handleKebabClick}>
|
||||
<KebabIcon />
|
||||
</div>
|
||||
|
||||
{/* Kebab Options */}
|
||||
{openKebabId === widget.id && (
|
||||
<div className="kebab-options" ref={widgetRef}>
|
||||
<div
|
||||
className={`edit btn ${
|
||||
isPanelFull(widget.panel) ? "btn-blur" : ""
|
||||
}`}
|
||||
onClick={duplicateWidget}
|
||||
>
|
||||
<div className="icon">
|
||||
<DublicateIcon />
|
||||
</div>
|
||||
<div className="label">Duplicate</div>
|
||||
</div>
|
||||
<div
|
||||
className="edit btn"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteSelectedChart();
|
||||
}}
|
||||
>
|
||||
<div className="icon">
|
||||
<DeleteIcon />
|
||||
</div>
|
||||
<div className="label">Delete</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Render charts based on widget type */}
|
||||
|
||||
{widget.type === "progress 1" && (
|
||||
<ProgressCard1 title={widget.title} id={widget.id} />
|
||||
)}
|
||||
{widget.type === "progress 2" && (
|
||||
<ProgressCard2 title={widget.title} id={widget.id} />
|
||||
)}
|
||||
{widget.type === "line" && (
|
||||
<LineGraphComponent
|
||||
id={widget.id}
|
||||
type={widget.type}
|
||||
title={widget.title}
|
||||
fontSize={widget.fontSize}
|
||||
fontWeight={widget.fontWeight}
|
||||
/>
|
||||
)}
|
||||
{widget.type === "bar" && (
|
||||
<BarGraphComponent
|
||||
id={widget.id}
|
||||
type={widget.type}
|
||||
title={widget.title}
|
||||
fontSize={widget.fontSize}
|
||||
fontWeight={widget.fontWeight}
|
||||
/>
|
||||
)}
|
||||
{widget.type === "pie" && (
|
||||
<PieGraphComponent
|
||||
id={widget.id}
|
||||
type={widget.type}
|
||||
title={widget.title}
|
||||
fontSize={widget.fontSize}
|
||||
fontWeight={widget.fontWeight}
|
||||
/>
|
||||
)}
|
||||
{widget.type === "doughnut" && (
|
||||
<DoughnutGraphComponent
|
||||
id={widget.id}
|
||||
type={widget.type}
|
||||
title={widget.title}
|
||||
fontSize={widget.fontSize}
|
||||
fontWeight={widget.fontWeight}
|
||||
/>
|
||||
)}
|
||||
{widget.type === "polarArea" && (
|
||||
<PolarAreaGraphComponent
|
||||
id={widget.id}
|
||||
type={widget.type}
|
||||
title={widget.title}
|
||||
fontSize={widget.fontSize}
|
||||
fontWeight={widget.fontWeight}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,228 @@
|
||||
|
||||
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { Bar } from "react-chartjs-2";
|
||||
import io from "socket.io-client";
|
||||
|
||||
import axios from "axios";
|
||||
import { useThemeStore } from "../../../../../store/useThemeStore";
|
||||
import { useWidgetStore } from "../../../../../store/useWidgetStore";
|
||||
import useChartStore from "../../../../../store/visualization/useChartStore";
|
||||
import { useSelectedZoneStore } from "../../../../../store/visualization/useZoneStore";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
interface ChartComponentProps {
|
||||
id: string;
|
||||
type: any;
|
||||
title: string;
|
||||
fontFamily?: string;
|
||||
fontSize?: string;
|
||||
fontWeight?: "Light" | "Regular" | "Bold";
|
||||
}
|
||||
|
||||
const BarGraphComponent = ({
|
||||
id,
|
||||
type,
|
||||
title,
|
||||
fontFamily,
|
||||
fontSize,
|
||||
fontWeight = "Regular",
|
||||
}: ChartComponentProps) => {
|
||||
const { themeColor } = useThemeStore();
|
||||
const {
|
||||
measurements: chartMeasurements,
|
||||
duration: chartDuration,
|
||||
name: widgetName,
|
||||
} = useChartStore();
|
||||
const [measurements, setmeasurements] = useState<any>({});
|
||||
const [duration, setDuration] = useState("1h");
|
||||
const [name, setName] = useState("Widget");
|
||||
const [chartData, setChartData] = useState<{
|
||||
labels: string[];
|
||||
datasets: any[];
|
||||
}>({
|
||||
labels: [],
|
||||
datasets: [],
|
||||
});
|
||||
const { selectedChartId } = useWidgetStore();
|
||||
const { selectedZone } = useSelectedZoneStore();
|
||||
const { projectId } = useParams();
|
||||
|
||||
|
||||
const iotApiUrl = process.env.REACT_APP_IOT_SOCKET_SERVER_URL;
|
||||
const email = localStorage.getItem("email") || "";
|
||||
const organization = email?.split("@")[1]?.split(".")[0];
|
||||
const defaultData = {
|
||||
labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
|
||||
datasets: [
|
||||
{
|
||||
label: "Dataset",
|
||||
data: [12, 19, 3, 5, 2, 3],
|
||||
backgroundColor: ["#6f42c1"],
|
||||
borderColor: "#b392f0",
|
||||
borderWidth: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
useEffect(() => { }, []);
|
||||
|
||||
// Memoize Theme Colors
|
||||
const buttonActionColor = useMemo(
|
||||
() => themeColor[0] || "#5c87df",
|
||||
[themeColor]
|
||||
);
|
||||
const buttonAbortColor = useMemo(
|
||||
() => themeColor[1] || "#ffffff",
|
||||
[themeColor]
|
||||
);
|
||||
|
||||
// Memoize Font Styling
|
||||
const chartFontWeightMap = useMemo(
|
||||
() => ({
|
||||
Light: "lighter" as const,
|
||||
Regular: "normal" as const,
|
||||
Bold: "bold" as const,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const fontSizeValue = useMemo(
|
||||
() => (fontSize ? parseInt(fontSize) : 12),
|
||||
[fontSize]
|
||||
);
|
||||
const fontWeightValue = useMemo(
|
||||
() => chartFontWeightMap[fontWeight],
|
||||
[fontWeight, chartFontWeightMap]
|
||||
);
|
||||
|
||||
const chartFontStyle = useMemo(
|
||||
() => ({
|
||||
family: fontFamily || "Arial",
|
||||
size: fontSizeValue,
|
||||
weight: fontWeightValue,
|
||||
}),
|
||||
[fontFamily, fontSizeValue, fontWeightValue]
|
||||
);
|
||||
|
||||
// Memoize Chart Options
|
||||
const options = useMemo(
|
||||
() => ({
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: name,
|
||||
font: chartFontStyle,
|
||||
},
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: {
|
||||
display: true, // This hides the x-axis labels
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
[title, chartFontStyle, name]
|
||||
);
|
||||
|
||||
// useEffect(() => {console.log(measurements);
|
||||
// },[measurements])
|
||||
|
||||
useEffect(() => {
|
||||
if (!iotApiUrl || !measurements || Object.keys(measurements).length === 0)
|
||||
return;
|
||||
|
||||
const socket = io(`http://${iotApiUrl}`);
|
||||
|
||||
const inputData = {
|
||||
measurements,
|
||||
duration,
|
||||
interval: 1000,
|
||||
};
|
||||
|
||||
const startStream = () => {
|
||||
socket.emit("lineInput", inputData);
|
||||
};
|
||||
|
||||
socket.on("connect", startStream);
|
||||
|
||||
socket.on("lineOutput", (response) => {
|
||||
const responseData = response.data;
|
||||
|
||||
// Extract timestamps and values
|
||||
const labels = responseData.time;
|
||||
const datasets = Object.keys(measurements).map((key) => {
|
||||
const measurement = measurements[key];
|
||||
const datasetKey = `${measurement.name}.${measurement.fields}`;
|
||||
return {
|
||||
label: datasetKey,
|
||||
data: responseData[datasetKey]?.values ?? [],
|
||||
backgroundColor: "#6f42c1",
|
||||
borderColor: "#b392f0",
|
||||
borderWidth: 1,
|
||||
};
|
||||
});
|
||||
|
||||
setChartData({ labels, datasets });
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket.off("lineOutput");
|
||||
socket.emit("stop_stream"); // Stop streaming when component unmounts
|
||||
socket.disconnect();
|
||||
};
|
||||
}, [measurements, duration, iotApiUrl]);
|
||||
|
||||
const fetchSavedInputes = async () => {
|
||||
if (id !== "") {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}/api/V1/widget/data?widgetID=${id}&zoneUuid=${selectedZone.zoneUuid}&projectId=${projectId}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: "Bearer <access_token>",
|
||||
"Content-Type": "application/json",
|
||||
token: localStorage.getItem("token") || "",
|
||||
refresh_token: localStorage.getItem("refreshToken") || "",
|
||||
},
|
||||
}
|
||||
);
|
||||
if (response.status === 200) {
|
||||
setmeasurements(response.data.Data.measurements);
|
||||
setDuration(response.data.Data.duration);
|
||||
setName(response.data.widgetName);
|
||||
} else {
|
||||
echo.error("Failed to fetch saved inputs");
|
||||
console.log("Unexpected response:", response);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("There was an error!", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchSavedInputes();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedChartId?.id === id) {
|
||||
fetchSavedInputes();
|
||||
}
|
||||
}, [chartMeasurements, chartDuration, widgetName]);
|
||||
|
||||
return (
|
||||
<Bar
|
||||
data={Object.keys(measurements).length > 0 ? chartData : defaultData}
|
||||
options={options}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default BarGraphComponent;
|
||||
@@ -0,0 +1,206 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { Doughnut } from "react-chartjs-2";
|
||||
import io from "socket.io-client";
|
||||
|
||||
import axios from "axios";
|
||||
import { useThemeStore } from "../../../../../store/useThemeStore";
|
||||
import { useWidgetStore } from "../../../../../store/useWidgetStore";
|
||||
import useChartStore from "../../../../../store/visualization/useChartStore";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useSelectedZoneStore } from "../../../../../store/visualization/useZoneStore";
|
||||
|
||||
interface ChartComponentProps {
|
||||
id: string;
|
||||
type: any;
|
||||
title: string;
|
||||
fontFamily?: string;
|
||||
fontSize?: string;
|
||||
fontWeight?: "Light" | "Regular" | "Bold";
|
||||
}
|
||||
|
||||
const DoughnutGraphComponent = ({
|
||||
id,
|
||||
type,
|
||||
title,
|
||||
fontFamily,
|
||||
fontSize,
|
||||
fontWeight = "Regular",
|
||||
}: ChartComponentProps) => {
|
||||
const { themeColor } = useThemeStore();
|
||||
const { measurements: chartMeasurements, duration: chartDuration, name: widgetName } = useChartStore();
|
||||
const [measurements, setmeasurements] = useState<any>({});
|
||||
const [duration, setDuration] = useState("1h")
|
||||
const [name, setName] = useState("Widget")
|
||||
const [chartData, setChartData] = useState<{ labels: string[]; datasets: any[] }>({
|
||||
labels: [],
|
||||
datasets: [],
|
||||
});
|
||||
const { selectedChartId } = useWidgetStore();
|
||||
const { selectedZone } = useSelectedZoneStore();
|
||||
const { projectId } = useParams();
|
||||
|
||||
|
||||
const iotApiUrl = process.env.REACT_APP_IOT_SOCKET_SERVER_URL;
|
||||
const email = localStorage.getItem("email") || "";
|
||||
const organization = email?.split("@")[1]?.split(".")[0]
|
||||
const defaultData = {
|
||||
labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
|
||||
datasets: [
|
||||
{
|
||||
label: "Dataset",
|
||||
data: [12, 19, 3, 5, 2, 3],
|
||||
backgroundColor: ["#6f42c1"],
|
||||
borderColor: "#b392f0",
|
||||
borderWidth: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
},[])
|
||||
|
||||
// Memoize Theme Colors
|
||||
const buttonActionColor = useMemo(() => themeColor[0] || "#5c87df", [themeColor]);
|
||||
const buttonAbortColor = useMemo(() => themeColor[1] || "#ffffff", [themeColor]);
|
||||
|
||||
// Memoize Font Styling
|
||||
const chartFontWeightMap = useMemo(
|
||||
() => ({
|
||||
Light: "lighter" as const,
|
||||
Regular: "normal" as const,
|
||||
Bold: "bold" as const,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const fontSizeValue = useMemo(() => (fontSize ? parseInt(fontSize) : 12), [fontSize]);
|
||||
const fontWeightValue = useMemo(() => chartFontWeightMap[fontWeight], [fontWeight, chartFontWeightMap]);
|
||||
|
||||
const chartFontStyle = useMemo(
|
||||
() => ({
|
||||
family: fontFamily || "Arial",
|
||||
size: fontSizeValue,
|
||||
weight: fontWeightValue,
|
||||
}),
|
||||
[fontFamily, fontSizeValue, fontWeightValue]
|
||||
);
|
||||
|
||||
// Memoize Chart Options
|
||||
const options = useMemo(
|
||||
() => ({
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: name,
|
||||
font: chartFontStyle,
|
||||
},
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
// x: {
|
||||
// ticks: {
|
||||
// display: true, // This hides the x-axis labels
|
||||
// },
|
||||
// },
|
||||
},
|
||||
}),
|
||||
[title, chartFontStyle, name]
|
||||
);
|
||||
|
||||
// useEffect(() => {console.log(measurements);
|
||||
// },[measurements])
|
||||
|
||||
useEffect(() => {
|
||||
if (!iotApiUrl || !measurements || Object.keys(measurements).length === 0) return;
|
||||
|
||||
const socket = io(`http://${iotApiUrl}`);
|
||||
|
||||
const inputData = {
|
||||
measurements,
|
||||
duration,
|
||||
interval: 1000,
|
||||
};
|
||||
|
||||
|
||||
const startStream = () => {
|
||||
socket.emit("lineInput", inputData);
|
||||
};
|
||||
|
||||
socket.on("connect", startStream);
|
||||
|
||||
socket.on("lineOutput", (response) => {
|
||||
const responseData = response.data;
|
||||
|
||||
// Extract timestamps and values
|
||||
const labels = responseData.time;
|
||||
const datasets = Object.keys(measurements).map((key) => {
|
||||
const measurement = measurements[key];
|
||||
const datasetKey = `${measurement.name}.${measurement.fields}`;
|
||||
return {
|
||||
label: datasetKey,
|
||||
data: responseData[datasetKey]?.values ?? [],
|
||||
backgroundColor: "#6f42c1",
|
||||
borderColor: "#b392f0",
|
||||
borderWidth: 1,
|
||||
};
|
||||
});
|
||||
|
||||
setChartData({ labels, datasets });
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket.off("lineOutput");
|
||||
socket.emit("stop_stream"); // Stop streaming when component unmounts
|
||||
socket.disconnect();
|
||||
};
|
||||
}, [measurements, duration, iotApiUrl]);
|
||||
|
||||
const fetchSavedInputes = async() => {
|
||||
|
||||
if (id !== "") {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}/api/V1/widget/data?widgetID=${id}&zoneUuid=${selectedZone.zoneUuid}&projectId=${projectId}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: "Bearer <access_token>",
|
||||
"Content-Type": "application/json",
|
||||
token: localStorage.getItem("token") || "",
|
||||
refresh_token: localStorage.getItem("refreshToken") || "",
|
||||
},
|
||||
}
|
||||
);
|
||||
if (response.status === 200) {
|
||||
setmeasurements(response.data.Data.measurements)
|
||||
setDuration(response.data.Data.duration)
|
||||
setName(response.data.widgetName)
|
||||
} else {
|
||||
console.log("Unexpected response:", response);
|
||||
}
|
||||
} catch (error) {
|
||||
echo.error("Failed to fetch saved inputs");
|
||||
console.error("There was an error!", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchSavedInputes();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedChartId?.id === id) {
|
||||
fetchSavedInputes();
|
||||
}
|
||||
}
|
||||
,[chartMeasurements, chartDuration, widgetName])
|
||||
|
||||
return <Doughnut data={Object.keys(measurements).length > 0 ? chartData : defaultData} options={options} />;
|
||||
};
|
||||
|
||||
export default DoughnutGraphComponent;
|
||||
@@ -0,0 +1,226 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { Line } from "react-chartjs-2";
|
||||
import io from "socket.io-client";
|
||||
|
||||
import axios from "axios";
|
||||
import { useThemeStore } from "../../../../../store/useThemeStore";
|
||||
import useChartStore from "../../../../../store/visualization/useChartStore";
|
||||
import { useWidgetStore } from "../../../../../store/useWidgetStore";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useSelectedZoneStore } from "../../../../../store/visualization/useZoneStore";
|
||||
|
||||
interface ChartComponentProps {
|
||||
id: string;
|
||||
type: any;
|
||||
title: string;
|
||||
fontFamily?: string;
|
||||
fontSize?: string;
|
||||
fontWeight?: "Light" | "Regular" | "Bold";
|
||||
}
|
||||
|
||||
const LineGraphComponent = ({
|
||||
id,
|
||||
type,
|
||||
title,
|
||||
fontFamily,
|
||||
fontSize,
|
||||
fontWeight = "Regular",
|
||||
}: ChartComponentProps) => {
|
||||
const { themeColor } = useThemeStore();
|
||||
const {
|
||||
measurements: chartMeasurements,
|
||||
duration: chartDuration,
|
||||
name: widgetName,
|
||||
} = useChartStore();
|
||||
const [measurements, setmeasurements] = useState<any>({});
|
||||
const [duration, setDuration] = useState("1h");
|
||||
const [name, setName] = useState("Widget");
|
||||
const [chartData, setChartData] = useState<{
|
||||
labels: string[];
|
||||
datasets: any[];
|
||||
}>({
|
||||
labels: [],
|
||||
datasets: [],
|
||||
});
|
||||
const { selectedChartId } = useWidgetStore();
|
||||
const { selectedZone } = useSelectedZoneStore();
|
||||
const { projectId } = useParams();
|
||||
|
||||
|
||||
const iotApiUrl = process.env.REACT_APP_IOT_SOCKET_SERVER_URL;
|
||||
const email = localStorage.getItem("email") || "";
|
||||
const organization = email?.split("@")[1]?.split(".")[0];
|
||||
const defaultData = {
|
||||
labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
|
||||
datasets: [
|
||||
{
|
||||
label: "Dataset",
|
||||
data: [12, 19, 3, 5, 2, 3],
|
||||
backgroundColor: ["#6f42c1"],
|
||||
borderColor: "#b392f0",
|
||||
borderWidth: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
useEffect(() => {}, []);
|
||||
|
||||
// Memoize Theme Colors
|
||||
const buttonActionColor = useMemo(
|
||||
() => themeColor[0] || "#5c87df",
|
||||
[themeColor]
|
||||
);
|
||||
const buttonAbortColor = useMemo(
|
||||
() => themeColor[1] || "#ffffff",
|
||||
[themeColor]
|
||||
);
|
||||
|
||||
// Memoize Font Styling
|
||||
const chartFontWeightMap = useMemo(
|
||||
() => ({
|
||||
Light: "lighter" as const,
|
||||
Regular: "normal" as const,
|
||||
Bold: "bold" as const,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const fontSizeValue = useMemo(
|
||||
() => (fontSize ? parseInt(fontSize) : 12),
|
||||
[fontSize]
|
||||
);
|
||||
const fontWeightValue = useMemo(
|
||||
() => chartFontWeightMap[fontWeight],
|
||||
[fontWeight, chartFontWeightMap]
|
||||
);
|
||||
|
||||
const chartFontStyle = useMemo(
|
||||
() => ({
|
||||
family: fontFamily || "Arial",
|
||||
size: fontSizeValue,
|
||||
weight: fontWeightValue,
|
||||
}),
|
||||
[fontFamily, fontSizeValue, fontWeightValue]
|
||||
);
|
||||
|
||||
// Memoize Chart Options
|
||||
const options = useMemo(
|
||||
() => ({
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: name,
|
||||
font: chartFontStyle,
|
||||
},
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: {
|
||||
display: true, // This hides the x-axis labels
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
[title, chartFontStyle, name]
|
||||
);
|
||||
|
||||
// useEffect(() => {console.log(measurements);
|
||||
// },[measurements])
|
||||
|
||||
useEffect(() => {
|
||||
if (!iotApiUrl || !measurements || Object.keys(measurements).length === 0)
|
||||
return;
|
||||
|
||||
const socket = io(`http://${iotApiUrl}`);
|
||||
|
||||
const inputData = {
|
||||
measurements,
|
||||
duration,
|
||||
interval: 1000,
|
||||
};
|
||||
|
||||
const startStream = () => {
|
||||
socket.emit("lineInput", inputData);
|
||||
};
|
||||
|
||||
socket.on("connect", startStream);
|
||||
|
||||
socket.on("lineOutput", (response) => {
|
||||
const responseData = response.data;
|
||||
|
||||
// Extract timestamps and values
|
||||
const labels = responseData.time;
|
||||
const datasets = Object.keys(measurements).map((key) => {
|
||||
const measurement = measurements[key];
|
||||
const datasetKey = `${measurement.name}.${measurement.fields}`;
|
||||
return {
|
||||
label: datasetKey,
|
||||
data: responseData[datasetKey]?.values ?? [],
|
||||
backgroundColor: "#6f42c1",
|
||||
borderColor: "#b392f0",
|
||||
borderWidth: 1,
|
||||
};
|
||||
});
|
||||
|
||||
setChartData({ labels, datasets });
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket.off("lineOutput");
|
||||
socket.emit("stop_stream"); // Stop streaming when component unmounts
|
||||
socket.disconnect();
|
||||
};
|
||||
}, [measurements, duration, iotApiUrl]);
|
||||
|
||||
const fetchSavedInputes = async () => {
|
||||
if (id !== "") {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}/api/V1/widget/data?widgetID=${id}&zoneUuid=${selectedZone.zoneUuid}&projectId=${projectId}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: "Bearer <access_token>",
|
||||
"Content-Type": "application/json",
|
||||
token: localStorage.getItem("token") || "",
|
||||
refresh_token: localStorage.getItem("refreshToken") || "",
|
||||
},
|
||||
}
|
||||
);
|
||||
if (response.status === 200) {
|
||||
setmeasurements(response.data.Data.measurements);
|
||||
setDuration(response.data.Data.duration);
|
||||
setName(response.data.widgetName);
|
||||
} else {
|
||||
echo.error("Failed to fetch saved inputs");
|
||||
console.log("Unexpected response:", response);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("There was an error!", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchSavedInputes();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedChartId?.id === id) {
|
||||
fetchSavedInputes();
|
||||
}
|
||||
}, [chartMeasurements, chartDuration, widgetName]);
|
||||
|
||||
return (
|
||||
<Line
|
||||
data={Object.keys(measurements).length > 0 ? chartData : defaultData}
|
||||
options={options}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default LineGraphComponent;
|
||||
@@ -0,0 +1,225 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { Pie } from "react-chartjs-2";
|
||||
import io from "socket.io-client";
|
||||
|
||||
import axios from "axios";
|
||||
import { useThemeStore } from "../../../../../store/useThemeStore";
|
||||
import useChartStore from "../../../../../store/visualization/useChartStore";
|
||||
import { useWidgetStore } from "../../../../../store/useWidgetStore";
|
||||
import { useSelectedZoneStore } from "../../../../../store/visualization/useZoneStore";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
interface ChartComponentProps {
|
||||
id: string;
|
||||
type: any;
|
||||
title: string;
|
||||
fontFamily?: string;
|
||||
fontSize?: string;
|
||||
fontWeight?: "Light" | "Regular" | "Bold";
|
||||
}
|
||||
|
||||
const PieChartComponent = ({
|
||||
id,
|
||||
type,
|
||||
title,
|
||||
fontFamily,
|
||||
fontSize,
|
||||
fontWeight = "Regular",
|
||||
}: ChartComponentProps) => {
|
||||
const { themeColor } = useThemeStore();
|
||||
const {
|
||||
measurements: chartMeasurements,
|
||||
duration: chartDuration,
|
||||
name: widgetName,
|
||||
} = useChartStore();
|
||||
const [measurements, setmeasurements] = useState<any>({});
|
||||
const [duration, setDuration] = useState("1h");
|
||||
const [name, setName] = useState("Widget");
|
||||
const [chartData, setChartData] = useState<{
|
||||
labels: string[];
|
||||
datasets: any[];
|
||||
}>({
|
||||
labels: [],
|
||||
datasets: [],
|
||||
});
|
||||
const { selectedChartId } = useWidgetStore();
|
||||
const { selectedZone } = useSelectedZoneStore();
|
||||
const { projectId } = useParams();
|
||||
|
||||
const iotApiUrl = process.env.REACT_APP_IOT_SOCKET_SERVER_URL;
|
||||
const email = localStorage.getItem("email") || "";
|
||||
const organization = email?.split("@")[1]?.split(".")[0];
|
||||
const defaultData = {
|
||||
labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
|
||||
datasets: [
|
||||
{
|
||||
label: "Dataset",
|
||||
data: [12, 19, 3, 5, 2, 3],
|
||||
backgroundColor: ["#6f42c1"],
|
||||
borderColor: "#b392f0",
|
||||
borderWidth: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
useEffect(() => {}, []);
|
||||
|
||||
// Memoize Theme Colors
|
||||
const buttonActionColor = useMemo(
|
||||
() => themeColor[0] || "#5c87df",
|
||||
[themeColor]
|
||||
);
|
||||
const buttonAbortColor = useMemo(
|
||||
() => themeColor[1] || "#ffffff",
|
||||
[themeColor]
|
||||
);
|
||||
|
||||
// Memoize Font Styling
|
||||
const chartFontWeightMap = useMemo(
|
||||
() => ({
|
||||
Light: "lighter" as const,
|
||||
Regular: "normal" as const,
|
||||
Bold: "bold" as const,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const fontSizeValue = useMemo(
|
||||
() => (fontSize ? parseInt(fontSize) : 12),
|
||||
[fontSize]
|
||||
);
|
||||
const fontWeightValue = useMemo(
|
||||
() => chartFontWeightMap[fontWeight],
|
||||
[fontWeight, chartFontWeightMap]
|
||||
);
|
||||
|
||||
const chartFontStyle = useMemo(
|
||||
() => ({
|
||||
family: fontFamily || "Arial",
|
||||
size: fontSizeValue,
|
||||
weight: fontWeightValue,
|
||||
}),
|
||||
[fontFamily, fontSizeValue, fontWeightValue]
|
||||
);
|
||||
|
||||
// Memoize Chart Options
|
||||
const options = useMemo(
|
||||
() => ({
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: name,
|
||||
font: chartFontStyle,
|
||||
},
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
// x: {
|
||||
// ticks: {
|
||||
// display: true, // This hides the x-axis labels
|
||||
// },
|
||||
// },
|
||||
},
|
||||
}),
|
||||
[title, chartFontStyle, name]
|
||||
);
|
||||
|
||||
// useEffect(() => {console.log(measurements);
|
||||
// },[measurements])
|
||||
|
||||
useEffect(() => {
|
||||
if (!iotApiUrl || !measurements || Object.keys(measurements).length === 0)
|
||||
return;
|
||||
|
||||
const socket = io(`http://${iotApiUrl}`);
|
||||
|
||||
const inputData = {
|
||||
measurements,
|
||||
duration,
|
||||
interval: 1000,
|
||||
};
|
||||
|
||||
const startStream = () => {
|
||||
socket.emit("lineInput", inputData);
|
||||
};
|
||||
|
||||
socket.on("connect", startStream);
|
||||
|
||||
socket.on("lineOutput", (response) => {
|
||||
const responseData = response.data;
|
||||
|
||||
// Extract timestamps and values
|
||||
const labels = responseData.time;
|
||||
const datasets = Object.keys(measurements).map((key) => {
|
||||
const measurement = measurements[key];
|
||||
const datasetKey = `${measurement.name}.${measurement.fields}`;
|
||||
return {
|
||||
label: datasetKey,
|
||||
data: responseData[datasetKey]?.values ?? [],
|
||||
backgroundColor: "#6f42c1",
|
||||
borderColor: "#b392f0",
|
||||
borderWidth: 1,
|
||||
};
|
||||
});
|
||||
|
||||
setChartData({ labels, datasets });
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket.off("lineOutput");
|
||||
socket.emit("stop_stream"); // Stop streaming when component unmounts
|
||||
socket.disconnect();
|
||||
};
|
||||
}, [measurements, duration, iotApiUrl]);
|
||||
|
||||
const fetchSavedInputes = async () => {
|
||||
if (id !== "") {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}/api/V1/widget/data?widgetID=${id}&zoneUuid=${selectedZone.zoneUuid}&projectId=${projectId}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: "Bearer <access_token>",
|
||||
"Content-Type": "application/json",
|
||||
token: localStorage.getItem("token") || "",
|
||||
refresh_token: localStorage.getItem("refreshToken") || "",
|
||||
},
|
||||
}
|
||||
);
|
||||
if (response.status === 200) {
|
||||
setmeasurements(response.data.Data.measurements);
|
||||
setDuration(response.data.Data.duration);
|
||||
setName(response.data.widgetName);
|
||||
} else {
|
||||
echo.error("Failed to fetch saved inputs");
|
||||
console.log("Unexpected response:", response);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("There was an error!", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchSavedInputes();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedChartId?.id === id) {
|
||||
fetchSavedInputes();
|
||||
}
|
||||
}, [chartMeasurements, chartDuration, widgetName]);
|
||||
|
||||
return (
|
||||
<Pie
|
||||
data={Object.keys(measurements).length > 0 ? chartData : defaultData}
|
||||
options={options}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PieChartComponent;
|
||||
@@ -0,0 +1,225 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { PolarArea } from "react-chartjs-2";
|
||||
import io from "socket.io-client";
|
||||
|
||||
import axios from "axios";
|
||||
import { useThemeStore } from "../../../../../store/useThemeStore";
|
||||
import useChartStore from "../../../../../store/visualization/useChartStore";
|
||||
import { useWidgetStore } from "../../../../../store/useWidgetStore";
|
||||
import { useSelectedZoneStore } from "../../../../../store/visualization/useZoneStore";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
interface ChartComponentProps {
|
||||
id: string;
|
||||
type: any;
|
||||
title: string;
|
||||
fontFamily?: string;
|
||||
fontSize?: string;
|
||||
fontWeight?: "Light" | "Regular" | "Bold";
|
||||
}
|
||||
|
||||
const PolarAreaGraphComponent = ({
|
||||
id,
|
||||
type,
|
||||
title,
|
||||
fontFamily,
|
||||
fontSize,
|
||||
fontWeight = "Regular",
|
||||
}: ChartComponentProps) => {
|
||||
const { themeColor } = useThemeStore();
|
||||
const {
|
||||
measurements: chartMeasurements,
|
||||
duration: chartDuration,
|
||||
name: widgetName,
|
||||
} = useChartStore();
|
||||
const [measurements, setmeasurements] = useState<any>({});
|
||||
const [duration, setDuration] = useState("1h");
|
||||
const [name, setName] = useState("Widget");
|
||||
const [chartData, setChartData] = useState<{
|
||||
labels: string[];
|
||||
datasets: any[];
|
||||
}>({
|
||||
labels: [],
|
||||
datasets: [],
|
||||
});
|
||||
const { selectedChartId } = useWidgetStore();
|
||||
const { selectedZone } = useSelectedZoneStore();
|
||||
const { projectId } = useParams();
|
||||
|
||||
const iotApiUrl = process.env.REACT_APP_IOT_SOCKET_SERVER_URL;
|
||||
const email = localStorage.getItem("email") || "";
|
||||
const organization = email?.split("@")[1]?.split(".")[0];
|
||||
const defaultData = {
|
||||
labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
|
||||
datasets: [
|
||||
{
|
||||
label: "Dataset",
|
||||
data: [12, 19, 3, 5, 2, 3],
|
||||
backgroundColor: ["#6f42c1"],
|
||||
borderColor: "#b392f0",
|
||||
borderWidth: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
useEffect(() => {}, []);
|
||||
|
||||
// Memoize Theme Colors
|
||||
const buttonActionColor = useMemo(
|
||||
() => themeColor[0] || "#5c87df",
|
||||
[themeColor]
|
||||
);
|
||||
const buttonAbortColor = useMemo(
|
||||
() => themeColor[1] || "#ffffff",
|
||||
[themeColor]
|
||||
);
|
||||
|
||||
// Memoize Font Styling
|
||||
const chartFontWeightMap = useMemo(
|
||||
() => ({
|
||||
Light: "lighter" as const,
|
||||
Regular: "normal" as const,
|
||||
Bold: "bold" as const,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const fontSizeValue = useMemo(
|
||||
() => (fontSize ? parseInt(fontSize) : 12),
|
||||
[fontSize]
|
||||
);
|
||||
const fontWeightValue = useMemo(
|
||||
() => chartFontWeightMap[fontWeight],
|
||||
[fontWeight, chartFontWeightMap]
|
||||
);
|
||||
|
||||
const chartFontStyle = useMemo(
|
||||
() => ({
|
||||
family: fontFamily || "Arial",
|
||||
size: fontSizeValue,
|
||||
weight: fontWeightValue,
|
||||
}),
|
||||
[fontFamily, fontSizeValue, fontWeightValue]
|
||||
);
|
||||
|
||||
// Memoize Chart Options
|
||||
const options = useMemo(
|
||||
() => ({
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: name,
|
||||
font: chartFontStyle,
|
||||
},
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
// x: {
|
||||
// ticks: {
|
||||
// display: true, // This hides the x-axis labels
|
||||
// },
|
||||
// },
|
||||
},
|
||||
}),
|
||||
[title, chartFontStyle, name]
|
||||
);
|
||||
|
||||
// useEffect(() => {console.log(measurements);
|
||||
// },[measurements])
|
||||
|
||||
useEffect(() => {
|
||||
if (!iotApiUrl || !measurements || Object.keys(measurements).length === 0)
|
||||
return;
|
||||
|
||||
const socket = io(`http://${iotApiUrl}`);
|
||||
|
||||
const inputData = {
|
||||
measurements,
|
||||
duration,
|
||||
interval: 1000,
|
||||
};
|
||||
|
||||
const startStream = () => {
|
||||
socket.emit("lineInput", inputData);
|
||||
};
|
||||
|
||||
socket.on("connect", startStream);
|
||||
|
||||
socket.on("lineOutput", (response) => {
|
||||
const responseData = response.data;
|
||||
|
||||
// Extract timestamps and values
|
||||
const labels = responseData.time;
|
||||
const datasets = Object.keys(measurements).map((key) => {
|
||||
const measurement = measurements[key];
|
||||
const datasetKey = `${measurement.name}.${measurement.fields}`;
|
||||
return {
|
||||
label: datasetKey,
|
||||
data: responseData[datasetKey]?.values ?? [],
|
||||
backgroundColor: "#6f42c1",
|
||||
borderColor: "#b392f0",
|
||||
borderWidth: 1,
|
||||
};
|
||||
});
|
||||
|
||||
setChartData({ labels, datasets });
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket.off("lineOutput");
|
||||
socket.emit("stop_stream"); // Stop streaming when component unmounts
|
||||
socket.disconnect();
|
||||
};
|
||||
}, [measurements, duration, iotApiUrl]);
|
||||
|
||||
const fetchSavedInputes = async () => {
|
||||
if (id !== "") {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}/api/V1/widget/data?widgetID=${id}&zoneUuid=${selectedZone.zoneUuid}&projectId=${projectId}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: "Bearer <access_token>",
|
||||
"Content-Type": "application/json",
|
||||
token: localStorage.getItem("token") || "",
|
||||
refresh_token: localStorage.getItem("refreshToken") || "",
|
||||
},
|
||||
}
|
||||
);
|
||||
if (response.status === 200) {
|
||||
setmeasurements(response.data.Data.measurements);
|
||||
setDuration(response.data.Data.duration);
|
||||
setName(response.data.widgetName);
|
||||
} else {
|
||||
echo.error("Failed to fetch saved inputs");
|
||||
console.log("Unexpected response:", response);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("There was an error!", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchSavedInputes();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedChartId?.id === id) {
|
||||
fetchSavedInputes();
|
||||
}
|
||||
}, [chartMeasurements, chartDuration, widgetName]);
|
||||
|
||||
return (
|
||||
<PolarArea
|
||||
data={Object.keys(measurements).length > 0 ? chartData : defaultData}
|
||||
options={options}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PolarAreaGraphComponent;
|
||||
@@ -0,0 +1,29 @@
|
||||
import { StockIncreseIcon } from "../../../../../components/icons/RealTimeVisulationIcons";
|
||||
|
||||
const ProgressCard = ({
|
||||
title,
|
||||
data,
|
||||
}: {
|
||||
title: string;
|
||||
data: { stocks: Array<{ key: string; value: number; description: string }> };
|
||||
}) => (
|
||||
<div className="chart progressBar">
|
||||
<div className="header">{title}</div>
|
||||
{data?.stocks?.map((stock, index) => (
|
||||
<div key={index} className="stock">
|
||||
<span className="stock-item">
|
||||
<span className="stockValues">
|
||||
<div className="key">{stock.key}</div>
|
||||
<div className="value">{stock.value}</div>
|
||||
</span>
|
||||
<div className="stock-description">{stock.description}</div>
|
||||
</span>
|
||||
<div className="icon">
|
||||
<StockIncreseIcon />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default ProgressCard;
|
||||
@@ -0,0 +1,119 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { Line } from "react-chartjs-2";
|
||||
import io from "socket.io-client";
|
||||
|
||||
import axios from "axios";
|
||||
import useChartStore from "../../../../../store/visualization/useChartStore";
|
||||
import { useWidgetStore } from "../../../../../store/useWidgetStore";
|
||||
import { StockIncreseIcon } from "../../../../../components/icons/RealTimeVisulationIcons";
|
||||
import { useSelectedZoneStore } from "../../../../../store/visualization/useZoneStore";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
const ProgressCard1 = ({ id, title }: { id: string; title: string }) => {
|
||||
const {
|
||||
measurements: chartMeasurements,
|
||||
duration: chartDuration,
|
||||
name: widgetName,
|
||||
} = useChartStore();
|
||||
const [measurements, setmeasurements] = useState<any>({});
|
||||
const [duration, setDuration] = useState("1h");
|
||||
const [name, setName] = useState(title);
|
||||
const [value, setValue] = useState<any>("");
|
||||
const { selectedChartId } = useWidgetStore();
|
||||
const { selectedZone } = useSelectedZoneStore();
|
||||
const { projectId } = useParams();
|
||||
|
||||
const iotApiUrl = process.env.REACT_APP_IOT_SOCKET_SERVER_URL;
|
||||
const email = localStorage.getItem("email") || "";
|
||||
const organization = email?.split("@")[1]?.split(".")[0];
|
||||
|
||||
useEffect(() => {
|
||||
const socket = io(`http://${iotApiUrl}`);
|
||||
if (!iotApiUrl || !measurements || Object.keys(measurements).length === 0)
|
||||
return;
|
||||
|
||||
const inputData = {
|
||||
measurements,
|
||||
duration,
|
||||
interval: 1000,
|
||||
};
|
||||
|
||||
const startStream = () => {
|
||||
socket.emit("lastInput", inputData);
|
||||
};
|
||||
|
||||
socket.on("connect", startStream);
|
||||
|
||||
socket.on("lastOutput", (response) => {
|
||||
const responseData = response.input1;
|
||||
setValue(responseData);
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket.off("lastOutput");
|
||||
socket.emit("stop_stream"); // Stop streaming when component unmounts
|
||||
socket.disconnect();
|
||||
};
|
||||
}, [measurements, duration]);
|
||||
|
||||
const fetchSavedInputes = async () => {
|
||||
if (id !== "") {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}/api/V1/widget/data?widgetID=${id}&zoneUuid=${selectedZone.zoneUuid}&projectId=${projectId}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: "Bearer <access_token>",
|
||||
"Content-Type": "application/json",
|
||||
token: localStorage.getItem("token") || "",
|
||||
refresh_token: localStorage.getItem("refreshToken") || "",
|
||||
},
|
||||
}
|
||||
);
|
||||
if (response.status === 200) {
|
||||
setmeasurements(response.data.Data.measurements);
|
||||
setDuration(response.data.Data.duration);
|
||||
setName(response.data.widgetName);
|
||||
} else {
|
||||
echo.error("Failed to fetch saved inputs");
|
||||
console.log("Unexpected response:", response);
|
||||
}
|
||||
} catch (error) {
|
||||
echo.error("Failed to fetch saved inputs");
|
||||
console.error("There was an error!", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchSavedInputes();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedChartId?.id === id) {
|
||||
fetchSavedInputes();
|
||||
}
|
||||
}, [chartMeasurements, chartDuration, widgetName]);
|
||||
|
||||
return (
|
||||
<div className="chart progressBar">
|
||||
<div className="header">{name}</div>
|
||||
<div className="stock">
|
||||
<span className="stock-item">
|
||||
<span className="stockValues">
|
||||
<div className="value">{value}</div>
|
||||
<div className="key">Units</div>
|
||||
</span>
|
||||
<div className="stock-description">
|
||||
{measurements ? `${measurements?.input1?.fields}` : "description"}
|
||||
</div>
|
||||
</span>
|
||||
<div className="icon">
|
||||
<StockIncreseIcon />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProgressCard1;
|
||||
@@ -0,0 +1,138 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { Line } from "react-chartjs-2";
|
||||
import io from "socket.io-client";
|
||||
|
||||
import axios from "axios";
|
||||
import useChartStore from "../../../../../store/visualization/useChartStore";
|
||||
import { useWidgetStore } from "../../../../../store/useWidgetStore";
|
||||
import { StockIncreseIcon } from "../../../../../components/icons/RealTimeVisulationIcons";
|
||||
import { useSelectedZoneStore } from "../../../../../store/visualization/useZoneStore";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
const ProgressCard2 = ({ id, title }: { id: string; title: string }) => {
|
||||
const {
|
||||
measurements: chartMeasurements,
|
||||
duration: chartDuration,
|
||||
name: widgetName,
|
||||
} = useChartStore();
|
||||
const [measurements, setmeasurements] = useState<any>({});
|
||||
const [duration, setDuration] = useState("1h");
|
||||
const [name, setName] = useState(title);
|
||||
const [value1, setValue1] = useState<any>("");
|
||||
const [value2, setValue2] = useState<any>("");
|
||||
const { selectedChartId } = useWidgetStore();
|
||||
const { selectedZone } = useSelectedZoneStore();
|
||||
const { projectId } = useParams();
|
||||
|
||||
const iotApiUrl = process.env.REACT_APP_IOT_SOCKET_SERVER_URL;
|
||||
const email = localStorage.getItem("email") || "";
|
||||
const organization = email?.split("@")[1]?.split(".")[0];
|
||||
|
||||
useEffect(() => {
|
||||
if (!iotApiUrl || !measurements || Object.keys(measurements).length === 0)
|
||||
return;
|
||||
|
||||
const socket = io(`http://${iotApiUrl}`);
|
||||
|
||||
const inputData = {
|
||||
measurements,
|
||||
duration,
|
||||
interval: 1000,
|
||||
};
|
||||
|
||||
const startStream = () => {
|
||||
socket.emit("lastInput", inputData);
|
||||
};
|
||||
|
||||
socket.on("connect", startStream);
|
||||
|
||||
socket.on("lastOutput", (response) => {
|
||||
const responseData1 = response.input1;
|
||||
const responseData2 = response.input2;
|
||||
setValue1(responseData1);
|
||||
setValue2(responseData2);
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket.off("lastOutput");
|
||||
socket.emit("stop_stream"); // Stop streaming when component unmounts
|
||||
socket.disconnect();
|
||||
};
|
||||
}, [measurements, duration]);
|
||||
|
||||
const fetchSavedInputes = async () => {
|
||||
if (id !== "") {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}/api/V1/widget/data?widgetID=${id}&zoneUuid=${selectedZone.zoneUuid}&projectId=${projectId}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: "Bearer <access_token>",
|
||||
"Content-Type": "application/json",
|
||||
token: localStorage.getItem("token") || "",
|
||||
refresh_token: localStorage.getItem("refreshToken") || "",
|
||||
},
|
||||
}
|
||||
);
|
||||
if (response.status === 200) {
|
||||
setmeasurements(response.data.Data.measurements);
|
||||
setDuration(response.data.Data.duration);
|
||||
setName(response.data.widgetName);
|
||||
} else {
|
||||
console.log("Unexpected response:", response);
|
||||
}
|
||||
} catch (error) {
|
||||
echo.error("Failed to fetch saved inputs");
|
||||
console.error("There was an error!", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchSavedInputes();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedChartId?.id === id) {
|
||||
fetchSavedInputes();
|
||||
}
|
||||
}, [chartMeasurements, chartDuration, widgetName]);
|
||||
|
||||
return (
|
||||
<div className="chart progressBar">
|
||||
<div className="header">{name}</div>
|
||||
|
||||
<div className="stock">
|
||||
<span className="stock-item">
|
||||
<span className="stockValues">
|
||||
<div className="value">{value1}</div>
|
||||
<div className="key">Units</div>
|
||||
</span>
|
||||
<div className="stock-description">
|
||||
{measurements ? `${measurements?.input1?.fields}` : "description"}
|
||||
</div>
|
||||
</span>
|
||||
<div className="icon">
|
||||
<StockIncreseIcon />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="stock">
|
||||
<span className="stock-item">
|
||||
<span className="stockValues">
|
||||
<div className="value">{value2}</div>
|
||||
<div className="key">Units</div>
|
||||
</span>
|
||||
<div className="stock-description">
|
||||
{measurements ? `${measurements?.input2?.fields}` : "description"}
|
||||
</div>
|
||||
</span>
|
||||
<div className="icon">
|
||||
<StockIncreseIcon />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProgressCard2;
|
||||
@@ -0,0 +1,102 @@
|
||||
import React, { useMemo } from "react";
|
||||
import { Radar } from "react-chartjs-2";
|
||||
import { ChartOptions, ChartData, RadialLinearScaleOptions } from "chart.js";
|
||||
|
||||
interface ChartComponentProps {
|
||||
type: string;
|
||||
title: string;
|
||||
fontFamily?: string;
|
||||
fontSize?: string;
|
||||
fontWeight?: "Light" | "Regular" | "Bold";
|
||||
data: number[]; // Expecting an array of numbers for radar chart data
|
||||
}
|
||||
|
||||
const RadarGraphComponent = ({
|
||||
title,
|
||||
fontFamily,
|
||||
fontSize,
|
||||
fontWeight = "Regular",
|
||||
data, // Now guaranteed to be number[]
|
||||
}: ChartComponentProps) => {
|
||||
// Memoize Font Weight Mapping
|
||||
const chartFontWeightMap = useMemo(
|
||||
() => ({
|
||||
Light: "lighter" as const,
|
||||
Regular: "normal" as const,
|
||||
Bold: "bold" as const,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const fontSizeValue = useMemo(
|
||||
() => (fontSize ? parseInt(fontSize) : 12),
|
||||
[fontSize]
|
||||
);
|
||||
|
||||
const fontWeightValue = useMemo(
|
||||
() => chartFontWeightMap[fontWeight],
|
||||
[fontWeight, chartFontWeightMap]
|
||||
);
|
||||
|
||||
const chartFontStyle = useMemo(
|
||||
() => ({
|
||||
family: fontFamily || "Arial",
|
||||
size: fontSizeValue,
|
||||
weight: fontWeightValue,
|
||||
}),
|
||||
[fontFamily, fontSizeValue, fontWeightValue]
|
||||
);
|
||||
|
||||
const options: ChartOptions<"radar"> = useMemo(
|
||||
() => ({
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: title,
|
||||
font: chartFontStyle,
|
||||
},
|
||||
legend: {
|
||||
display: false,
|
||||
position: "top",
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
r: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
angleLines: {
|
||||
display: true,
|
||||
},
|
||||
ticks: {
|
||||
display: true,
|
||||
stepSize: 20,
|
||||
},
|
||||
} as RadialLinearScaleOptions,
|
||||
},
|
||||
}),
|
||||
[title, chartFontStyle]
|
||||
);
|
||||
|
||||
const chartData: ChartData<"radar"> = useMemo(
|
||||
() => ({
|
||||
labels: ["January", "February", "March", "April", "May", "June", "July"],
|
||||
datasets: [
|
||||
{
|
||||
label: "Dataset 1",
|
||||
data, // Use the data passed as a prop
|
||||
backgroundColor: "rgba(111, 66, 193, 0.2)",
|
||||
borderColor: "#6f42c1",
|
||||
borderWidth: 2,
|
||||
fill: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
[data]
|
||||
);
|
||||
|
||||
return <Radar data={chartData} options={options} />;
|
||||
};
|
||||
|
||||
export default RadarGraphComponent;
|
||||
828
app/src/modules/visualization/widgets/3d/Dropped3dWidget.tsx
Normal file
828
app/src/modules/visualization/widgets/3d/Dropped3dWidget.tsx
Normal file
@@ -0,0 +1,828 @@
|
||||
import { useThree } from "@react-three/fiber";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import * as THREE from "three";
|
||||
import {
|
||||
useAsset3dWidget,
|
||||
useSocketStore,
|
||||
useWidgetSubOption,
|
||||
} from "../../../../store/builder/store";
|
||||
import useModuleStore from "../../../../store/useModuleStore";
|
||||
import { ThreeState } from "../../../../types/world/worldTypes";
|
||||
import { useSelectedZoneStore } from "../../../../store/visualization/useZoneStore";
|
||||
import {
|
||||
useEditWidgetOptionsStore,
|
||||
useLeftData,
|
||||
useRightClickSelected,
|
||||
useRightSelected,
|
||||
useTopData,
|
||||
useZoneWidgetStore,
|
||||
} from "../../../../store/visualization/useZone3DWidgetStore";
|
||||
import { use3DWidget } from "../../../../store/visualization/useDroppedObjectsStore";
|
||||
import { get3dWidgetZoneData } from "../../../../services/visulization/zone/get3dWidgetData";
|
||||
import { generateUniqueId } from "../../../../functions/generateUniqueId";
|
||||
import ProductionCapacity from "./cards/ProductionCapacity";
|
||||
import ReturnOfInvestment from "./cards/ReturnOfInvestment";
|
||||
import StateWorking from "./cards/StateWorking";
|
||||
import Throughput from "./cards/Throughput";
|
||||
import { useWidgetStore } from "../../../../store/useWidgetStore";
|
||||
import useChartStore from "../../../../store/visualization/useChartStore";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
type WidgetData = {
|
||||
id: string;
|
||||
type: string;
|
||||
position: [number, number, number];
|
||||
rotation?: [number, number, number];
|
||||
tempPosition?: [number, number, number];
|
||||
};
|
||||
|
||||
export default function Dropped3dWidgets() {
|
||||
const { widgetSelect } = useAsset3dWidget();
|
||||
const { activeModule } = useModuleStore();
|
||||
const { raycaster, gl, scene, mouse, camera }: ThreeState = useThree();
|
||||
const { widgetSubOption } = useWidgetSubOption();
|
||||
const { selectedZone } = useSelectedZoneStore();
|
||||
let lastClientY = useRef<number | null>(null);
|
||||
const { top, setTop } = useTopData();
|
||||
const { left, setLeft } = useLeftData();
|
||||
const { rightSelect, setRightSelect } = useRightSelected();
|
||||
const { editWidgetOptions, setEditWidgetOptions } =
|
||||
useEditWidgetOptionsStore();
|
||||
const {
|
||||
zoneWidgetData,
|
||||
setZoneWidgetData,
|
||||
addWidget,
|
||||
updateWidgetPosition,
|
||||
updateWidgetRotation,
|
||||
tempWidget,
|
||||
tempWidgetPosition,
|
||||
} = useZoneWidgetStore();
|
||||
const { setWidgets3D } = use3DWidget();
|
||||
const { visualizationSocket } = useSocketStore();
|
||||
const { rightClickSelected, setRightClickSelected } = useRightClickSelected();
|
||||
const plane = useRef(new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)); // Floor plane for horizontal move
|
||||
const verticalPlane = useRef(new THREE.Plane(new THREE.Vector3(0, 0, 1), 0)); // Vertical plane for vertical move
|
||||
const planeIntersect = useRef(new THREE.Vector3());
|
||||
const rotationStartRef = useRef<[number, number, number]>([0, 0, 0]);
|
||||
const mouseStartRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
|
||||
const { setSelectedChartId } = useWidgetStore();
|
||||
const { measurements, duration } = useChartStore();
|
||||
let [floorPlanesVertical, setFloorPlanesVertical] = useState(
|
||||
new THREE.Plane(new THREE.Vector3(0, 1, 0))
|
||||
);
|
||||
const [intersectcontextmenu, setintersectcontextmenu] = useState<
|
||||
number | undefined
|
||||
>();
|
||||
const [horizontalX, setHorizontalX] = useState<number | undefined>();
|
||||
const [horizontalZ, setHorizontalZ] = useState<number | undefined>();
|
||||
|
||||
const activeZoneWidgets = zoneWidgetData[selectedZone.zoneUuid] || [];
|
||||
const { projectId } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
if (activeModule !== "visualization") return;
|
||||
if (!selectedZone.zoneUuid) return;
|
||||
|
||||
const email = localStorage.getItem("email") || "";
|
||||
const organization = email?.split("@")[1]?.split(".")[0];
|
||||
|
||||
async function get3dWidgetData() {
|
||||
const result = await get3dWidgetZoneData(
|
||||
selectedZone.zoneUuid,
|
||||
organization,
|
||||
projectId
|
||||
);
|
||||
|
||||
setWidgets3D(result);
|
||||
if (result.length < 0) return;
|
||||
|
||||
const formattedWidgets = result?.map((widget: WidgetData) => ({
|
||||
id: widget.id,
|
||||
type: widget.type,
|
||||
position: widget.position,
|
||||
rotation: widget.rotation || [0, 0, 0],
|
||||
}));
|
||||
|
||||
setZoneWidgetData(selectedZone.zoneUuid, formattedWidgets);
|
||||
}
|
||||
|
||||
get3dWidgetData();
|
||||
}, [selectedZone.zoneUuid, activeModule]);
|
||||
|
||||
const createdWidgetRef = useRef<WidgetData | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeModule !== "visualization") return;
|
||||
if (widgetSubOption === "Floating" || widgetSubOption === "2D") return;
|
||||
if (selectedZone.zoneName === "") return;
|
||||
|
||||
const canvasElement = document.getElementById("work-space-three-d-canvas");
|
||||
|
||||
if (!canvasElement) return;
|
||||
|
||||
const hasEntered = { current: false };
|
||||
|
||||
const handleDragEnter = (event: DragEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (hasEntered.current || !widgetSelect.startsWith("ui")) return;
|
||||
hasEntered.current = true;
|
||||
|
||||
const group1 = scene.getObjectByName("itemsGroup");
|
||||
if (!group1) return;
|
||||
|
||||
const rect = canvasElement.getBoundingClientRect();
|
||||
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
||||
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
||||
raycaster.setFromCamera(mouse, camera);
|
||||
|
||||
const intersects = raycaster
|
||||
.intersectObjects(scene.children, true)
|
||||
.filter(
|
||||
(intersect) =>
|
||||
!intersect.object.name.includes("Roof") &&
|
||||
!intersect.object.name.includes("MeasurementReference") &&
|
||||
!intersect.object.name.includes("agv-collider") &&
|
||||
!intersect.object.name.includes("zonePlane") &&
|
||||
!intersect.object.name.includes("SelectionGroup") &&
|
||||
!intersect.object.name.includes("selectionAssetGroup") &&
|
||||
!intersect.object.name.includes("SelectionGroupBoundingBoxLine") &&
|
||||
!intersect.object.name.includes("SelectionGroupBoundingBox") &&
|
||||
!intersect.object.name.includes("SelectionGroupBoundingLine") &&
|
||||
intersect.object.type !== "GridHelper"
|
||||
);
|
||||
|
||||
if (intersects.length > 0) {
|
||||
const { x, y, z } = intersects[0].point;
|
||||
const newWidget: WidgetData = {
|
||||
id: generateUniqueId(),
|
||||
type: widgetSelect,
|
||||
position: [x, y, z],
|
||||
rotation: [0, 0, 0],
|
||||
};
|
||||
|
||||
createdWidgetRef.current = newWidget;
|
||||
tempWidget(selectedZone.zoneUuid, newWidget); // temp add in UI
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragOver = (event: DragEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
event.dataTransfer!.dropEffect = "move"; // ✅ Add this line
|
||||
const widget = createdWidgetRef.current;
|
||||
if (!widget) return;
|
||||
|
||||
const rect = canvasElement.getBoundingClientRect();
|
||||
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
||||
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
||||
raycaster.setFromCamera(mouse, camera);
|
||||
|
||||
const intersects = raycaster
|
||||
.intersectObjects(scene.children, true)
|
||||
.filter(
|
||||
(intersect) =>
|
||||
!intersect.object.name.includes("Roof") &&
|
||||
!intersect.object.name.includes("MeasurementReference") &&
|
||||
!intersect.object.name.includes("agv-collider") &&
|
||||
!intersect.object.name.includes("zonePlane") &&
|
||||
!intersect.object.name.includes("SelectionGroup") &&
|
||||
!intersect.object.name.includes("selectionAssetGroup") &&
|
||||
!intersect.object.name.includes("SelectionGroupBoundingBoxLine") &&
|
||||
!intersect.object.name.includes("SelectionGroupBoundingBox") &&
|
||||
!intersect.object.name.includes("SelectionGroupBoundingLine") &&
|
||||
intersect.object.type !== "GridHelper"
|
||||
);
|
||||
// Update widget's position in memory
|
||||
if (intersects.length > 0) {
|
||||
const { x, y, z } = intersects[0].point;
|
||||
tempWidgetPosition(selectedZone.zoneUuid, widget.id, [x, y, z]);
|
||||
widget.position = [x, y, z];
|
||||
}
|
||||
};
|
||||
|
||||
const onDrop = (event: any) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
hasEntered.current = false;
|
||||
|
||||
const email = localStorage.getItem("email") || "";
|
||||
const organization = email?.split("@")[1]?.split(".")[0];
|
||||
const userId = localStorage.getItem("userId");
|
||||
|
||||
const newWidget = createdWidgetRef.current;
|
||||
if (!newWidget || !widgetSelect.startsWith("ui")) return;
|
||||
|
||||
// ✅ Extract 2D drop position
|
||||
let [x, y, z] = newWidget.position;
|
||||
|
||||
// ✅ Clamp Y to at least 0
|
||||
y = Math.max(y, 0);
|
||||
newWidget.position = [x, y, z];
|
||||
|
||||
// ✅ Prepare polygon from selectedZone.points
|
||||
const points3D = selectedZone.points as Array<[number, number, number]>;
|
||||
const zonePolygonXZ = points3D.map(
|
||||
([x, , z]) => [x, z] as [number, number]
|
||||
);
|
||||
|
||||
const isInside = isPointInPolygon([x, z], zonePolygonXZ);
|
||||
|
||||
// ✅ Remove temp widget
|
||||
const prevWidgets =
|
||||
useZoneWidgetStore.getState().zoneWidgetData[selectedZone.zoneUuid] || [];
|
||||
const cleanedWidgets = prevWidgets.filter((w) => w.id !== newWidget.id);
|
||||
useZoneWidgetStore.setState((state) => ({
|
||||
zoneWidgetData: {
|
||||
...state.zoneWidgetData,
|
||||
[selectedZone.zoneUuid]: cleanedWidgets,
|
||||
},
|
||||
}));
|
||||
|
||||
// (Optional) Prevent adding if dropped outside zone
|
||||
// if (!isInside) {
|
||||
// createdWidgetRef.current = null;
|
||||
// return;
|
||||
// }
|
||||
|
||||
// ✅ Add widget
|
||||
addWidget(selectedZone.zoneUuid, newWidget);
|
||||
|
||||
const add3dWidget = {
|
||||
organization,
|
||||
widget: newWidget,
|
||||
zoneUuid: selectedZone.zoneUuid,
|
||||
projectId, userId
|
||||
};
|
||||
|
||||
if (visualizationSocket) {
|
||||
visualizationSocket.emit("v1:viz-3D-widget:add", add3dWidget);
|
||||
}
|
||||
|
||||
createdWidgetRef.current = null;
|
||||
};
|
||||
|
||||
canvasElement.addEventListener("dragenter", handleDragEnter);
|
||||
canvasElement.addEventListener("dragover", handleDragOver);
|
||||
canvasElement.addEventListener("drop", onDrop);
|
||||
|
||||
return () => {
|
||||
canvasElement.removeEventListener("dragenter", handleDragEnter);
|
||||
canvasElement.removeEventListener("dragover", handleDragOver);
|
||||
canvasElement.removeEventListener("drop", onDrop);
|
||||
};
|
||||
}, [
|
||||
widgetSelect,
|
||||
activeModule,
|
||||
selectedZone.zoneUuid,
|
||||
widgetSubOption,
|
||||
camera,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!rightClickSelected) return;
|
||||
const email = localStorage.getItem("email") || "";
|
||||
const organization = email?.split("@")[1]?.split(".")[0];
|
||||
const userId = localStorage.getItem("userId");
|
||||
|
||||
if (rightSelect === "Duplicate") {
|
||||
async function duplicateWidget() {
|
||||
const widgetToDuplicate = activeZoneWidgets.find(
|
||||
(w: WidgetData) => w.id === rightClickSelected
|
||||
);
|
||||
console.log("3d widget to duplecate", widgetToDuplicate);
|
||||
|
||||
if (!widgetToDuplicate) return;
|
||||
const newWidget: any = {
|
||||
id: generateUniqueId(),
|
||||
type: widgetToDuplicate.type,
|
||||
position: [
|
||||
widgetToDuplicate.position[0] + 0.5,
|
||||
widgetToDuplicate.position[1],
|
||||
widgetToDuplicate.position[2] + 0.5,
|
||||
],
|
||||
rotation: widgetToDuplicate.rotation || [0, 0, 0],
|
||||
Data: {
|
||||
measurements: measurements,
|
||||
duration: duration,
|
||||
},
|
||||
};
|
||||
const adding3dWidget = {
|
||||
organization: organization,
|
||||
widget: newWidget,
|
||||
zoneUuid: selectedZone.zoneUuid,
|
||||
projectId, userId
|
||||
};
|
||||
if (visualizationSocket) {
|
||||
visualizationSocket.emit("v1:viz-3D-widget:add", adding3dWidget);
|
||||
}
|
||||
// let response = await adding3dWidgets(selectedZone.zoneUuid, organization, newWidget)
|
||||
//
|
||||
|
||||
addWidget(selectedZone.zoneUuid, newWidget);
|
||||
setRightSelect(null);
|
||||
setRightClickSelected(null);
|
||||
}
|
||||
duplicateWidget();
|
||||
}
|
||||
|
||||
if (rightSelect === "Delete") {
|
||||
const deleteWidgetApi = async () => {
|
||||
try {
|
||||
const deleteWidget = {
|
||||
organization,
|
||||
id: rightClickSelected,
|
||||
zoneUuid: selectedZone.zoneUuid,
|
||||
projectId, userId
|
||||
};
|
||||
|
||||
if (visualizationSocket) {
|
||||
visualizationSocket.emit("v1:viz-3D-widget:delete", deleteWidget);
|
||||
}
|
||||
// Call the API to delete the widget
|
||||
// const response = await delete3dWidgetApi(selectedZone.zoneUuid, organization, rightClickSelected);
|
||||
setZoneWidgetData(
|
||||
selectedZone.zoneUuid,
|
||||
activeZoneWidgets.filter(
|
||||
(w: WidgetData) => w.id !== rightClickSelected
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
echo.error("Failed to delete widget");
|
||||
} finally {
|
||||
setRightClickSelected(null);
|
||||
setRightSelect(null);
|
||||
}
|
||||
};
|
||||
|
||||
deleteWidgetApi();
|
||||
}
|
||||
}, [rightSelect, rightClickSelected]);
|
||||
|
||||
function isPointInPolygon(point: [number, number], polygon: Array<[number, number]>): boolean {
|
||||
const [x, z] = point;
|
||||
let inside = false;
|
||||
|
||||
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
||||
const [xi, zi] = polygon[i];
|
||||
const [xj, zj] = polygon[j];
|
||||
|
||||
const intersect =
|
||||
zi > z !== zj > z && x < ((xj - xi) * (z - zi)) / (zj - zi) + xi;
|
||||
|
||||
if (intersect) inside = !inside;
|
||||
}
|
||||
|
||||
return inside;
|
||||
}
|
||||
const [prevX, setPrevX] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const email = localStorage.getItem("email") || "";
|
||||
const organization = email?.split("@")[1]?.split(".")[0];
|
||||
const userId = localStorage.getItem("userId");
|
||||
const handleMouseDown = (event: MouseEvent) => {
|
||||
if (!rightClickSelected || !rightSelect) return;
|
||||
const selectedzoneUuid = Object.keys(zoneWidgetData).find(
|
||||
(zoneUuid: string) =>
|
||||
zoneWidgetData[zoneUuid].some(
|
||||
(widget: WidgetData) => widget.id === rightClickSelected
|
||||
)
|
||||
);
|
||||
if (!selectedzoneUuid) return;
|
||||
const selectedWidget = zoneWidgetData[selectedzoneUuid].find(
|
||||
(widget: WidgetData) => widget.id === rightClickSelected
|
||||
);
|
||||
if (!selectedWidget) return;
|
||||
// let points = [];
|
||||
// points.push(new THREE.Vector3(0, 0, 0));
|
||||
// points.push(new THREE.Vector3(0, selectedWidget.position[1], 0));
|
||||
// const newgeometry = new THREE.BufferGeometry().setFromPoints(points);
|
||||
// let vector = new THREE.Vector3();
|
||||
// camera.getWorldDirection(vector);
|
||||
// let cameraDirection = vector;
|
||||
// let newPlane = new THREE.Plane(cameraDirection);
|
||||
// floorPlanesVertical = newPlane;
|
||||
// setFloorPlanesVertical(newPlane);
|
||||
// const intersect1 = raycaster?.ray?.intersectPlane(
|
||||
// floorPlanesVertical,
|
||||
// planeIntersect.current
|
||||
// );
|
||||
|
||||
// setintersectcontextmenu(intersect1.y);
|
||||
|
||||
if (rightSelect === "RotateX" || rightSelect === "RotateY") {
|
||||
mouseStartRef.current = { x: event.clientX, y: event.clientY };
|
||||
|
||||
const selectedzoneUuid = Object.keys(zoneWidgetData).find(
|
||||
(zoneUuid: string) =>
|
||||
zoneWidgetData[zoneUuid].some(
|
||||
(widget: WidgetData) => widget.id === rightClickSelected
|
||||
)
|
||||
);
|
||||
if (!selectedzoneUuid) return;
|
||||
const selectedWidget = zoneWidgetData[selectedzoneUuid].find(
|
||||
(widget: WidgetData) => widget.id === rightClickSelected
|
||||
);
|
||||
if (selectedWidget) {
|
||||
rotationStartRef.current = selectedWidget.rotation || [0, 0, 0];
|
||||
}
|
||||
}
|
||||
};
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
if (!rightClickSelected || !rightSelect) return;
|
||||
const selectedzoneUuid = Object.keys(zoneWidgetData).find(
|
||||
(zoneUuid: string) =>
|
||||
zoneWidgetData[zoneUuid].some(
|
||||
(widget: WidgetData) => widget.id === rightClickSelected
|
||||
)
|
||||
);
|
||||
if (!selectedzoneUuid) return;
|
||||
|
||||
const selectedWidget = zoneWidgetData[selectedzoneUuid].find(
|
||||
(widget: WidgetData) => widget.id === rightClickSelected
|
||||
);
|
||||
if (!selectedWidget) return;
|
||||
|
||||
const rect = gl.domElement.getBoundingClientRect();
|
||||
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
||||
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
||||
|
||||
raycaster.setFromCamera(mouse, camera);
|
||||
|
||||
if (rightSelect === "Horizontal Move") {
|
||||
const intersect = raycaster.ray.intersectPlane(
|
||||
plane.current,
|
||||
planeIntersect.current
|
||||
);
|
||||
if (
|
||||
intersect &&
|
||||
typeof horizontalX === "number" &&
|
||||
typeof horizontalZ === "number"
|
||||
) {
|
||||
const selectedzoneUuid = Object.keys(zoneWidgetData).find((zoneUuid) =>
|
||||
zoneWidgetData[zoneUuid].some(
|
||||
(widget) => widget.id === rightClickSelected
|
||||
)
|
||||
);
|
||||
if (!selectedzoneUuid) return;
|
||||
|
||||
const selectedWidget = zoneWidgetData[selectedzoneUuid].find(
|
||||
(widget) => widget.id === rightClickSelected
|
||||
);
|
||||
if (!selectedWidget) return;
|
||||
|
||||
const newPosition: [number, number, number] = [
|
||||
intersect.x + horizontalX,
|
||||
selectedWidget.position[1],
|
||||
intersect.z + horizontalZ,
|
||||
];
|
||||
|
||||
updateWidgetPosition(selectedzoneUuid, rightClickSelected, newPosition);
|
||||
}
|
||||
}
|
||||
|
||||
// if (rightSelect === "Vertical Move") {
|
||||
// // console.log('rightSelect: ', rightSelect);
|
||||
|
||||
// // console.log('floorPlanesVertical: ', floorPlanesVertical);
|
||||
// // console.log('planeIntersect.current: ', planeIntersect.current);
|
||||
// // const intersect = raycaster.ray.intersectPlane(
|
||||
// // floorPlanesVertical,
|
||||
// // planeIntersect.current
|
||||
// // );
|
||||
// // console.log('intersect: ', intersect);
|
||||
|
||||
// let intersect = event.clientY
|
||||
|
||||
// if (intersect && typeof intersectcontextmenu === "number") {
|
||||
// console.log('intersect: ', intersect);
|
||||
// const diff = intersect - intersectcontextmenu;
|
||||
// const unclampedY = selectedWidget.position[1] + diff;
|
||||
// const newY = Math.max(0, unclampedY); // Prevent going below floor (y=0)
|
||||
|
||||
// setintersectcontextmenu(intersect);
|
||||
|
||||
// const newPosition: [number, number, number] = [
|
||||
// selectedWidget.position[0],
|
||||
// newY,
|
||||
// selectedWidget.position[2],
|
||||
// ];
|
||||
// console.log('newPosition: ', newPosition);
|
||||
|
||||
|
||||
// updateWidgetPosition(selectedzoneUuid, rightClickSelected, newPosition);
|
||||
// }
|
||||
// }
|
||||
if (rightSelect === "Vertical Move") {
|
||||
if (lastClientY.current === null) {
|
||||
lastClientY.current = event.clientY;
|
||||
return;
|
||||
}
|
||||
|
||||
const diff = lastClientY.current - event.clientY; // dragging up = increase Y
|
||||
const scaleFactor = 0.05; // tune this based on your scene scale
|
||||
|
||||
const unclampedY = selectedWidget.position[1] + diff * scaleFactor;
|
||||
const newY = Math.max(0, unclampedY);
|
||||
|
||||
lastClientY.current = event.clientY;
|
||||
|
||||
const newPosition: [number, number, number] = [
|
||||
selectedWidget.position[0],
|
||||
newY,
|
||||
selectedWidget.position[2],
|
||||
];
|
||||
|
||||
updateWidgetPosition(selectedzoneUuid, rightClickSelected, newPosition);
|
||||
}
|
||||
|
||||
if (rightSelect?.startsWith("Rotate")) {
|
||||
const axis = rightSelect.slice(-1).toLowerCase(); // "x", "y", or "z"
|
||||
const currentX = event.pageX;
|
||||
const sign = currentX > prevX ? 1 : currentX < prevX ? -1 : 0;
|
||||
setPrevX(currentX);
|
||||
if (selectedWidget?.rotation && selectedWidget.rotation.length >= 3) {
|
||||
const index = axis === "x" ? 0 : axis === "y" ? 1 : 2;
|
||||
const currentRotation = selectedWidget.rotation as [
|
||||
number,
|
||||
number,
|
||||
number
|
||||
]; // assert type
|
||||
const newRotation: [number, number, number] = [...currentRotation];
|
||||
newRotation[index] += 0.05 * sign;
|
||||
updateWidgetRotation(selectedzoneUuid, rightClickSelected, newRotation);
|
||||
}
|
||||
}
|
||||
// if (rightSelect === "RotateX") {
|
||||
//
|
||||
// const currentX = event.pageX;
|
||||
// const sign = currentX > prevX ? 1 : currentX < prevX ? -1 : 0;
|
||||
//
|
||||
// setPrevX(currentX);
|
||||
// if (selectedWidget?.rotation && selectedWidget.rotation.length >= 3) {
|
||||
//
|
||||
// const newRotation: [number, number, number] = [
|
||||
// selectedWidget.rotation[0] + 0.05 * sign,
|
||||
// selectedWidget.rotation[1],
|
||||
// selectedWidget.rotation[2],
|
||||
// ];
|
||||
// updateWidgetRotation(selectedzoneUuid, rightClickSelected, newRotation);
|
||||
// }
|
||||
// }
|
||||
// if (rightSelect === "RotateY") {
|
||||
// const currentX = event.pageX;
|
||||
// const sign = currentX > prevX ? 1 : currentX < prevX ? -1 : 0;
|
||||
// setPrevX(currentX);
|
||||
// if (selectedWidget?.rotation && selectedWidget.rotation.length >= 3) {
|
||||
// const newRotation: [number, number, number] = [
|
||||
// selectedWidget.rotation[0],
|
||||
// selectedWidget.rotation[1] + 0.05 * sign,
|
||||
// selectedWidget.rotation[2],
|
||||
// ];
|
||||
|
||||
// updateWidgetRotation(selectedzoneUuid, rightClickSelected, newRotation);
|
||||
// }
|
||||
// }
|
||||
// if (rightSelect === "RotateZ") {
|
||||
// const currentX = event.pageX;
|
||||
// const sign = currentX > prevX ? 1 : currentX < prevX ? -1 : 0;
|
||||
// setPrevX(currentX);
|
||||
// if (selectedWidget?.rotation && selectedWidget.rotation.length >= 3) {
|
||||
// const newRotation: [number, number, number] = [
|
||||
// selectedWidget.rotation[0],
|
||||
// selectedWidget.rotation[1],
|
||||
// selectedWidget.rotation[2] + 0.05 * sign,
|
||||
// ];
|
||||
|
||||
// updateWidgetRotation(selectedzoneUuid, rightClickSelected, newRotation);
|
||||
// }
|
||||
// }
|
||||
};
|
||||
const handleMouseUp = () => {
|
||||
if (!rightClickSelected || !rightSelect) return;
|
||||
const selectedzoneUuid = Object.keys(zoneWidgetData).find((zoneUuid) =>
|
||||
zoneWidgetData[zoneUuid].some(
|
||||
(widget) => widget.id === rightClickSelected
|
||||
)
|
||||
);
|
||||
if (!selectedzoneUuid) return;
|
||||
|
||||
const selectedWidget = zoneWidgetData[selectedzoneUuid].find(
|
||||
(widget) => widget.id === rightClickSelected
|
||||
);
|
||||
if (!selectedWidget) return;
|
||||
// Format values to 2 decimal places
|
||||
const formatValues = (vals: number[]) =>
|
||||
vals.map((val) => parseFloat(val.toFixed(2)));
|
||||
if (
|
||||
rightSelect === "Horizontal Move" ||
|
||||
rightSelect === "Vertical Move"
|
||||
) {
|
||||
let lastPosition = formatValues(selectedWidget.position) as [
|
||||
number,
|
||||
number,
|
||||
number
|
||||
];
|
||||
// (async () => {
|
||||
// let response = await update3dWidget(selectedzoneUuid, organization, rightClickSelected, lastPosition);
|
||||
//
|
||||
// if (response) {
|
||||
//
|
||||
// }
|
||||
// })();
|
||||
let updatingPosition = {
|
||||
organization: organization,
|
||||
zoneUuid: selectedzoneUuid,
|
||||
id: rightClickSelected,
|
||||
position: lastPosition,
|
||||
projectId, userId
|
||||
};
|
||||
if (visualizationSocket) {
|
||||
visualizationSocket.emit(
|
||||
"v1:viz-3D-widget:modifyPositionRotation",
|
||||
updatingPosition
|
||||
);
|
||||
}
|
||||
} else if (rightSelect.includes("Rotate")) {
|
||||
const rotation = selectedWidget.rotation || [0, 0, 0];
|
||||
|
||||
let lastRotation = formatValues(rotation) as [number, number, number];
|
||||
|
||||
// (async () => {
|
||||
// let response = await update3dWidgetRotation(selectedzoneUuid, organization, rightClickSelected, lastRotation);
|
||||
//
|
||||
// if (response) {
|
||||
//
|
||||
// }
|
||||
// })();
|
||||
let updatingRotation = {
|
||||
organization: organization,
|
||||
zoneUuid: selectedzoneUuid,
|
||||
id: rightClickSelected,
|
||||
rotation: lastRotation,
|
||||
projectId, userId
|
||||
};
|
||||
if (visualizationSocket) {
|
||||
visualizationSocket.emit(
|
||||
"v1:viz-3D-widget:modifyPositionRotation",
|
||||
updatingRotation
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset selection
|
||||
setTimeout(() => {
|
||||
setRightClickSelected(null);
|
||||
setRightSelect(null);
|
||||
}, 50);
|
||||
};
|
||||
window.addEventListener("mousedown", handleMouseDown);
|
||||
window.addEventListener("mousemove", handleMouseMove);
|
||||
window.addEventListener("mouseup", handleMouseUp);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("mousedown", handleMouseDown);
|
||||
window.removeEventListener("mousemove", handleMouseMove);
|
||||
window.removeEventListener("mouseup", handleMouseUp);
|
||||
};
|
||||
}, [rightClickSelected, rightSelect, zoneWidgetData, gl]);
|
||||
|
||||
const handleRightClick3d = (event: React.MouseEvent, id: string) => {
|
||||
event.preventDefault();
|
||||
|
||||
const canvasElement = document.getElementById("work-space-three-d-canvas");
|
||||
if (!canvasElement) throw new Error("Canvas element not found");
|
||||
|
||||
const canvasRect = canvasElement.getBoundingClientRect();
|
||||
const relativeX = event.clientX - canvasRect.left;
|
||||
const relativeY = event.clientY - canvasRect.top;
|
||||
|
||||
setEditWidgetOptions(true);
|
||||
setRightClickSelected(id);
|
||||
setTop(relativeY);
|
||||
setLeft(relativeX);
|
||||
|
||||
const selectedzoneUuid = Object.keys(zoneWidgetData).find((zoneUuid) =>
|
||||
zoneWidgetData[zoneUuid].some((widget) => widget.id === id)
|
||||
);
|
||||
if (!selectedzoneUuid) return;
|
||||
|
||||
const selectedWidget = zoneWidgetData[selectedzoneUuid].find(
|
||||
(widget) => widget.id === id
|
||||
);
|
||||
if (!selectedWidget) return;
|
||||
|
||||
const { top, left, width, height } = canvasElement.getBoundingClientRect();
|
||||
mouse.x = ((event.clientX - left) / width) * 2 - 1;
|
||||
mouse.y = -((event.clientY - top) / height) * 2 + 1;
|
||||
|
||||
raycaster.setFromCamera(mouse, camera);
|
||||
|
||||
const up = new THREE.Vector3(0, 1, 0);
|
||||
const cameraDirection = new THREE.Vector3();
|
||||
camera.getWorldDirection(cameraDirection);
|
||||
const right = new THREE.Vector3().crossVectors(up, cameraDirection).normalize();
|
||||
const verticalPlane = new THREE.Plane(right, 0);
|
||||
// const verticalPlane = new THREE.Plane(cameraDirection);
|
||||
setFloorPlanesVertical(verticalPlane);
|
||||
|
||||
const intersectPoint = raycaster.ray.intersectPlane(
|
||||
verticalPlane,
|
||||
planeIntersect.current
|
||||
);
|
||||
if (intersectPoint) {
|
||||
setintersectcontextmenu(intersectPoint.y);
|
||||
}
|
||||
const intersect2 = raycaster.ray.intersectPlane(
|
||||
plane.current,
|
||||
planeIntersect.current
|
||||
);
|
||||
if (intersect2) {
|
||||
const xDiff = -intersect2.x + selectedWidget.position[0];
|
||||
const zDiff = -intersect2.z + selectedWidget.position[2];
|
||||
setHorizontalX(xDiff);
|
||||
setHorizontalZ(zDiff);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{activeZoneWidgets.map(
|
||||
({ id, type, position, Data, rotation = [0, 0, 0] }: any) => {
|
||||
const handleRightClick = (event: React.MouseEvent, id: string) => {
|
||||
setSelectedChartId({ id: id, type: type });
|
||||
event.preventDefault();
|
||||
const canvasElement = document.getElementById(
|
||||
"work-space-three-d-canvas"
|
||||
);
|
||||
if (!canvasElement) throw new Error("Canvas element not found");
|
||||
const canvasRect = canvasElement.getBoundingClientRect();
|
||||
const relativeX = event.clientX - canvasRect.left;
|
||||
const relativeY = event.clientY - canvasRect.top;
|
||||
setEditWidgetOptions(true);
|
||||
setRightClickSelected(id);
|
||||
setTop(relativeY);
|
||||
setLeft(relativeX);
|
||||
handleRightClick3d(event, id);
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
case "ui-Widget 1":
|
||||
return (
|
||||
<ProductionCapacity
|
||||
key={id}
|
||||
id={id}
|
||||
type={type}
|
||||
position={position}
|
||||
rotation={rotation}
|
||||
Data={Data}
|
||||
onContextMenu={(e) => handleRightClick(e, id)}
|
||||
/>
|
||||
);
|
||||
case "ui-Widget 2":
|
||||
return (
|
||||
<ReturnOfInvestment
|
||||
key={id}
|
||||
id={id}
|
||||
type={type}
|
||||
position={position}
|
||||
rotation={rotation}
|
||||
Data={Data}
|
||||
onContextMenu={(e) => handleRightClick(e, id)}
|
||||
/>
|
||||
);
|
||||
case "ui-Widget 3":
|
||||
return (
|
||||
<StateWorking
|
||||
key={id}
|
||||
id={id}
|
||||
type={type}
|
||||
position={position}
|
||||
rotation={rotation}
|
||||
Data={Data}
|
||||
onContextMenu={(e) => handleRightClick(e, id)}
|
||||
/>
|
||||
);
|
||||
case "ui-Widget 4":
|
||||
return (
|
||||
<Throughput
|
||||
key={id}
|
||||
id={id}
|
||||
type={type}
|
||||
position={position}
|
||||
rotation={rotation}
|
||||
Data={Data}
|
||||
onContextMenu={(e) => handleRightClick(e, id)}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
import { Html } from "@react-three/drei";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { Bar } from "react-chartjs-2";
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
BarElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
TooltipItem, // Import TooltipItem for typing
|
||||
} from "chart.js";
|
||||
|
||||
import axios from "axios";
|
||||
import io from "socket.io-client";
|
||||
import { useWidgetStore } from "../../../../../store/useWidgetStore";
|
||||
import useChartStore from "../../../../../store/visualization/useChartStore";
|
||||
|
||||
// Register ChartJS components
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
BarElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend
|
||||
);
|
||||
interface ProductionCapacityProps {
|
||||
id: string;
|
||||
type: string;
|
||||
position: [number, number, number];
|
||||
rotation: [number, number, number];
|
||||
scale?: [number, number, number];
|
||||
Data?: any;
|
||||
onContextMenu?: (event: React.MouseEvent) => void;
|
||||
// onPointerDown:any
|
||||
}
|
||||
|
||||
const ProductionCapacity: React.FC<ProductionCapacityProps> = ({
|
||||
id,
|
||||
type,
|
||||
Data,
|
||||
position,
|
||||
rotation = [0, 0, 0],
|
||||
scale = [0.5, 0.5, 0.5],
|
||||
onContextMenu,
|
||||
}) => {
|
||||
const { selectedChartId, setSelectedChartId } = useWidgetStore();
|
||||
const {
|
||||
measurements: chartMeasurements,
|
||||
duration: chartDuration,
|
||||
name: widgetName,
|
||||
} = useChartStore();
|
||||
const [measurements, setmeasurements] = useState<any>(
|
||||
Data?.measurements ? Data.measurements : {}
|
||||
);
|
||||
const [duration, setDuration] = useState(
|
||||
Data?.duration ? Data.duration : "1h"
|
||||
);
|
||||
const [name, setName] = useState("Widget");
|
||||
const [chartData, setChartData] = useState<{
|
||||
labels: string[];
|
||||
datasets: any[];
|
||||
}>({
|
||||
labels: [],
|
||||
datasets: [],
|
||||
});
|
||||
const iotApiUrl = process.env.REACT_APP_IOT_SOCKET_SERVER_URL;
|
||||
const email = localStorage.getItem("email") || "";
|
||||
const organization = email?.split("@")[1]?.split(".")[0];
|
||||
// Chart data for a week
|
||||
const defaultChartData = {
|
||||
labels: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"], // Days of the week
|
||||
datasets: [
|
||||
{
|
||||
label: "Production Capacity (units/day)",
|
||||
data: [1500, 1600, 1400, 1700, 1800, 1900, 2000], // Example daily production data
|
||||
backgroundColor: "#6f42c1", // Theme color
|
||||
borderColor: "#6f42c1",
|
||||
borderWidth: 1,
|
||||
borderRadius: 8, // Rounded corners for the bars
|
||||
borderSkipped: false, // Ensure all corners are rounded
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Chart options
|
||||
const chartOptions = {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false, // Hide legend
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: "Weekly Production Capacity",
|
||||
font: {
|
||||
size: 16,
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
// Explicitly type the context parameter
|
||||
label: (context: TooltipItem<"bar">) => {
|
||||
const value = context.parsed.y; // Extract the y-axis value
|
||||
return `${value} units`; // Customize tooltip to display "units"
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
display: false, // Hide x-axis grid lines
|
||||
},
|
||||
},
|
||||
y: {
|
||||
display: false, // Remove the y-axis completely
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!iotApiUrl || !measurements || Object.keys(measurements).length === 0)
|
||||
return;
|
||||
|
||||
const socket = io(`http://${iotApiUrl}`);
|
||||
|
||||
const inputData = {
|
||||
measurements,
|
||||
duration,
|
||||
interval: 1000,
|
||||
};
|
||||
|
||||
const startStream = () => {
|
||||
socket.emit("lineInput", inputData);
|
||||
};
|
||||
|
||||
socket.on("connect", startStream);
|
||||
|
||||
socket.on("lineOutput", (response) => {
|
||||
const responseData = response.data;
|
||||
|
||||
// Extract timestamps and values
|
||||
const labels = responseData.time;
|
||||
const datasets = Object.keys(measurements).map((key) => {
|
||||
const measurement = measurements[key];
|
||||
const datasetKey = `${measurement.name}.${measurement.fields}`;
|
||||
return {
|
||||
label: datasetKey,
|
||||
data: responseData[datasetKey]?.values ?? [],
|
||||
backgroundColor: "#6f42c1", // Theme color
|
||||
borderColor: "#6f42c1",
|
||||
borderWidth: 1,
|
||||
borderRadius: 8, // Rounded corners for the bars
|
||||
borderSkipped: false, // Ensure all corners are rounded
|
||||
};
|
||||
});
|
||||
|
||||
setChartData({ labels, datasets });
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket.off("lineOutput");
|
||||
socket.emit("stop_stream"); // Stop streaming when component unmounts
|
||||
socket.disconnect();
|
||||
};
|
||||
}, [measurements, duration, iotApiUrl]);
|
||||
|
||||
const fetchSavedInputes = async () => {
|
||||
if (id !== "") {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}/api/v2/widget3D/${id}/${organization}`
|
||||
);
|
||||
|
||||
if (response.status === 200) {
|
||||
setmeasurements(response.data.Data.measurements);
|
||||
setDuration(response.data.Data.duration);
|
||||
setName(response.data.widgetName);
|
||||
} else {
|
||||
}
|
||||
} catch (error) {
|
||||
echo.error("Failed to fetch saved inputs");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchSavedInputes();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedChartId?.id === id) {
|
||||
fetchSavedInputes();
|
||||
}
|
||||
}, [chartMeasurements, chartDuration, widgetName]);
|
||||
|
||||
useEffect(() => {}, [rotation]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{position && scale && rotation && <Html
|
||||
// data
|
||||
position={position}
|
||||
scale={scale}
|
||||
rotation={rotation}
|
||||
// class
|
||||
wrapperClass="pointer-none"
|
||||
// other
|
||||
transform
|
||||
zIndexRange={[1, 0]}
|
||||
prepend
|
||||
// sprite
|
||||
distanceFactor={20}
|
||||
// events
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`productionCapacity-wrapper card ${selectedChartId?.id === id ? "activeChart" : ""
|
||||
}`}
|
||||
onClick={() => setSelectedChartId({ id: id, type: type })}
|
||||
onContextMenu={onContextMenu}
|
||||
style={{
|
||||
width: "300px", // Original width
|
||||
height: "300px", // Original height
|
||||
// transform: transformStyle.transform,
|
||||
transformStyle: "preserve-3d",
|
||||
position: "absolute",
|
||||
transform: "translate(-50%, -50%)",
|
||||
}}
|
||||
>
|
||||
<div className="headeproductionCapacityr-wrapper">
|
||||
<div className="header">Production Capacity</div>
|
||||
<div className="production-capacity">
|
||||
<div className="value">1,200</div>{" "}
|
||||
<div className="value">units/hour</div>
|
||||
</div>
|
||||
<div className="production-capacity">
|
||||
<div className="current">
|
||||
<div className="key">Current</div>
|
||||
<div className="value">1500</div>
|
||||
</div>
|
||||
<div className="target">
|
||||
<div className="key">Target</div>
|
||||
<div className="value">2.345</div>
|
||||
</div>
|
||||
{/* <div className="value">units/hour</div> */}
|
||||
</div>
|
||||
</div>{" "}
|
||||
<div className="bar-chart charts">
|
||||
{/* Bar Chart */}
|
||||
<Bar
|
||||
data={
|
||||
Object.keys(measurements).length > 0
|
||||
? chartData
|
||||
: defaultChartData
|
||||
}
|
||||
options={chartOptions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Html>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductionCapacity;
|
||||
@@ -0,0 +1,282 @@
|
||||
import { Html } from "@react-three/drei";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { Line } from "react-chartjs-2";
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Tooltip,
|
||||
ChartData,
|
||||
ChartOptions,
|
||||
} from "chart.js";
|
||||
|
||||
import axios from "axios";
|
||||
import io from "socket.io-client";
|
||||
import { useWidgetStore } from "../../../../../store/useWidgetStore";
|
||||
import useChartStore from "../../../../../store/visualization/useChartStore";
|
||||
import { WavyIcon } from "../../../../../components/icons/3dChartIcons";
|
||||
|
||||
// Register Chart.js components
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Tooltip
|
||||
);
|
||||
|
||||
// Define Props for SmoothLineGraphComponent
|
||||
interface SmoothLineGraphProps {
|
||||
data: ChartData<"line">; // Type for chart data
|
||||
options?: ChartOptions<"line">; // Type for chart options (optional)
|
||||
}
|
||||
|
||||
// SmoothLineGraphComponent using react-chartjs-2
|
||||
const SmoothLineGraphComponent: React.FC<SmoothLineGraphProps> = ({
|
||||
data,
|
||||
options,
|
||||
}) => {
|
||||
return <Line data={data} options={options} />;
|
||||
};
|
||||
interface ReturnOfInvestmentProps {
|
||||
id: string;
|
||||
type: string;
|
||||
position: [number, number, number];
|
||||
rotation: [number, number, number];
|
||||
Data?: any;
|
||||
onContextMenu?: (event: React.MouseEvent) => void;
|
||||
}
|
||||
const ReturnOfInvestment: React.FC<ReturnOfInvestmentProps> = ({
|
||||
id,
|
||||
type,
|
||||
Data,
|
||||
position,
|
||||
rotation,
|
||||
onContextMenu,
|
||||
}) => {
|
||||
const { selectedChartId, setSelectedChartId } = useWidgetStore();
|
||||
const {
|
||||
measurements: chartMeasurements,
|
||||
duration: chartDuration,
|
||||
name: widgetName,
|
||||
} = useChartStore();
|
||||
const [measurements, setmeasurements] = useState<any>(
|
||||
Data?.measurements ? Data.measurements : {}
|
||||
);
|
||||
const [duration, setDuration] = useState(
|
||||
Data?.duration ? Data.duration : "1h"
|
||||
);
|
||||
const [name, setName] = useState("Widget");
|
||||
const [chartData, setChartData] = useState<{
|
||||
labels: string[];
|
||||
datasets: any[];
|
||||
}>({
|
||||
labels: [],
|
||||
datasets: [],
|
||||
});
|
||||
const iotApiUrl = process.env.REACT_APP_IOT_SOCKET_SERVER_URL;
|
||||
const email = localStorage.getItem("email") || "";
|
||||
const organization = email?.split("@")[1]?.split(".")[0];
|
||||
// Improved sample data for the smooth curve graph (single day)
|
||||
const graphData: ChartData<"line"> = {
|
||||
labels: [
|
||||
"12 AM",
|
||||
"3 AM",
|
||||
"6 AM",
|
||||
"9 AM",
|
||||
"12 PM",
|
||||
"3 PM",
|
||||
"6 PM",
|
||||
"9 PM",
|
||||
"12 AM",
|
||||
],
|
||||
datasets: [
|
||||
{
|
||||
label: "Investment",
|
||||
data: [100, 250, 400, 400, 500, 600, 700, 800, 900], // Example investment growth
|
||||
borderColor: "rgba(75, 192, 192, 1)", // Light blue color
|
||||
backgroundColor: "rgba(75, 192, 192, 0.2)",
|
||||
fill: true,
|
||||
tension: 0.4, // Smooth curve effect
|
||||
pointRadius: 0, // Hide dots
|
||||
pointHoverRadius: 0, // Hide hover dots
|
||||
},
|
||||
{
|
||||
label: "Return",
|
||||
data: [100, 200, 500, 250, 300, 350, 400, 450, 500], // Example return values
|
||||
borderColor: "rgba(255, 99, 132, 1)", // Pink color
|
||||
backgroundColor: "rgba(255, 99, 132, 0.2)",
|
||||
fill: true,
|
||||
tension: 0.4, // Smooth curve effect
|
||||
pointRadius: 0, // Hide dots
|
||||
pointHoverRadius: 0, // Hide hover dots
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Options for the smooth curve graph
|
||||
const graphOptions: ChartOptions<"line"> = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
tooltip: {
|
||||
enabled: true, // Enable tooltips on hover
|
||||
mode: "index", // Show both datasets' values at the same index
|
||||
intersect: false, // Allow hovering anywhere on the graph
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
display: false, // Hide x-axis grid lines
|
||||
},
|
||||
ticks: {
|
||||
display: false, // Hide x-axis labels
|
||||
},
|
||||
},
|
||||
y: {
|
||||
grid: {
|
||||
display: false, // Hide y-axis grid lines
|
||||
},
|
||||
ticks: {
|
||||
display: false, // Hide y-axis labels
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!iotApiUrl || !measurements || Object.keys(measurements).length === 0)
|
||||
return;
|
||||
|
||||
const socket = io(`http://${iotApiUrl}`);
|
||||
|
||||
const inputData = {
|
||||
measurements,
|
||||
duration,
|
||||
interval: 1000,
|
||||
};
|
||||
|
||||
const startStream = () => {
|
||||
socket.emit("lineInput", inputData);
|
||||
};
|
||||
|
||||
socket.on("connect", startStream);
|
||||
|
||||
socket.on("lineOutput", (response) => {
|
||||
const responseData = response.data;
|
||||
|
||||
// Extract timestamps and values
|
||||
const labels = responseData.time;
|
||||
const datasets = Object.keys(measurements).map((key, index) => {
|
||||
const measurement = measurements[key];
|
||||
const datasetKey = `${measurement.name}.${measurement.fields}`;
|
||||
return {
|
||||
label: datasetKey,
|
||||
data: responseData[datasetKey]?.values ?? [],
|
||||
borderColor:
|
||||
index === 0 ? "rgba(75, 192, 192, 1)" : "rgba(255, 99, 132, 1)", // Light blue color
|
||||
backgroundColor:
|
||||
index === 0 ? "rgba(75, 192, 192, 0.2)" : "rgba(255, 99, 132, 0.2)",
|
||||
fill: true,
|
||||
tension: 0.4, // Smooth curve effect
|
||||
pointRadius: 0, // Hide dots
|
||||
pointHoverRadius: 0, // Hide hover dots
|
||||
};
|
||||
});
|
||||
|
||||
setChartData({ labels, datasets });
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket.off("lineOutput");
|
||||
socket.emit("stop_stream"); // Stop streaming when component unmounts
|
||||
socket.disconnect();
|
||||
};
|
||||
}, [measurements, duration, iotApiUrl]);
|
||||
|
||||
const fetchSavedInputes = async () => {
|
||||
if (id !== "") {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}/api/v2/widget3D/${id}/${organization}`
|
||||
);
|
||||
if (response.status === 200) {
|
||||
setmeasurements(response.data.Data.measurements);
|
||||
setDuration(response.data.Data.duration);
|
||||
setName(response.data.widgetName);
|
||||
} else {
|
||||
console.log("Unexpected response:", response);
|
||||
}
|
||||
} catch (error) {
|
||||
echo.error("Failed to fetch saved inputs");
|
||||
console.error("There was an error!", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchSavedInputes();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedChartId?.id === id) {
|
||||
fetchSavedInputes();
|
||||
}
|
||||
}, [chartMeasurements, chartDuration, widgetName]);
|
||||
const rotationDegrees = {
|
||||
x: (rotation[0] * 180) / Math.PI,
|
||||
y: (rotation[1] * 180) / Math.PI,
|
||||
z: (rotation[2] * 180) / Math.PI,
|
||||
};
|
||||
|
||||
const transformStyle = {
|
||||
transform: `rotateX(${rotationDegrees.x}deg) rotateY(${rotationDegrees.y}deg) rotateZ(${rotationDegrees.z}deg)`,
|
||||
};
|
||||
|
||||
return (
|
||||
<Html
|
||||
position={[position[0], position[1], position[2]]}
|
||||
rotation={rotation}
|
||||
scale={[0.5, 0.5, 0.5]}
|
||||
transform
|
||||
zIndexRange={[1, 0]}
|
||||
prepend
|
||||
distanceFactor={20}
|
||||
|
||||
|
||||
|
||||
>
|
||||
<div
|
||||
className={`returnOfInvestment card ${
|
||||
selectedChartId?.id === id ? "activeChart" : ""
|
||||
}`}
|
||||
onClick={() => setSelectedChartId({ id: id, type: type })}
|
||||
onContextMenu={onContextMenu}
|
||||
>
|
||||
<div className="header">Return of Investment</div>
|
||||
<div className="lineGraph charts">
|
||||
{/* Smooth curve graph with two datasets */}
|
||||
<SmoothLineGraphComponent
|
||||
data={Object.keys(measurements).length > 0 ? chartData : graphData}
|
||||
options={graphOptions}
|
||||
/>
|
||||
</div>
|
||||
<div className="returns-wrapper">
|
||||
<div className="icon">
|
||||
<WavyIcon />
|
||||
</div>
|
||||
<div className="value">5.78</div>
|
||||
<div className="key">Years</div>
|
||||
</div>
|
||||
<div className="footer">
|
||||
in <span>5y</span> with avg <span>7%</span> yearly return
|
||||
</div>
|
||||
</div>
|
||||
</Html>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReturnOfInvestment;
|
||||
211
app/src/modules/visualization/widgets/3d/cards/StateWorking.tsx
Normal file
211
app/src/modules/visualization/widgets/3d/cards/StateWorking.tsx
Normal file
@@ -0,0 +1,211 @@
|
||||
import { Html } from "@react-three/drei";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import axios from "axios";
|
||||
import io from "socket.io-client";
|
||||
import { useWidgetStore } from "../../../../../store/useWidgetStore";
|
||||
import useChartStore from "../../../../../store/visualization/useChartStore";
|
||||
|
||||
// import image from "../../../../assets/image/temp/image.png";
|
||||
interface StateWorkingProps {
|
||||
id: string;
|
||||
type: string;
|
||||
position: [number, number, number];
|
||||
rotation: [number, number, number];
|
||||
Data?: any;
|
||||
onContextMenu?: (event: React.MouseEvent) => void;
|
||||
}
|
||||
const StateWorking: React.FC<StateWorkingProps> = ({
|
||||
id,
|
||||
type,
|
||||
Data,
|
||||
position,
|
||||
rotation,
|
||||
onContextMenu,
|
||||
}) => {
|
||||
const { selectedChartId, setSelectedChartId } = useWidgetStore();
|
||||
const {
|
||||
measurements: chartMeasurements,
|
||||
duration: chartDuration,
|
||||
name: widgetName,
|
||||
} = useChartStore();
|
||||
const [measurements, setmeasurements] = useState<any>(
|
||||
Data?.measurements ? Data.measurements : {}
|
||||
);
|
||||
const [duration, setDuration] = useState(
|
||||
Data?.duration ? Data.duration : "1h"
|
||||
);
|
||||
const [name, setName] = useState("Widget");
|
||||
const [datas, setDatas] = useState<any>({});
|
||||
const iotApiUrl = process.env.REACT_APP_IOT_SOCKET_SERVER_URL;
|
||||
const email = localStorage.getItem("email") || "";
|
||||
const organization = email?.split("@")[1]?.split(".")[0];
|
||||
// const datas = [
|
||||
// { key: "Oil Tank:", value: "24/341" },
|
||||
// { key: "Oil Refin:", value: 36.023 },
|
||||
// { key: "Transmission:", value: 36.023 },
|
||||
// { key: "Fuel:", value: 36732 },
|
||||
// { key: "Power:", value: 1300 },
|
||||
// { key: "Time:", value: 13 - 9 - 2023 },
|
||||
// ];
|
||||
|
||||
useEffect(() => {
|
||||
if (!iotApiUrl || !measurements || Object.keys(measurements).length === 0)
|
||||
return;
|
||||
const socket = io(`http://${iotApiUrl}`);
|
||||
const inputData = {
|
||||
measurements,
|
||||
duration,
|
||||
interval: 1000,
|
||||
};
|
||||
const startStream = () => {
|
||||
socket.emit("lastInput", inputData);
|
||||
};
|
||||
socket.on("connect", startStream);
|
||||
socket.on("lastOutput", (response) => {
|
||||
const responseData = response;
|
||||
|
||||
setDatas(responseData);
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket.off("lastOutput");
|
||||
socket.emit("stop_stream"); // Stop streaming when component unmounts
|
||||
socket.disconnect();
|
||||
};
|
||||
}, [measurements, duration, iotApiUrl]);
|
||||
|
||||
const fetchSavedInputes = async () => {
|
||||
if (id !== "") {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}/api/v2/widget3D/${id}/${organization}`
|
||||
);
|
||||
if (response.status === 200) {
|
||||
setmeasurements(response.data.Data.measurements);
|
||||
setDuration(response.data.Data.duration);
|
||||
setName(response.data.widgetName);
|
||||
} else {
|
||||
console.log("Unexpected response:", response);
|
||||
}
|
||||
} catch (error) {
|
||||
echo.error("Failed to fetch saved inputs");
|
||||
console.error("There was an error!", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchSavedInputes();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedChartId?.id === id) {
|
||||
fetchSavedInputes();
|
||||
}
|
||||
}, [chartMeasurements, chartDuration, widgetName]);
|
||||
|
||||
const rotationDegrees = {
|
||||
x: (rotation[0] * 180) / Math.PI,
|
||||
y: (rotation[1] * 180) / Math.PI,
|
||||
z: (rotation[2] * 180) / Math.PI,
|
||||
};
|
||||
|
||||
const transformStyle = {
|
||||
transform: `rotateX(${rotationDegrees.x}deg) rotateY(${rotationDegrees.y}deg) rotateZ(${rotationDegrees.z}deg)`,
|
||||
};
|
||||
return (
|
||||
<Html
|
||||
position={[position[0], position[1], position[2]]}
|
||||
rotation={rotation}
|
||||
scale={[0.5, 0.5, 0.5]}
|
||||
transform
|
||||
zIndexRange={[1, 0]}
|
||||
// sprite={true}
|
||||
prepend
|
||||
distanceFactor={20}
|
||||
// style={{
|
||||
// transform: transformStyle.transform,
|
||||
// transformStyle: "preserve-3d",
|
||||
// transition: "transform 0.1s ease-out",
|
||||
// }}
|
||||
>
|
||||
<div
|
||||
className={`stateWorking-wrapper card ${
|
||||
selectedChartId?.id === id ? "activeChart" : ""
|
||||
}`}
|
||||
onClick={() => setSelectedChartId({ id: id, type: type })}
|
||||
onContextMenu={onContextMenu}
|
||||
>
|
||||
<div className="header-wrapper">
|
||||
<div className="header">
|
||||
<span>State</span>
|
||||
<span>
|
||||
{datas?.input1 ? datas.input1 : "input1"} <span>.</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="img">{/* <img src={image} alt="" /> */}</div>
|
||||
</div>
|
||||
{/* Data */}
|
||||
<div className="data-wrapper">
|
||||
{/* {datas.map((data, index) => (
|
||||
<div className="data-table" key={index}>
|
||||
<div className="data">{data.key}</div>
|
||||
<div className="key">{data.value}</div>
|
||||
</div>
|
||||
))} */}
|
||||
<div className="data-table">
|
||||
<div className="data">
|
||||
{measurements?.input2?.fields
|
||||
? measurements.input2.fields
|
||||
: "input2"}
|
||||
</div>
|
||||
<div className="key">{datas?.input2 ? datas.input2 : "data"}</div>
|
||||
</div>
|
||||
<div className="data-table">
|
||||
<div className="data">
|
||||
{measurements?.input3?.fields
|
||||
? measurements.input3.fields
|
||||
: "input3"}
|
||||
</div>
|
||||
<div className="key">{datas?.input3 ? datas.input3 : "data"}</div>
|
||||
</div>
|
||||
<div className="data-table">
|
||||
<div className="data">
|
||||
{measurements?.input4?.fields
|
||||
? measurements.input4.fields
|
||||
: "input4"}
|
||||
</div>
|
||||
<div className="key">{datas?.input4 ? datas.input4 : "data"}</div>
|
||||
</div>
|
||||
<div className="data-table">
|
||||
<div className="data">
|
||||
{measurements?.input5?.fields
|
||||
? measurements.input5.fields
|
||||
: "input5"}
|
||||
</div>
|
||||
<div className="key">{datas?.input5 ? datas.input5 : "data"}</div>
|
||||
</div>
|
||||
<div className="data-table">
|
||||
<div className="data">
|
||||
{measurements?.input6?.fields
|
||||
? measurements.input6.fields
|
||||
: "input6"}
|
||||
</div>
|
||||
<div className="key">{datas?.input6 ? datas.input6 : "data"}</div>
|
||||
</div>
|
||||
<div className="data-table">
|
||||
<div className="data">
|
||||
{measurements?.input7?.fields
|
||||
? measurements.input7.fields
|
||||
: "input7"}
|
||||
</div>
|
||||
<div className="key">{datas?.input7 ? datas.input7 : "data"}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Html>
|
||||
);
|
||||
};
|
||||
|
||||
export default StateWorking;
|
||||
272
app/src/modules/visualization/widgets/3d/cards/Throughput.tsx
Normal file
272
app/src/modules/visualization/widgets/3d/cards/Throughput.tsx
Normal file
@@ -0,0 +1,272 @@
|
||||
import { Html } from "@react-three/drei";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { Line } from "react-chartjs-2";
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ChartData,
|
||||
ChartOptions,
|
||||
} from "chart.js";
|
||||
|
||||
import axios from "axios";
|
||||
import io from "socket.io-client";
|
||||
import { useWidgetStore } from "../../../../../store/useWidgetStore";
|
||||
import useChartStore from "../../../../../store/visualization/useChartStore";
|
||||
import { ThroughputIcon } from "../../../../../components/icons/3dChartIcons";
|
||||
|
||||
// Register Chart.js components
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend
|
||||
);
|
||||
|
||||
// Define Props for LineGraphComponent
|
||||
interface LineGraphProps {
|
||||
data: ChartData<"line">; // Type for chart data
|
||||
options?: ChartOptions<"line">; // Type for chart options (optional)
|
||||
}
|
||||
|
||||
// LineGraphComponent using react-chartjs-2
|
||||
const LineGraphComponent: React.FC<LineGraphProps> = ({ data, options }) => {
|
||||
return <Line data={data} options={options} />;
|
||||
};
|
||||
|
||||
interface ThroughputProps {
|
||||
id: string;
|
||||
type: string;
|
||||
position: [number, number, number];
|
||||
rotation: [number, number, number];
|
||||
Data?: any;
|
||||
onContextMenu?: (event: React.MouseEvent) => void;
|
||||
}
|
||||
|
||||
const Throughput: React.FC<ThroughputProps> = ({
|
||||
id,
|
||||
type,
|
||||
Data,
|
||||
position,
|
||||
rotation,
|
||||
onContextMenu,
|
||||
}) => {
|
||||
const { selectedChartId, setSelectedChartId } = useWidgetStore();
|
||||
const {
|
||||
measurements: chartMeasurements,
|
||||
duration: chartDuration,
|
||||
name: widgetName,
|
||||
} = useChartStore();
|
||||
const [measurements, setmeasurements] = useState<any>(
|
||||
Data?.measurements ? Data.measurements : {}
|
||||
);
|
||||
const [duration, setDuration] = useState(
|
||||
Data?.duration ? Data.duration : "1h"
|
||||
);
|
||||
const [name, setName] = useState("Widget");
|
||||
const [chartData, setChartData] = useState<{
|
||||
labels: string[];
|
||||
datasets: any[];
|
||||
}>({
|
||||
labels: [],
|
||||
datasets: [],
|
||||
});
|
||||
const iotApiUrl = process.env.REACT_APP_IOT_SOCKET_SERVER_URL;
|
||||
const email = localStorage.getItem("email") || "";
|
||||
const organization = email?.split("@")[1]?.split(".")[0];
|
||||
|
||||
|
||||
|
||||
// Sample data for the line graph
|
||||
const graphData: ChartData<"line"> = {
|
||||
labels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"],
|
||||
datasets: [
|
||||
{
|
||||
label: "Throughput",
|
||||
data: [1000, 1200, 1100, 1300, 1250, 1400], // Example throughput values
|
||||
borderColor: "rgba(75, 192, 192, 1)",
|
||||
backgroundColor: "rgba(75, 192, 192, 0.2)",
|
||||
fill: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Options for the line graph
|
||||
const graphOptions: ChartOptions<"line"> = {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: "top",
|
||||
display: false,
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: "Throughput Over Time",
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
display: true, // Show vertical grid lines
|
||||
},
|
||||
ticks: {
|
||||
display: false, // Hide x-axis labels
|
||||
},
|
||||
},
|
||||
y: {
|
||||
grid: {
|
||||
display: false, // Hide horizontal grid lines
|
||||
},
|
||||
ticks: {
|
||||
display: false, // Hide y-axis labels
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!iotApiUrl || !measurements || Object.keys(measurements).length === 0)
|
||||
return;
|
||||
|
||||
const socket = io(`http://${iotApiUrl}`);
|
||||
|
||||
const inputData = {
|
||||
measurements,
|
||||
duration,
|
||||
interval: 1000,
|
||||
};
|
||||
|
||||
const startStream = () => {
|
||||
socket.emit("lineInput", inputData);
|
||||
};
|
||||
|
||||
socket.on("connect", startStream);
|
||||
|
||||
socket.on("lineOutput", (response) => {
|
||||
const responseData = response.data;
|
||||
|
||||
// Extract timestamps and values
|
||||
const labels = responseData.time;
|
||||
const datasets = Object.keys(measurements).map((key) => {
|
||||
const measurement = measurements[key];
|
||||
const datasetKey = `${measurement.name}.${measurement.fields}`;
|
||||
return {
|
||||
label: datasetKey,
|
||||
data: responseData[datasetKey]?.values ?? [],
|
||||
borderColor: "rgba(75, 192, 192, 1)",
|
||||
backgroundColor: "rgba(75, 192, 192, 0.2)",
|
||||
fill: true,
|
||||
};
|
||||
});
|
||||
|
||||
setChartData({ labels, datasets });
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket.off("lineOutput");
|
||||
socket.emit("stop_stream"); // Stop streaming when component unmounts
|
||||
socket.disconnect();
|
||||
};
|
||||
}, [measurements, duration, iotApiUrl]);
|
||||
|
||||
const fetchSavedInputes = async () => {
|
||||
if (id !== "") {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}/api/v2/widget3D/${id}/${organization}`
|
||||
);
|
||||
if (response.status === 200) {
|
||||
setmeasurements(response.data.Data.measurements);
|
||||
setDuration(response.data.Data.duration);
|
||||
setName(response.data.widgetName);
|
||||
} else {
|
||||
console.log("Unexpected response:", response);
|
||||
}
|
||||
} catch (error) {
|
||||
echo.error("Failed to fetch saved inputs");
|
||||
console.error("There was an error!", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchSavedInputes();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedChartId?.id === id) {
|
||||
fetchSavedInputes();
|
||||
}
|
||||
}, [chartMeasurements, chartDuration, widgetName]);
|
||||
const rotationDegrees = {
|
||||
x: (rotation[0] * 180) / Math.PI,
|
||||
y: (rotation[1] * 180) / Math.PI,
|
||||
z: (rotation[2] * 180) / Math.PI,
|
||||
};
|
||||
|
||||
const transformStyle = {
|
||||
transform: `rotateX(${rotationDegrees.x}deg) rotateY(${rotationDegrees.y}deg) rotateZ(${rotationDegrees.z}deg)`,
|
||||
};
|
||||
|
||||
return (
|
||||
<Html
|
||||
position={[position[0], position[1], position[2]]}
|
||||
rotation={rotation}
|
||||
scale={[0.5, 0.5, 0.5]}
|
||||
transform
|
||||
zIndexRange={[1, 0]}
|
||||
prepend
|
||||
distanceFactor={20}
|
||||
// sprite={true}
|
||||
>
|
||||
<div
|
||||
className={`throughput-wrapper card ${selectedChartId?.id === id ? "activeChart" : ""
|
||||
}`}
|
||||
onClick={() => setSelectedChartId({ id: id, type: type })}
|
||||
onContextMenu={onContextMenu}
|
||||
>
|
||||
<div className="header">{name}</div>
|
||||
<div className="display-value">
|
||||
<div className="left">
|
||||
<div className="icon">
|
||||
<ThroughputIcon />
|
||||
</div>
|
||||
<div className="value-container">
|
||||
<div className="value-wrapper">
|
||||
<div className="value">1,200</div>
|
||||
<div className="key"> Units/hr</div>
|
||||
</div>
|
||||
<div className="total-sales">
|
||||
<div className="value">316</div>
|
||||
<div className="key">sales</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="right">
|
||||
<div className="percent-increase">5.77%</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="line-graph">
|
||||
{/* Line graph using react-chartjs-2 */}
|
||||
<LineGraphComponent
|
||||
data={Object.keys(measurements).length > 0 ? chartData : graphData}
|
||||
options={graphOptions}
|
||||
/>
|
||||
</div>
|
||||
<div className="footer">
|
||||
You made an extra <span className="value">$1256.13</span> this month
|
||||
</div>
|
||||
</div>
|
||||
</Html>
|
||||
);
|
||||
};
|
||||
|
||||
export default Throughput;
|
||||
130
app/src/modules/visualization/widgets/floating/DistanceLines.tsx
Normal file
130
app/src/modules/visualization/widgets/floating/DistanceLines.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
import React from "react";
|
||||
|
||||
interface DistanceLinesProps {
|
||||
obj: {
|
||||
position: {
|
||||
top?: number | "auto";
|
||||
left?: number | "auto";
|
||||
right?: number | "auto";
|
||||
bottom?: number | "auto";
|
||||
};
|
||||
};
|
||||
activeEdges: {
|
||||
vertical: "top" | "bottom";
|
||||
horizontal: "left" | "right";
|
||||
} | null;
|
||||
}
|
||||
|
||||
const DistanceLines: React.FC<DistanceLinesProps> = ({ obj, activeEdges }) => {
|
||||
if (!activeEdges) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{activeEdges.vertical === "top" &&
|
||||
typeof obj.position.top === "number" && (
|
||||
<div
|
||||
className="distance-line top"
|
||||
style={{
|
||||
top: 0,
|
||||
left:
|
||||
activeEdges.horizontal === "left"
|
||||
? `${(obj.position.left as number) + 125}px`
|
||||
: `calc(100% - ${(obj.position.right as number) + 125}px)`,
|
||||
height: `${obj.position.top}px`,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="distance-label"
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
transform: "translate(-50%,0%)",
|
||||
}}
|
||||
>
|
||||
{obj.position.top.toFixed(1)}px
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeEdges.vertical === "bottom" &&
|
||||
typeof obj.position.bottom === "number" && (
|
||||
<div
|
||||
className="distance-line bottom"
|
||||
style={{
|
||||
bottom: 0,
|
||||
left:
|
||||
activeEdges.horizontal === "left"
|
||||
? `${(obj.position.left as number) + 125}px`
|
||||
: `calc(100% - ${(obj.position.right as number) + 125}px)`,
|
||||
height: `${obj.position.bottom}px`,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="distance-label"
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: "50%",
|
||||
transform: "translate(-50%,0%)",
|
||||
}}
|
||||
>
|
||||
{obj.position.bottom.toFixed(1)}px
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeEdges.horizontal === "left" &&
|
||||
typeof obj.position.left === "number" && (
|
||||
<div
|
||||
className="distance-line left"
|
||||
style={{
|
||||
left: 0,
|
||||
top:
|
||||
activeEdges.vertical === "top"
|
||||
? `${(obj.position.top as number) + 41.5}px`
|
||||
: `calc(100% - ${(obj.position.bottom as number) + 41.5}px)`,
|
||||
width: `${obj.position.left}px`,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="distance-label"
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: "50%",
|
||||
transform: "translate(0,-50%)",
|
||||
}}
|
||||
>
|
||||
{obj.position.left.toFixed(1)}px
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeEdges.horizontal === "right" &&
|
||||
typeof obj.position.right === "number" && (
|
||||
<div
|
||||
className="distance-line right"
|
||||
style={{
|
||||
right: 0,
|
||||
top:
|
||||
activeEdges.vertical === "top"
|
||||
? `${(obj.position.top as number) + 41.5}px`
|
||||
: `calc(100% - ${(obj.position.bottom as number) + 41.5}px)`,
|
||||
width: `${obj.position.right}px`,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="distance-label"
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: "50%",
|
||||
transform: "translate(0,-50%)",
|
||||
}}
|
||||
>
|
||||
{obj.position.right.toFixed(1)}px
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DistanceLines;
|
||||
@@ -0,0 +1,634 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useDroppedObjectsStore } from "../../../../store/visualization/useDroppedObjectsStore";
|
||||
import useModuleStore from "../../../../store/useModuleStore";
|
||||
import { determinePosition } from "../../functions/determinePosition";
|
||||
import { getActiveProperties } from "../../functions/getActiveProperties";
|
||||
import { addingFloatingWidgets } from "../../../../services/visulization/zone/addFloatingWidgets";
|
||||
import {
|
||||
DublicateIcon,
|
||||
KebabIcon,
|
||||
DeleteIcon,
|
||||
} from "../../../../components/icons/ExportCommonIcons";
|
||||
import DistanceLines from "./DistanceLines"; // Import the DistanceLines component
|
||||
|
||||
import TotalCardComponent from "./cards/TotalCardComponent";
|
||||
import WarehouseThroughputComponent from "./cards/WarehouseThroughputComponent";
|
||||
import FleetEfficiencyComponent from "./cards/FleetEfficiencyComponent";
|
||||
import { useWidgetStore } from "../../../../store/useWidgetStore";
|
||||
import { useSocketStore } from "../../../../store/builder/store";
|
||||
import { usePlayButtonStore } from "../../../../store/usePlayButtonStore";
|
||||
import { useSelectedZoneStore } from "../../../../store/visualization/useZoneStore";
|
||||
import { useParams } from "react-router-dom";
|
||||
interface DraggingState {
|
||||
zone: string;
|
||||
index: number;
|
||||
initialPosition: {
|
||||
top: number | "auto";
|
||||
left: number | "auto";
|
||||
right: number | "auto";
|
||||
bottom: number | "auto";
|
||||
};
|
||||
}
|
||||
|
||||
interface DraggingState {
|
||||
zone: string;
|
||||
index: number;
|
||||
initialPosition: {
|
||||
top: number | "auto";
|
||||
left: number | "auto";
|
||||
right: number | "auto";
|
||||
bottom: number | "auto";
|
||||
};
|
||||
}
|
||||
const DroppedObjects: React.FC = () => {
|
||||
const { visualizationSocket } = useSocketStore();
|
||||
const { isPlaying } = usePlayButtonStore();
|
||||
const zones = useDroppedObjectsStore((state) => state.zones);
|
||||
const { projectId } = useParams();
|
||||
|
||||
const [openKebabId, setOpenKebabId] = useState<string | null>(null);
|
||||
const updateObjectPosition = useDroppedObjectsStore(
|
||||
(state) => state.updateObjectPosition
|
||||
);
|
||||
const { selectedZone } = useSelectedZoneStore();
|
||||
|
||||
const deleteObject = useDroppedObjectsStore((state) => state.deleteObject);
|
||||
|
||||
const duplicateObject = useDroppedObjectsStore(
|
||||
(state) => state.duplicateObject
|
||||
);
|
||||
const [draggingIndex, setDraggingIndex] = useState<DraggingState | null>(
|
||||
null
|
||||
);
|
||||
const [offset, setOffset] = useState<[number, number] | null>(null);
|
||||
const { selectedChartId, setSelectedChartId } = useWidgetStore();
|
||||
|
||||
const [activeEdges, setActiveEdges] = useState<{
|
||||
vertical: "top" | "bottom";
|
||||
horizontal: "left" | "right";
|
||||
} | null>(null); // State to track active edges for distance lines
|
||||
|
||||
const [currentPosition, setCurrentPosition] = useState<{
|
||||
top: number | "auto";
|
||||
left: number | "auto";
|
||||
right: number | "auto";
|
||||
bottom: number | "auto";
|
||||
} | null>(null); // State to track the current position during drag
|
||||
const animationRef = useRef<number | null>(null);
|
||||
const chartWidget = useRef<HTMLDivElement>(null);
|
||||
|
||||
const kebabRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Clean up animation frame on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (animationRef.current) {
|
||||
cancelAnimationFrame(animationRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
kebabRef.current &&
|
||||
!kebabRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setOpenKebabId(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Add event listener when component mounts
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
|
||||
// Clean up event listener when component unmounts
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const zoneEntries = Object.entries(zones);
|
||||
if (zoneEntries.length === 0) return null;
|
||||
const [zoneName, zone] = zoneEntries[0];
|
||||
|
||||
function handleDuplicate(zoneName: string, index: number) {
|
||||
setOpenKebabId(null);
|
||||
duplicateObject(zoneName, index,projectId); // Call the duplicateObject method from the store
|
||||
setSelectedChartId(null);
|
||||
}
|
||||
|
||||
async function handleDelete(zoneName: string, id: string) {
|
||||
try {
|
||||
setSelectedChartId(null);
|
||||
const email = localStorage.getItem("email") || "";
|
||||
const organization = email?.split("@")[1]?.split(".")[0];
|
||||
const userId = localStorage.getItem("userId");
|
||||
|
||||
let deleteFloatingWidget = {
|
||||
floatWidgetID: id,
|
||||
organization: organization,
|
||||
zoneUuid: zone.zoneUuid,
|
||||
userId,projectId
|
||||
};
|
||||
|
||||
if (visualizationSocket) {
|
||||
visualizationSocket.emit("v1:viz-float:delete", deleteFloatingWidget);
|
||||
}
|
||||
deleteObject(zoneName, id);
|
||||
} catch (error) {
|
||||
echo.error("Failed to delete widget");
|
||||
}
|
||||
}
|
||||
|
||||
const handlePointerDown = (event: React.PointerEvent, index: number) => {
|
||||
if (
|
||||
(event.target as HTMLElement).closest(".kebab-options") ||
|
||||
(event.target as HTMLElement).closest(".kebab")
|
||||
) {
|
||||
return; // Prevent dragging when clicking on the kebab menu or its options
|
||||
}
|
||||
const obj = zone.objects[index];
|
||||
const element = event.currentTarget as HTMLElement;
|
||||
element.setPointerCapture(event.pointerId);
|
||||
const container = document.getElementById("work-space-three-d-canvas");
|
||||
if (!container) return;
|
||||
|
||||
const rect = container.getBoundingClientRect();
|
||||
const relativeX = event.clientX - rect.left;
|
||||
const relativeY = event.clientY - rect.top;
|
||||
|
||||
// Determine active properties for the initial position
|
||||
const [activeProp1, activeProp2] = getActiveProperties(obj.position);
|
||||
|
||||
// Set active edges for distance lines
|
||||
const vertical = activeProp1 === "top" ? "top" : "bottom";
|
||||
const horizontal = activeProp2 === "left" ? "left" : "right";
|
||||
setActiveEdges({ vertical, horizontal });
|
||||
|
||||
// Store the initial position strategy and active edges
|
||||
setDraggingIndex({
|
||||
zone: zoneName,
|
||||
index,
|
||||
initialPosition: { ...obj.position },
|
||||
});
|
||||
|
||||
// Calculate offset from mouse to object edges
|
||||
let offsetX = 0;
|
||||
let offsetY = 0;
|
||||
|
||||
if (activeProp1 === "top") {
|
||||
offsetY = relativeY - (obj.position.top as number);
|
||||
} else {
|
||||
offsetY = rect.height - relativeY - (obj.position.bottom as number);
|
||||
}
|
||||
|
||||
if (activeProp2 === "left") {
|
||||
offsetX = relativeX - (obj.position.left as number);
|
||||
} else {
|
||||
offsetX = rect.width - relativeX - (obj.position.right as number);
|
||||
}
|
||||
|
||||
setOffset([offsetY, offsetX]);
|
||||
|
||||
// Add native event listeners for smoother tracking
|
||||
const handlePointerMoveNative = (e: PointerEvent) => {
|
||||
if (!draggingIndex || !offset) return;
|
||||
if (isPlaying === true) return;
|
||||
|
||||
const rect = container.getBoundingClientRect();
|
||||
const relativeX = e.clientX - rect.left;
|
||||
const relativeY = e.clientY - rect.top;
|
||||
|
||||
// Use requestAnimationFrame for smooth updates
|
||||
animationRef.current = requestAnimationFrame(() => {
|
||||
// Dynamically determine the current position strategy
|
||||
const newPositionStrategy = determinePosition(
|
||||
rect,
|
||||
relativeX,
|
||||
relativeY
|
||||
);
|
||||
const [activeProp1, activeProp2] =
|
||||
getActiveProperties(newPositionStrategy);
|
||||
|
||||
// Update active edges for distance lines
|
||||
const vertical = activeProp1 === "top" ? "top" : "bottom";
|
||||
const horizontal = activeProp2 === "left" ? "left" : "right";
|
||||
setActiveEdges({ vertical, horizontal });
|
||||
|
||||
// Calculate new position based on the active properties
|
||||
let newY = 0;
|
||||
let newX = 0;
|
||||
|
||||
if (activeProp1 === "top") {
|
||||
newY = relativeY - offset[0];
|
||||
} else {
|
||||
newY = rect.height - (relativeY + offset[0]);
|
||||
}
|
||||
|
||||
if (activeProp2 === "left") {
|
||||
newX = relativeX - offset[1];
|
||||
} else {
|
||||
newX = rect.width - (relativeX + offset[1]);
|
||||
}
|
||||
|
||||
// Apply boundaries
|
||||
newX = Math.max(0, Math.min(rect.width - 50, newX));
|
||||
newY = Math.max(0, Math.min(rect.height - 50, newY));
|
||||
|
||||
// Create new position object
|
||||
const newPosition = {
|
||||
...newPositionStrategy,
|
||||
[activeProp1]: newY,
|
||||
[activeProp2]: newX,
|
||||
// Clear opposite properties
|
||||
[activeProp1 === "top" ? "bottom" : "top"]: "auto",
|
||||
[activeProp2 === "left" ? "right" : "left"]: "auto",
|
||||
};
|
||||
|
||||
// Update the current position state for DistanceLines
|
||||
setCurrentPosition(newPosition);
|
||||
updateObjectPosition(zoneName, draggingIndex.index, newPosition);
|
||||
});
|
||||
};
|
||||
|
||||
const handlePointerUpNative = async (e: PointerEvent) => {
|
||||
// Clean up native event listeners
|
||||
element.removeEventListener("pointermove", handlePointerMoveNative);
|
||||
element.removeEventListener("pointerup", handlePointerUpNative);
|
||||
element.releasePointerCapture(e.pointerId);
|
||||
|
||||
if (!draggingIndex || !offset) return;
|
||||
|
||||
const rect = container.getBoundingClientRect();
|
||||
const relativeX = e.clientX - rect.left;
|
||||
const relativeY = e.clientY - rect.top;
|
||||
|
||||
// Determine final position strategy
|
||||
const finalPosition = determinePosition(rect, relativeX, relativeY);
|
||||
const [activeProp1, activeProp2] = getActiveProperties(finalPosition);
|
||||
|
||||
// Calculate final position
|
||||
let finalY = 0;
|
||||
let finalX = 0;
|
||||
|
||||
if (activeProp1 === "top") {
|
||||
finalY = relativeY - offset[0];
|
||||
} else {
|
||||
finalY = rect.height - (relativeY + offset[0]);
|
||||
}
|
||||
|
||||
if (activeProp2 === "left") {
|
||||
finalX = relativeX - offset[1];
|
||||
} else {
|
||||
finalX = rect.width - (relativeX + offset[1]);
|
||||
}
|
||||
|
||||
// Apply boundaries
|
||||
finalX = Math.max(0, Math.min(rect.width - 50, finalX));
|
||||
finalY = Math.max(0, Math.min(rect.height - 50, finalY));
|
||||
|
||||
const boundedPosition = {
|
||||
...finalPosition,
|
||||
[activeProp1]: finalY,
|
||||
[activeProp2]: finalX,
|
||||
[activeProp1 === "top" ? "bottom" : "top"]: "auto",
|
||||
[activeProp2 === "left" ? "right" : "left"]: "auto",
|
||||
};
|
||||
|
||||
try {
|
||||
// Save to backend
|
||||
const email = localStorage.getItem("email") || "";
|
||||
const organization = email?.split("@")[1]?.split(".")[0];
|
||||
const response = await addingFloatingWidgets(
|
||||
zone.zoneUuid,
|
||||
organization,
|
||||
{
|
||||
...zone.objects[draggingIndex.index],
|
||||
position: boundedPosition,
|
||||
}
|
||||
);
|
||||
|
||||
if (response.message === "Widget updated successfully") {
|
||||
updateObjectPosition(zoneName, draggingIndex.index, boundedPosition);
|
||||
}
|
||||
} catch (error) {
|
||||
echo.error("Failed to add widget");
|
||||
console.log(error);
|
||||
} finally {
|
||||
setDraggingIndex(null);
|
||||
setOffset(null);
|
||||
setActiveEdges(null);
|
||||
setCurrentPosition(null);
|
||||
|
||||
if (animationRef.current) {
|
||||
cancelAnimationFrame(animationRef.current);
|
||||
animationRef.current = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Add native event listeners
|
||||
element.addEventListener("pointermove", handlePointerMoveNative);
|
||||
element.addEventListener("pointerup", handlePointerUpNative);
|
||||
};
|
||||
|
||||
const handlePointerMove = (event: React.PointerEvent) => {
|
||||
if (!draggingIndex || !offset) return;
|
||||
if (isPlaying === true) return;
|
||||
const container = document.getElementById("work-space-three-d-canvas");
|
||||
if (!container) return;
|
||||
|
||||
const rect = container.getBoundingClientRect();
|
||||
const relativeX = event.clientX - rect.left;
|
||||
const relativeY = event.clientY - rect.top;
|
||||
|
||||
// Dynamically determine the current position strategy
|
||||
const newPositionStrategy = determinePosition(rect, relativeX, relativeY);
|
||||
const [activeProp1, activeProp2] = getActiveProperties(newPositionStrategy);
|
||||
|
||||
// Update active edges for distance lines
|
||||
const vertical = activeProp1 === "top" ? "top" : "bottom";
|
||||
const horizontal = activeProp2 === "left" ? "left" : "right";
|
||||
setActiveEdges({ vertical, horizontal });
|
||||
|
||||
// Calculate new position based on the active properties
|
||||
let newY = 0;
|
||||
let newX = 0;
|
||||
|
||||
if (activeProp1 === "top") {
|
||||
newY = relativeY - offset[0];
|
||||
} else {
|
||||
newY = rect.height - (relativeY + offset[0]);
|
||||
}
|
||||
|
||||
if (activeProp2 === "left") {
|
||||
newX = relativeX - offset[1];
|
||||
} else {
|
||||
newX = rect.width - (relativeX + offset[1]);
|
||||
}
|
||||
|
||||
// Apply boundaries
|
||||
newX = Math.max(0, Math.min(rect.width - 50, newX));
|
||||
newY = Math.max(0, Math.min(rect.height - 50, newY));
|
||||
|
||||
// Create new position object
|
||||
const newPosition = {
|
||||
...newPositionStrategy,
|
||||
[activeProp1]: newY,
|
||||
[activeProp2]: newX,
|
||||
// Clear opposite properties
|
||||
[activeProp1 === "top" ? "bottom" : "top"]: "auto",
|
||||
[activeProp2 === "left" ? "right" : "left"]: "auto",
|
||||
};
|
||||
|
||||
// Update the current position state for DistanceLines
|
||||
setCurrentPosition(newPosition);
|
||||
// Update position immediately without animation frame
|
||||
updateObjectPosition(zoneName, draggingIndex.index, newPosition);
|
||||
};
|
||||
|
||||
const handlePointerUp = async (event: React.PointerEvent<HTMLDivElement>) => {
|
||||
try {
|
||||
if (!draggingIndex || !offset) return;
|
||||
if (isPlaying === true) return;
|
||||
|
||||
const container = document.getElementById("work-space-three-d-canvas");
|
||||
if (!container) return;
|
||||
|
||||
const rect = container.getBoundingClientRect();
|
||||
const relativeX = event.clientX - rect.left;
|
||||
const relativeY = event.clientY - rect.top;
|
||||
|
||||
// Determine final position strategy
|
||||
const finalPosition = determinePosition(rect, relativeX, relativeY);
|
||||
const [activeProp1, activeProp2] = getActiveProperties(finalPosition);
|
||||
|
||||
// Calculate final position
|
||||
let finalY = 0;
|
||||
let finalX = 0;
|
||||
|
||||
if (activeProp1 === "top") {
|
||||
finalY = relativeY - offset[0];
|
||||
} else {
|
||||
finalY = rect.height - (relativeY + offset[0]);
|
||||
}
|
||||
|
||||
if (activeProp2 === "left") {
|
||||
finalX = relativeX - offset[1];
|
||||
} else {
|
||||
finalX = rect.width - (relativeX + offset[1]);
|
||||
}
|
||||
|
||||
// Apply boundaries
|
||||
finalX = Math.max(0, Math.min(rect.width - 50, finalX));
|
||||
finalY = Math.max(0, Math.min(rect.height - 50, finalY));
|
||||
|
||||
const boundedPosition = {
|
||||
...finalPosition,
|
||||
[activeProp1]: finalY,
|
||||
[activeProp2]: finalX,
|
||||
// Clear opposite properties
|
||||
[activeProp1 === "top" ? "bottom" : "top"]: "auto",
|
||||
[activeProp2 === "left" ? "right" : "left"]: "auto",
|
||||
};
|
||||
|
||||
// Save to backend
|
||||
const email = localStorage.getItem("email") || "";
|
||||
const organization = email?.split("@")[1]?.split(".")[0];
|
||||
let updateFloatingWidget = {
|
||||
organization: organization,
|
||||
widget: {
|
||||
...zone.objects[draggingIndex.index],
|
||||
position: boundedPosition,
|
||||
},
|
||||
index: draggingIndex.index,
|
||||
zoneUuid: zone.zoneUuid,
|
||||
};
|
||||
if (visualizationSocket) {
|
||||
visualizationSocket.emit("v2:viz-float:add", updateFloatingWidget);
|
||||
}
|
||||
|
||||
updateObjectPosition(zoneName, draggingIndex.index, boundedPosition);
|
||||
} catch (error) {
|
||||
echo.error("Failed to add widget");
|
||||
console.log(error);
|
||||
} finally {
|
||||
// Clean up regardless of success or failure
|
||||
setDraggingIndex(null);
|
||||
setOffset(null);
|
||||
setActiveEdges(null);
|
||||
setCurrentPosition(null);
|
||||
|
||||
// Cancel any pending animation frame
|
||||
if (animationRef.current) {
|
||||
cancelAnimationFrame(animationRef.current);
|
||||
animationRef.current = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleKebabClick = (id: string, event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
setOpenKebabId((prevId) => (prevId === id ? null : id));
|
||||
};
|
||||
|
||||
const containerHeight = getComputedStyle(
|
||||
document.documentElement
|
||||
).getPropertyValue("--realTimeViz-container-height");
|
||||
const containerWidth = getComputedStyle(
|
||||
document.documentElement
|
||||
).getPropertyValue("--realTimeViz-container-width");
|
||||
|
||||
const heightMultiplier = parseFloat(containerHeight) * 0.14;
|
||||
|
||||
const widthMultiplier = parseFloat(containerWidth) * 0.13;
|
||||
|
||||
return (
|
||||
<div
|
||||
onPointerMove={handlePointerMove}
|
||||
onPointerUp={handlePointerUp}
|
||||
className="floating-wrapper"
|
||||
>
|
||||
{zone?.objects?.map((obj, index) => {
|
||||
const topPosition =
|
||||
typeof obj?.position?.top === "number"
|
||||
? `calc(${obj?.position?.top}px + ${
|
||||
isPlaying && selectedZone?.activeSides?.includes("top")
|
||||
? `${heightMultiplier - 55}px`
|
||||
: "0px"
|
||||
})`
|
||||
: "auto";
|
||||
|
||||
const leftPosition =
|
||||
typeof obj?.position?.left === "number"
|
||||
? `calc(${obj?.position?.left}px + ${
|
||||
isPlaying && selectedZone?.activeSides?.includes("left")
|
||||
? `${widthMultiplier - 150}px`
|
||||
: "0px"
|
||||
})`
|
||||
: "auto";
|
||||
|
||||
const rightPosition =
|
||||
typeof obj?.position?.right === "number"
|
||||
? `calc(${obj?.position?.right}px + ${
|
||||
isPlaying && selectedZone?.activeSides?.includes("right")
|
||||
? `${widthMultiplier - 150}px`
|
||||
: "0px"
|
||||
})`
|
||||
: "auto";
|
||||
const bottomPosition =
|
||||
typeof obj?.position?.bottom === "number"
|
||||
? `calc(${obj?.position?.bottom}px + ${
|
||||
isPlaying && selectedZone?.activeSides?.includes("bottom")
|
||||
? `${heightMultiplier - 55}px`
|
||||
: "0px"
|
||||
})`
|
||||
: "auto";
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`${obj.id}-${index}`}
|
||||
className={`${obj.className} ${
|
||||
selectedChartId?.id === obj.id && "activeChart"
|
||||
} `}
|
||||
ref={chartWidget}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: topPosition,
|
||||
left: leftPosition,
|
||||
right: rightPosition,
|
||||
bottom: bottomPosition,
|
||||
pointerEvents: isPlaying ? "none" : "auto",
|
||||
minHeight: `${
|
||||
obj.className === "warehouseThroughput" && "150px !important"
|
||||
} `,
|
||||
}}
|
||||
onPointerDown={(event) => {
|
||||
setSelectedChartId(obj);
|
||||
handlePointerDown(event, index);
|
||||
}}
|
||||
>
|
||||
{obj.className === "floating total-card" ? (
|
||||
<TotalCardComponent object={obj} />
|
||||
) : obj.className === "warehouseThroughput floating" ? (
|
||||
<WarehouseThroughputComponent object={obj} />
|
||||
) : obj.className === "fleetEfficiency floating" ? (
|
||||
<FleetEfficiencyComponent object={obj} />
|
||||
) : null}
|
||||
|
||||
<div
|
||||
className="icon kebab"
|
||||
ref={kebabRef}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleKebabClick(obj.id, event);
|
||||
}}
|
||||
>
|
||||
<KebabIcon />
|
||||
</div>
|
||||
|
||||
{openKebabId === obj.id && (
|
||||
<div className="kebab-options" ref={kebabRef}>
|
||||
<div
|
||||
className="dublicate btn"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleDuplicate(zoneName, index); // Call the duplicate handler
|
||||
}}
|
||||
>
|
||||
<div className="icon">
|
||||
<DublicateIcon />
|
||||
</div>
|
||||
<div className="label">Duplicate</div>
|
||||
</div>
|
||||
<div
|
||||
className="edit btn"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleDelete(zoneName, obj.id); // Call the delete handler
|
||||
}}
|
||||
>
|
||||
<div className="icon">
|
||||
<DeleteIcon />
|
||||
</div>
|
||||
<div className="label">Delete</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Render DistanceLines component during drag */}
|
||||
{isPlaying === false &&
|
||||
draggingIndex !== null &&
|
||||
activeEdges !== null &&
|
||||
currentPosition !== null && (
|
||||
<DistanceLines
|
||||
obj={{
|
||||
position: {
|
||||
top:
|
||||
currentPosition.top !== "auto"
|
||||
? currentPosition.top
|
||||
: undefined,
|
||||
bottom:
|
||||
currentPosition.bottom !== "auto"
|
||||
? currentPosition.bottom
|
||||
: undefined,
|
||||
left:
|
||||
currentPosition.left !== "auto"
|
||||
? currentPosition.left
|
||||
: undefined,
|
||||
right:
|
||||
currentPosition.right !== "auto"
|
||||
? currentPosition.right
|
||||
: undefined,
|
||||
},
|
||||
}}
|
||||
activeEdges={activeEdges}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DroppedObjects;
|
||||
@@ -0,0 +1,49 @@
|
||||
const FleetEfficiency = () => {
|
||||
const progress = 50; // Example progress value (0-100)
|
||||
|
||||
// Calculate the rotation angle for the progress bar
|
||||
const rotationAngle = 45 + progress * 1.8;
|
||||
|
||||
const handleDragStart = (event: React.DragEvent<HTMLDivElement>) => {
|
||||
const rect = event.currentTarget.getBoundingClientRect(); // Get position
|
||||
|
||||
const cardData = JSON.stringify({
|
||||
className: event.currentTarget.className,
|
||||
position: [rect.top, rect.left], // Store position
|
||||
value: rotationAngle, // Example value (you can change if dynamic)
|
||||
per: progress,
|
||||
|
||||
});
|
||||
event.dataTransfer.setData("text/plain", cardData);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className="fleetEfficiency floating" draggable onDragStart={handleDragStart}>
|
||||
<h2 className="header">Fleet Efficiency</h2>
|
||||
|
||||
<div className="progressContainer">
|
||||
<div className="progress">
|
||||
<div className="barOverflow">
|
||||
{/* Apply dynamic rotation to the bar */}
|
||||
<div
|
||||
className="bar"
|
||||
style={{ transform: `rotate(${rotationAngle}deg)` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="scaleLabels">
|
||||
<span>0%</span>
|
||||
<div className="centerText">
|
||||
<div className="percentage">{progress}%</div>
|
||||
<div className="status">Optimal</div>
|
||||
</div>
|
||||
<span>100%</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FleetEfficiency;
|
||||
@@ -0,0 +1,118 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Line } from "react-chartjs-2";
|
||||
import useChartStore from "../../../../../store/visualization/useChartStore";
|
||||
import { useWidgetStore } from "../../../../../store/useWidgetStore";
|
||||
import axios from "axios";
|
||||
import io from "socket.io-client";
|
||||
import { usePlayButtonStore } from "../../../../../store/usePlayButtonStore";
|
||||
|
||||
const FleetEfficiencyComponent = ({ object }: any) => {
|
||||
const [progress, setProgress] = useState<any>(0);
|
||||
const [measurements, setmeasurements] = useState<any>({});
|
||||
const [duration, setDuration] = useState("1h");
|
||||
const [name, setName] = useState(object.header ? object.header : "");
|
||||
const email = localStorage.getItem("email") || "";
|
||||
const organization = email?.split("@")[1]?.split(".")[0];
|
||||
const { header, flotingDuration, flotingMeasurements } = useChartStore();
|
||||
const { selectedChartId } = useWidgetStore();
|
||||
const { isPlaying } = usePlayButtonStore();
|
||||
|
||||
const iotApiUrl = process.env.REACT_APP_IOT_SOCKET_SERVER_URL;
|
||||
|
||||
// Calculate the rotation angle for the progress bar
|
||||
const rotationAngle = 45 + progress * 1.8;
|
||||
|
||||
useEffect(() => {
|
||||
if (!iotApiUrl || !measurements || Object.keys(measurements).length === 0)
|
||||
return;
|
||||
|
||||
const socket = io(`http://${iotApiUrl}`);
|
||||
|
||||
const inputData = {
|
||||
measurements,
|
||||
duration,
|
||||
interval: 1000,
|
||||
};
|
||||
|
||||
const startStream = () => {
|
||||
socket.emit("lastInput", inputData);
|
||||
};
|
||||
|
||||
socket.on("connect", startStream);
|
||||
|
||||
socket.on("lastOutput", (response) => {
|
||||
const responseData = response.input1;
|
||||
// console.log(responseData);
|
||||
|
||||
if (typeof responseData === "number") {
|
||||
console.log("It's a number!");
|
||||
setProgress(responseData);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket.off("lastOutput");
|
||||
socket.emit("stop_stream"); // Stop streaming when component unmounts
|
||||
socket.disconnect();
|
||||
};
|
||||
}, [measurements, duration, iotApiUrl]);
|
||||
|
||||
const fetchSavedInputes = async () => {
|
||||
if (object?.id !== "") {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}/api/v2/A_floatWidget/${object?.id}/${organization}`
|
||||
);
|
||||
if (response.status === 200) {
|
||||
setmeasurements(response.data.Data.measurements);
|
||||
setDuration(response.data.Data.duration);
|
||||
setName(response.data.header);
|
||||
} else {
|
||||
console.log("Unexpected response:", response);
|
||||
}
|
||||
} catch (error) {
|
||||
echo.error("Failed to fetch saved inputs");
|
||||
console.error("There was an error!", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchSavedInputes();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedChartId?.id === object?.id) {
|
||||
fetchSavedInputes();
|
||||
}
|
||||
}, [header, flotingDuration, flotingMeasurements]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2 className="header">{name}</h2>
|
||||
<div
|
||||
className="progressContainer"
|
||||
style={{ transform: isPlaying ? "skew(-14deg, 0deg)" : "none" }}
|
||||
>
|
||||
<div className="progress">
|
||||
<div className="barOverflow">
|
||||
<div
|
||||
className="bar"
|
||||
style={{ transform: `rotate(${rotationAngle}deg)` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="scaleLabels">
|
||||
<span>0%</span>
|
||||
<div className="centerText">
|
||||
<div className="percentage">{progress}%</div>
|
||||
<div className="status">Optimal</div>
|
||||
</div>
|
||||
<span>100%</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FleetEfficiencyComponent;
|
||||
@@ -0,0 +1,86 @@
|
||||
import React from "react";
|
||||
|
||||
|
||||
interface ProductivityData {
|
||||
distancePerTask: number;
|
||||
spaceUtilization: number;
|
||||
taskCompletionTime: string;
|
||||
}
|
||||
|
||||
const ProductivityDashboard: React.FC = () => {
|
||||
const data: ProductivityData = {
|
||||
distancePerTask: 45,
|
||||
spaceUtilization: 72,
|
||||
taskCompletionTime: "7:44",
|
||||
};
|
||||
|
||||
// Function to calculate the stroke dash offset for the circular progress
|
||||
const calculateDashOffset = (percentage: number, circumference: number) => {
|
||||
return circumference - (percentage / 100) * circumference;
|
||||
};
|
||||
|
||||
// Constants for the circular progress chart
|
||||
const radius = 60; // Radius of the circle
|
||||
const strokeWidth = 10; // Thickness of the stroke
|
||||
const diameter = radius * 2; // Diameter of the circle
|
||||
const circumference = Math.PI * (radius * 2); // Circumference of the circle
|
||||
|
||||
return (
|
||||
<div className="productivity-dashboard">
|
||||
<header>
|
||||
<h2>Productivity</h2>
|
||||
<div className="options">...</div>
|
||||
</header>
|
||||
<main>
|
||||
<section className="metrics">
|
||||
<div className="metric">
|
||||
<p className="label">Distance per Task</p>
|
||||
<p className="value">{data.distancePerTask} m</p>
|
||||
</div>
|
||||
<div className="metric">
|
||||
<p className="label">Space Utilization</p>
|
||||
<p className="value">{data.spaceUtilization}%</p>
|
||||
</div>
|
||||
</section>
|
||||
<section className="chart-section">
|
||||
<svg
|
||||
className="progress-circle"
|
||||
width={diameter}
|
||||
height={diameter}
|
||||
viewBox={`0 0 ${diameter} ${diameter}`}
|
||||
>
|
||||
{/* Background Circle */}
|
||||
<circle
|
||||
cx={radius}
|
||||
cy={radius}
|
||||
r={radius - strokeWidth / 2}
|
||||
fill="transparent"
|
||||
stroke="#6c757d"
|
||||
strokeWidth={strokeWidth}
|
||||
/>
|
||||
{/* Progress Circle */}
|
||||
<circle
|
||||
cx={radius}
|
||||
cy={radius}
|
||||
r={radius - strokeWidth / 2}
|
||||
fill="transparent"
|
||||
stroke="#2ecc71"
|
||||
strokeWidth={strokeWidth}
|
||||
strokeDasharray={circumference}
|
||||
strokeDashoffset={calculateDashOffset(data.spaceUtilization, circumference)}
|
||||
strokeLinecap="round"
|
||||
transform={`rotate(-90 ${radius} ${radius})`}
|
||||
/>
|
||||
</svg>
|
||||
<div className="chart-details">
|
||||
<p className="title">Task Completion Time</p>
|
||||
<p className="time">{data.taskCompletionTime}</p>
|
||||
<p className="subtitle">Total Score</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductivityDashboard;
|
||||
@@ -0,0 +1,58 @@
|
||||
import React from "react";
|
||||
|
||||
interface SimpleCardProps {
|
||||
header: string;
|
||||
icon: React.ElementType; // React component for SVG icon
|
||||
iconName?: string;
|
||||
value: string;
|
||||
per: string; // Percentage change
|
||||
position?: [number, number];
|
||||
}
|
||||
|
||||
const SimpleCard: React.FC<SimpleCardProps> = ({
|
||||
header,
|
||||
icon: Icon,
|
||||
iconName,
|
||||
value,
|
||||
per,
|
||||
position = [0, 0],
|
||||
}) => {
|
||||
const handleDragStart = (event: React.DragEvent<HTMLDivElement>) => {
|
||||
const rect = event.currentTarget.getBoundingClientRect(); // Get position
|
||||
|
||||
const cardData = JSON.stringify({
|
||||
header,
|
||||
value,
|
||||
per,
|
||||
iconName: iconName || "UnknownIcon", // Use the custom iconName
|
||||
className: event.currentTarget.className,
|
||||
position: [rect.top, rect.left], // ✅ Store position
|
||||
});
|
||||
|
||||
event.dataTransfer.setData("text/plain", cardData);
|
||||
// console.log('cardData: ', cardData);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="floating total-card"
|
||||
draggable
|
||||
onDragStart={handleDragStart}
|
||||
style={{ top: position[0], left: position[1] }} // No need for ?? 0 if position is guaranteed
|
||||
>
|
||||
<div className="header-wrapper">
|
||||
<div className="header">{header}</div>
|
||||
<div className="data-values">
|
||||
<div className="value">{value}</div>
|
||||
<div className="per">{per}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="icon">
|
||||
<Icon />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SimpleCard;
|
||||
@@ -0,0 +1,115 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Line } from "react-chartjs-2";
|
||||
import useChartStore from "../../../../../store/visualization/useChartStore";
|
||||
import { useWidgetStore } from "../../../../../store/useWidgetStore";
|
||||
import axios from "axios";
|
||||
import io from "socket.io-client";
|
||||
import {
|
||||
CartIcon,
|
||||
DocumentIcon,
|
||||
GlobeIcon,
|
||||
WalletIcon,
|
||||
} from "../../../../../components/icons/3dChartIcons";
|
||||
|
||||
const TotalCardComponent = ({ object }: any) => {
|
||||
const [progress, setProgress] = useState<any>(0);
|
||||
const [measurements, setmeasurements] = useState<any>({});
|
||||
const [duration, setDuration] = useState("1h");
|
||||
const [name, setName] = useState(object.header ? object.header : "");
|
||||
|
||||
const email = localStorage.getItem("email") || "";
|
||||
const organization = email?.split("@")[1]?.split(".")[0];
|
||||
const { header, flotingDuration, flotingMeasurements } = useChartStore();
|
||||
const { selectedChartId } = useWidgetStore();
|
||||
|
||||
const iotApiUrl = process.env.REACT_APP_IOT_SOCKET_SERVER_URL;
|
||||
|
||||
useEffect(() => {
|
||||
if (!iotApiUrl || !measurements || Object.keys(measurements).length === 0)
|
||||
return;
|
||||
|
||||
const socket = io(`http://${iotApiUrl}`);
|
||||
|
||||
const inputData = {
|
||||
measurements,
|
||||
duration,
|
||||
interval: 1000,
|
||||
};
|
||||
|
||||
const startStream = () => {
|
||||
socket.emit("lastInput", inputData);
|
||||
};
|
||||
|
||||
socket.on("connect", startStream);
|
||||
|
||||
socket.on("lastOutput", (response) => {
|
||||
const responseData = response.input1;
|
||||
|
||||
if (typeof responseData === "number") {
|
||||
setProgress(responseData);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket.off("lastOutput");
|
||||
socket.emit("stop_stream"); // Stop streaming when component unmounts
|
||||
socket.disconnect();
|
||||
};
|
||||
}, [measurements, duration, iotApiUrl]);
|
||||
|
||||
const fetchSavedInputes = async () => {
|
||||
if (object?.id !== "") {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}/api/v2/A_floatWidget/${object?.id}/${organization}`
|
||||
);
|
||||
if (response.status === 200) {
|
||||
setmeasurements(response.data.Data.measurements);
|
||||
setDuration(response.data.Data.duration);
|
||||
setName(response.data.header);
|
||||
} else {
|
||||
echo.error("Failed to fetch saved inputs");
|
||||
}
|
||||
} catch (error) {}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchSavedInputes();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedChartId?.id === object?.id) {
|
||||
fetchSavedInputes();
|
||||
}
|
||||
}, [header, flotingDuration, flotingMeasurements]);
|
||||
|
||||
const mapIcon = (iconName: string) => {
|
||||
switch (iconName) {
|
||||
case "WalletIcon":
|
||||
return <WalletIcon />;
|
||||
case "GlobeIcon":
|
||||
return <GlobeIcon />;
|
||||
case "DocumentIcon":
|
||||
return <DocumentIcon />;
|
||||
case "CartIcon":
|
||||
return <CartIcon />;
|
||||
default:
|
||||
return <WalletIcon />;
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<div className="header-wrapper">
|
||||
<div className="header">{name}</div>
|
||||
<div className="data-values">
|
||||
<div className="value">{progress}</div>
|
||||
<div className="per">{object.per}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="icon">{mapIcon(object.iconName)}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TotalCardComponent;
|
||||
@@ -0,0 +1,149 @@
|
||||
import React from "react";
|
||||
import { Line } from "react-chartjs-2";
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Filler, // Import Filler for area fill
|
||||
} from "chart.js";
|
||||
|
||||
// Register ChartJS components
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Filler
|
||||
);
|
||||
|
||||
const WarehouseThroughput = () => {
|
||||
// Line graph data for a year (monthly throughput)
|
||||
const lineGraphData = {
|
||||
labels: [
|
||||
"Jan",
|
||||
"Feb",
|
||||
"Mar",
|
||||
"Apr",
|
||||
"May",
|
||||
"Jun",
|
||||
"Jul",
|
||||
"Aug",
|
||||
"Sep",
|
||||
"Oct",
|
||||
"Nov",
|
||||
"Dec",
|
||||
], // Months of the year
|
||||
datasets: [
|
||||
{
|
||||
label: "Throughput (units/month)",
|
||||
data: [500, 400, 300, 450, 350, 250, 200, 300, 250, 150, 100, 150], // Example monthly data
|
||||
borderColor: "#6f42c1", // Use the desired color for the line (purple)
|
||||
backgroundColor: "rgba(111, 66, 193, 0.2)", // Use a semi-transparent purple for the fill
|
||||
borderWidth: 2, // Line thickness
|
||||
fill: true, // Enable fill for this dataset
|
||||
pointRadius: 0, // Remove dots at each data point
|
||||
tension: 0.5, // Smooth interpolation for the line
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Line graph options
|
||||
const lineGraphOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false, // Allow custom height/width adjustments
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false, // Hide legend
|
||||
},
|
||||
title: {
|
||||
display: false, // No chart title needed
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (context: any) => {
|
||||
const value = context.parsed.y;
|
||||
return `${value} units`; // Customize tooltip to display "units"
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
display: false, // Hide x-axis grid lines
|
||||
},
|
||||
ticks: {
|
||||
maxRotation: 0, // Prevent label rotation
|
||||
autoSkip: false, // Display all months
|
||||
font: {
|
||||
size: 8, // Adjust font size for readability
|
||||
color: "#ffffff", // Light text color for labels
|
||||
},
|
||||
},
|
||||
},
|
||||
y: {
|
||||
display: true, // Show y-axis
|
||||
grid: {
|
||||
drawBorder: false, // Remove border line
|
||||
color: "rgba(255, 255, 255, 0.2)", // Light gray color for grid lines
|
||||
borderDash: [5, 5], // Dotted line style (array defines dash and gap lengths)
|
||||
},
|
||||
ticks: {
|
||||
font: {
|
||||
size: 8, // Adjust font size for readability
|
||||
color: "#ffffff", // Light text color for ticks
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
elements: {
|
||||
line: {
|
||||
tension: 0.5, // Smooth interpolation for the line
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const handleDragStart = (event: React.DragEvent<HTMLDivElement>) => {
|
||||
const rect = event.currentTarget.getBoundingClientRect(); // Get element position
|
||||
|
||||
const cardData = JSON.stringify({
|
||||
header: "Warehouse Throughput", // Static header
|
||||
value: "+5", // Example value (you can change if dynamic)
|
||||
per: "2025", // Example percentage or date
|
||||
icon: "📊", // Placeholder for an icon (if needed)
|
||||
className: event.currentTarget.className,
|
||||
position: [rect.top, rect.left], // ✅ Store initial position
|
||||
lineGraphData, // ✅ Include chart data
|
||||
lineGraphOptions, // ✅ Include chart options
|
||||
});
|
||||
|
||||
|
||||
event.dataTransfer.setData("text/plain", cardData);
|
||||
// event.dataTransfer.effectAllowed = "move"; // Improve drag effect
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="warehouseThroughput floating" style={{ minHeight: "160px !important" }} draggable onDragStart={handleDragStart}>
|
||||
<div className="header">
|
||||
<h2>Warehouse Throughput</h2>
|
||||
<p>
|
||||
<span>(+5) more</span> in 2025
|
||||
</p>
|
||||
</div>
|
||||
<div className="lineGraph" style={{ height: "100%" }}>
|
||||
{/* Line Graph */}
|
||||
<Line data={lineGraphData} options={lineGraphOptions} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WarehouseThroughput;
|
||||
@@ -0,0 +1,209 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Line } from "react-chartjs-2";
|
||||
import useChartStore from "../../../../../store/visualization/useChartStore";
|
||||
import { useWidgetStore } from "../../../../../store/useWidgetStore";
|
||||
import axios from "axios";
|
||||
import io from "socket.io-client";
|
||||
|
||||
const WarehouseThroughputComponent = ({ object }: any) => {
|
||||
const [measurements, setmeasurements] = useState<any>({});
|
||||
const [duration, setDuration] = useState("1h");
|
||||
const [name, setName] = useState(object.header ? object.header : "");
|
||||
const [chartData, setChartData] = useState<{
|
||||
labels: string[];
|
||||
datasets: any[];
|
||||
}>({
|
||||
labels: [],
|
||||
datasets: [],
|
||||
});
|
||||
const email = localStorage.getItem("email") || "";
|
||||
const organization = email?.split("@")[1]?.split(".")[0];
|
||||
const { header, flotingDuration, flotingMeasurements } = useChartStore();
|
||||
const { selectedChartId } = useWidgetStore();
|
||||
|
||||
const iotApiUrl = process.env.REACT_APP_IOT_SOCKET_SERVER_URL;
|
||||
|
||||
const lineGraphData = {
|
||||
labels: [
|
||||
"Jan",
|
||||
"Feb",
|
||||
"Mar",
|
||||
"Apr",
|
||||
"May",
|
||||
"Jun",
|
||||
"Jul",
|
||||
"Aug",
|
||||
"Sep",
|
||||
"Oct",
|
||||
"Nov",
|
||||
"Dec",
|
||||
], // Months of the year
|
||||
datasets: [
|
||||
{
|
||||
label: "Throughput (units/month)",
|
||||
data: [500, 400, 300, 450, 350, 250, 200, 300, 250, 150, 100, 150], // Example monthly data
|
||||
borderColor: "#6f42c1", // Use the desired color for the line (purple)
|
||||
backgroundColor: "rgba(111, 66, 193, 0.2)", // Use a semi-transparent purple for the fill
|
||||
borderWidth: 2, // Line thickness
|
||||
fill: true, // Enable fill for this dataset
|
||||
pointRadius: 0, // Remove dots at each data point
|
||||
tension: 0.5, // Smooth interpolation for the line
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Line graph options
|
||||
const lineGraphOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false, // Allow custom height/width adjustments
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false, // Hide legend
|
||||
},
|
||||
title: {
|
||||
display: false, // No chart title needed
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (context: any) => {
|
||||
const value = context.parsed.y;
|
||||
return `${value} units`; // Customize tooltip to display "units"
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
display: false, // Hide x-axis grid lines
|
||||
},
|
||||
ticks: {
|
||||
maxRotation: 0, // Prevent label rotation
|
||||
autoSkip: false, // Display all months
|
||||
font: {
|
||||
size: 8, // Adjust font size for readability
|
||||
color: "#ffffff", // Light text color for labels
|
||||
},
|
||||
},
|
||||
},
|
||||
y: {
|
||||
display: true, // Show y-axis
|
||||
grid: {
|
||||
drawBorder: false, // Remove border line
|
||||
color: "rgba(255, 255, 255, 0.2)", // Light gray color for grid lines
|
||||
borderDash: [5, 5], // Dotted line style (array defines dash and gap lengths)
|
||||
},
|
||||
ticks: {
|
||||
font: {
|
||||
size: 8, // Adjust font size for readability
|
||||
color: "#ffffff", // Light text color for ticks
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
elements: {
|
||||
line: {
|
||||
tension: 0.5, // Smooth interpolation for the line
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!iotApiUrl || !measurements || Object.keys(measurements).length === 0)
|
||||
return;
|
||||
|
||||
const socket = io(`http://${iotApiUrl}`);
|
||||
|
||||
const inputData = {
|
||||
measurements,
|
||||
duration,
|
||||
interval: 1000,
|
||||
};
|
||||
|
||||
const startStream = () => {
|
||||
socket.emit("lineInput", inputData);
|
||||
};
|
||||
|
||||
socket.on("connect", startStream);
|
||||
|
||||
socket.on("lineOutput", (response) => {
|
||||
const responseData = response.data;
|
||||
|
||||
// Extract timestamps and values
|
||||
const labels = responseData.time;
|
||||
const datasets = Object.keys(measurements).map((key) => {
|
||||
const measurement = measurements[key];
|
||||
const datasetKey = `${measurement.name}.${measurement.fields}`;
|
||||
return {
|
||||
label: datasetKey,
|
||||
data: responseData[datasetKey]?.values ?? [],
|
||||
borderColor: "#6f42c1", // Use the desired color for the line (purple)
|
||||
backgroundColor: "rgba(111, 66, 193, 0.2)", // Use a semi-transparent purple for the fill
|
||||
borderWidth: 2, // Line thickness
|
||||
fill: true, // Enable fill for this dataset
|
||||
pointRadius: 0, // Remove dots at each data point
|
||||
tension: 0.5, // Smooth interpolation for the line
|
||||
};
|
||||
});
|
||||
|
||||
setChartData({ labels, datasets });
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket.off("lineOutput");
|
||||
socket.emit("stop_stream"); // Stop streaming when component unmounts
|
||||
socket.disconnect();
|
||||
};
|
||||
}, [measurements, duration, iotApiUrl]);
|
||||
|
||||
const fetchSavedInputes = async () => {
|
||||
if (object?.id !== "") {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}/api/v2/A_floatWidget/${object?.id}/${organization}`
|
||||
);
|
||||
if (response.status === 200) {
|
||||
setmeasurements(response.data.Data.measurements);
|
||||
setDuration(response.data.Data.duration);
|
||||
setName(response.data.header);
|
||||
} else {
|
||||
console.log("Unexpected response:", response);
|
||||
}
|
||||
} catch (error) {
|
||||
echo.error("Failed to fetch saved inputs");
|
||||
console.error("There was an error!", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchSavedInputes();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedChartId?.id === object?.id) {
|
||||
fetchSavedInputes();
|
||||
}
|
||||
}, [header, flotingDuration, flotingMeasurements]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="header">
|
||||
<h2>{name}</h2>
|
||||
{/* <p>
|
||||
<span>(+5) more</span> in 2025
|
||||
</p> */}
|
||||
</div>
|
||||
<div className="lineGraph" style={{ height: "100%" }}>
|
||||
<Line
|
||||
data={
|
||||
Object.keys(measurements).length > 0 ? chartData : lineGraphData
|
||||
}
|
||||
options={lineGraphOptions}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default WarehouseThroughputComponent;
|
||||
354
app/src/modules/visualization/widgets/panel/AddButtons.tsx
Normal file
354
app/src/modules/visualization/widgets/panel/AddButtons.tsx
Normal file
@@ -0,0 +1,354 @@
|
||||
import React from "react";
|
||||
import {
|
||||
CleanPannel,
|
||||
EyeIcon,
|
||||
LockIcon,
|
||||
} from "../../../../components/icons/RealTimeVisulationIcons";
|
||||
import { AddIcon } from "../../../../components/icons/ExportCommonIcons";
|
||||
import { useSocketStore } from "../../../../store/builder/store";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
// Define the type for `Side`
|
||||
type Side = "top" | "bottom" | "left" | "right";
|
||||
|
||||
// Define the type for HiddenPanels, where keys are zone IDs and values are arrays of hidden sides
|
||||
interface HiddenPanels {
|
||||
[zoneUuid: string]: Side[];
|
||||
}
|
||||
|
||||
// Define the type for the props passed to the Buttons component
|
||||
interface ButtonsProps {
|
||||
selectedZone: {
|
||||
zoneName: string;
|
||||
activeSides: Side[];
|
||||
panelOrder: Side[];
|
||||
lockedPanels: Side[];
|
||||
points: [];
|
||||
zoneUuid: string;
|
||||
zoneViewPortTarget: number[];
|
||||
zoneViewPortPosition: number[];
|
||||
widgets: {
|
||||
id: string;
|
||||
type: string;
|
||||
title: string;
|
||||
panel: Side;
|
||||
data: any;
|
||||
}[];
|
||||
};
|
||||
setSelectedZone: React.Dispatch<
|
||||
React.SetStateAction<{
|
||||
zoneName: string;
|
||||
activeSides: Side[];
|
||||
panelOrder: Side[];
|
||||
lockedPanels: Side[];
|
||||
points: [];
|
||||
zoneUuid: string;
|
||||
zoneViewPortTarget: number[];
|
||||
zoneViewPortPosition: number[];
|
||||
widgets: {
|
||||
id: string;
|
||||
type: string;
|
||||
title: string;
|
||||
panel: Side;
|
||||
data: any;
|
||||
}[];
|
||||
}>
|
||||
>;
|
||||
hiddenPanels: HiddenPanels; // Updated prop type
|
||||
setHiddenPanels: React.Dispatch<React.SetStateAction<HiddenPanels>>; // Updated prop type
|
||||
waitingPanels: any;
|
||||
setWaitingPanels: any;
|
||||
}
|
||||
|
||||
const AddButtons: React.FC<ButtonsProps> = ({
|
||||
selectedZone,
|
||||
setSelectedZone,
|
||||
setHiddenPanels,
|
||||
hiddenPanels,
|
||||
waitingPanels,
|
||||
setWaitingPanels,
|
||||
}) => {
|
||||
const { visualizationSocket } = useSocketStore();
|
||||
const { projectId } = useParams();
|
||||
|
||||
// Function to toggle visibility of a panel
|
||||
const toggleVisibility = (side: Side) => {
|
||||
const isHidden = hiddenPanels[selectedZone.zoneUuid]?.includes(side) ?? false;
|
||||
|
||||
if (isHidden) {
|
||||
// If the panel is already hidden, remove it from the hiddenPanels array for this zone
|
||||
setHiddenPanels((prevHiddenPanels) => ({
|
||||
...prevHiddenPanels,
|
||||
[selectedZone.zoneUuid]: prevHiddenPanels[selectedZone.zoneUuid].filter(
|
||||
(panel) => panel !== side
|
||||
),
|
||||
}));
|
||||
} else {
|
||||
// If the panel is visible, add it to the hiddenPanels array for this zone
|
||||
setHiddenPanels((prevHiddenPanels) => ({
|
||||
...prevHiddenPanels,
|
||||
[selectedZone.zoneUuid]: [
|
||||
...(prevHiddenPanels[selectedZone.zoneUuid] || []),
|
||||
side,
|
||||
],
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
// Function to toggle lock/unlock a panel
|
||||
const toggleLockPanel = async (side: Side) => {
|
||||
const email = localStorage.getItem("email") ?? "";
|
||||
const organization = email?.split("@")[1]?.split(".")[0]; // Fallback value
|
||||
const userId = localStorage.getItem("userId");
|
||||
//add api
|
||||
const newLockedPanels = selectedZone.lockedPanels.includes(side)
|
||||
? selectedZone.lockedPanels.filter((panel) => panel !== side)
|
||||
: [...selectedZone.lockedPanels, side];
|
||||
|
||||
const updatedZone = {
|
||||
...selectedZone,
|
||||
lockedPanels: newLockedPanels,
|
||||
};
|
||||
|
||||
let lockedPanel = {
|
||||
organization: organization,
|
||||
lockedPanel: newLockedPanels,
|
||||
zoneUuid: selectedZone.zoneUuid,
|
||||
userId, projectId
|
||||
};
|
||||
if (visualizationSocket) {
|
||||
visualizationSocket.emit("v1:viz-panel:locked", lockedPanel);
|
||||
}
|
||||
|
||||
setSelectedZone(updatedZone);
|
||||
};
|
||||
|
||||
// Function to clean all widgets from a panel
|
||||
const cleanPanel = async (side: Side) => {
|
||||
//add api
|
||||
// console.log('side: ', side);
|
||||
if (
|
||||
hiddenPanels[selectedZone.zoneUuid]?.includes(side) ||
|
||||
selectedZone.lockedPanels.includes(side)
|
||||
)
|
||||
return;
|
||||
|
||||
const email = localStorage.getItem("email") ?? "";
|
||||
const organization = email?.split("@")[1]?.split(".")[0]; // Fallback value
|
||||
const userId = localStorage.getItem("userId");
|
||||
|
||||
let clearPanel = {
|
||||
organization: organization,
|
||||
panelName: side,
|
||||
zoneUuid: selectedZone.zoneUuid,
|
||||
userId, projectId
|
||||
};
|
||||
if (visualizationSocket) {
|
||||
visualizationSocket.emit("v1:viz-panel:clear", clearPanel);
|
||||
}
|
||||
const cleanedWidgets = selectedZone.widgets.filter(
|
||||
(widget) => widget.panel !== side
|
||||
);
|
||||
const updatedZone = {
|
||||
...selectedZone,
|
||||
widgets: cleanedWidgets,
|
||||
};
|
||||
// Update the selectedZone state
|
||||
setSelectedZone(updatedZone);
|
||||
};
|
||||
|
||||
// Function to handle "+" button click
|
||||
const handlePlusButtonClick = async (side: Side) => {
|
||||
const zoneUuid = selectedZone.zoneUuid;
|
||||
|
||||
if (selectedZone.activeSides.includes(side)) {
|
||||
// Already active: Schedule removal
|
||||
|
||||
setWaitingPanels(side); // Mark as waiting
|
||||
|
||||
setTimeout(() => {
|
||||
console.log("Removing after wait...");
|
||||
|
||||
const email = localStorage.getItem("email") ?? "";
|
||||
const organization = email?.split("@")[1]?.split(".")[0] ?? "";
|
||||
const userId = localStorage.getItem("userId");
|
||||
|
||||
// Remove widgets for that side
|
||||
const cleanedWidgets = selectedZone.widgets.filter(
|
||||
(widget) => widget.panel !== side
|
||||
);
|
||||
const newActiveSides = selectedZone.activeSides.filter(
|
||||
(s) => s !== side
|
||||
);
|
||||
|
||||
const updatedZone = {
|
||||
...selectedZone,
|
||||
widgets: cleanedWidgets,
|
||||
activeSides: newActiveSides,
|
||||
panelOrder: newActiveSides,
|
||||
};
|
||||
|
||||
const deletePanel = {
|
||||
organization,
|
||||
panelName: side,
|
||||
zoneUuid,
|
||||
projectId,
|
||||
userId
|
||||
};
|
||||
|
||||
if (visualizationSocket) {
|
||||
visualizationSocket.emit("v1:viz-panel:delete", deletePanel);
|
||||
}
|
||||
|
||||
setSelectedZone(updatedZone);
|
||||
|
||||
if (hiddenPanels[zoneUuid]?.includes(side)) {
|
||||
setHiddenPanels((prev) => ({
|
||||
...prev,
|
||||
[zoneUuid]: prev[zoneUuid].filter((s) => s !== side),
|
||||
}));
|
||||
}
|
||||
|
||||
// Remove from waiting state
|
||||
setWaitingPanels(null);
|
||||
}, 500); // Wait for 2 seconds
|
||||
} else {
|
||||
// Panel does not exist: Add it
|
||||
try {
|
||||
const email = localStorage.getItem("email") ?? "";
|
||||
const organization = email?.split("@")[1]?.split(".")[0] ?? "";
|
||||
const userId = localStorage.getItem("userId");
|
||||
|
||||
const newActiveSides = selectedZone.activeSides.includes(side)
|
||||
? [...selectedZone.activeSides]
|
||||
: [...selectedZone.activeSides, side];
|
||||
|
||||
const updatedZone = {
|
||||
...selectedZone,
|
||||
activeSides: newActiveSides,
|
||||
panelOrder: newActiveSides,
|
||||
};
|
||||
|
||||
const addPanel = {
|
||||
organization,
|
||||
zoneUuid,
|
||||
projectId,
|
||||
userId,
|
||||
panelOrder: newActiveSides,
|
||||
};
|
||||
|
||||
if (visualizationSocket) {
|
||||
visualizationSocket.emit("v1:viz-panel:add", addPanel);
|
||||
}
|
||||
|
||||
setSelectedZone(updatedZone);
|
||||
} catch (error) {
|
||||
echo.error("Failed to adding panel");
|
||||
console.error("Error adding panel:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{(["top", "right", "bottom", "left"] as Side[]).map((side) => (
|
||||
<div key={side} className={`side-button-container ${side}`}>
|
||||
{/* "+" Button */}
|
||||
|
||||
<button
|
||||
id={`${side}-add-button`}
|
||||
className={`side-button ${side}${selectedZone.activeSides.includes(side) ? " active" : ""
|
||||
}`}
|
||||
onClick={() => handlePlusButtonClick(side)}
|
||||
title={
|
||||
selectedZone.activeSides.includes(side)
|
||||
? `Remove all items and close ${side} panel`
|
||||
: `Activate ${side} panel`
|
||||
}
|
||||
>
|
||||
<div className="add-icon">
|
||||
<AddIcon />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Extra Buttons */}
|
||||
{selectedZone.activeSides.includes(side) && (
|
||||
<div
|
||||
className={`extra-Bs
|
||||
${waitingPanels === side ? "extra-Bs-addclosing" : ""}
|
||||
${!hiddenPanels[selectedZone.zoneUuid]?.includes(side) &&
|
||||
waitingPanels !== side
|
||||
? "extra-Bs-addopening"
|
||||
: ""
|
||||
}
|
||||
`}
|
||||
>
|
||||
{/* Hide Panel */}
|
||||
<button
|
||||
className={`icon ${hiddenPanels[selectedZone.zoneUuid]?.includes(side)
|
||||
? "active"
|
||||
: ""
|
||||
}`}
|
||||
id={`${side}-hide-panel-visulization`}
|
||||
title={
|
||||
hiddenPanels[selectedZone.zoneUuid]?.includes(side)
|
||||
? "Show Panel"
|
||||
: "Hide Panel"
|
||||
}
|
||||
onClick={() => toggleVisibility(side)}
|
||||
>
|
||||
<EyeIcon
|
||||
fill={
|
||||
hiddenPanels[selectedZone.zoneUuid]?.includes(side)
|
||||
? "var(--icon-default-color-active)"
|
||||
: "var(--text-color)"
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{/* Clean Panel */}
|
||||
<button
|
||||
className="icon"
|
||||
title="Clean Panel"
|
||||
id={`${side}-clean-panel-visulization`}
|
||||
onClick={() => cleanPanel(side)}
|
||||
style={{
|
||||
cursor:
|
||||
hiddenPanels[selectedZone.zoneUuid]?.includes(side) ||
|
||||
selectedZone.lockedPanels.includes(side)
|
||||
? "not-allowed"
|
||||
: "pointer",
|
||||
}}
|
||||
>
|
||||
<CleanPannel />
|
||||
</button>
|
||||
|
||||
{/* Lock/Unlock Panel */}
|
||||
<button
|
||||
className={`icon ${selectedZone.lockedPanels.includes(side) ? "active" : ""
|
||||
}`}
|
||||
id={`${side}-lock-panel-visulization`}
|
||||
title={
|
||||
selectedZone.lockedPanels.includes(side)
|
||||
? "Unlock Panel"
|
||||
: "Lock Panel"
|
||||
}
|
||||
onClick={() => toggleLockPanel(side)}
|
||||
>
|
||||
<LockIcon
|
||||
fill={
|
||||
selectedZone.lockedPanels.includes(side)
|
||||
? "var(--icon-default-color-active)"
|
||||
: "var(--text-color)"
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddButtons;
|
||||
343
app/src/modules/visualization/widgets/panel/Panel.tsx
Normal file
343
app/src/modules/visualization/widgets/panel/Panel.tsx
Normal file
@@ -0,0 +1,343 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import { arrayMove } from "@dnd-kit/sortable";
|
||||
import { useSocketStore } from "../../../../store/builder/store";
|
||||
import { usePlayButtonStore } from "../../../../store/usePlayButtonStore";
|
||||
import { useWidgetStore } from "../../../../store/useWidgetStore";
|
||||
import { DraggableWidget } from "../2d/DraggableWidget";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
type Side = "top" | "bottom" | "left" | "right";
|
||||
|
||||
interface Widget {
|
||||
id: string;
|
||||
type: string;
|
||||
title: string;
|
||||
panel: Side;
|
||||
data: any;
|
||||
}
|
||||
|
||||
interface PanelProps {
|
||||
selectedZone: {
|
||||
zoneName: string;
|
||||
activeSides: Side[];
|
||||
panelOrder: Side[];
|
||||
lockedPanels: Side[];
|
||||
points: [];
|
||||
zoneUuid: string;
|
||||
zoneViewPortTarget: number[];
|
||||
zoneViewPortPosition: number[];
|
||||
widgets: Widget[];
|
||||
};
|
||||
setSelectedZone: React.Dispatch<
|
||||
React.SetStateAction<{
|
||||
zoneName: string;
|
||||
activeSides: Side[];
|
||||
panelOrder: Side[];
|
||||
lockedPanels: Side[];
|
||||
points: [];
|
||||
zoneUuid: string;
|
||||
zoneViewPortTarget: number[];
|
||||
zoneViewPortPosition: number[];
|
||||
widgets: Widget[];
|
||||
}>
|
||||
>;
|
||||
hiddenPanels: any;
|
||||
setZonesData: React.Dispatch<React.SetStateAction<any>>;
|
||||
waitingPanels: any;
|
||||
}
|
||||
|
||||
const generateUniqueId = () =>
|
||||
`${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
||||
|
||||
const Panel: React.FC<PanelProps> = ({
|
||||
selectedZone,
|
||||
setSelectedZone,
|
||||
hiddenPanels,
|
||||
setZonesData,
|
||||
waitingPanels,
|
||||
}) => {
|
||||
const panelRefs = useRef<{ [side in Side]?: HTMLDivElement }>({});
|
||||
const [panelDimensions, setPanelDimensions] = useState<{
|
||||
[side in Side]?: { width: number; height: number };
|
||||
}>({});
|
||||
const [openKebabId, setOpenKebabId] = useState<string | null>(null);
|
||||
const { isPlaying } = usePlayButtonStore();
|
||||
const { visualizationSocket } = useSocketStore();
|
||||
const [canvasDimensions, setCanvasDimensions] = useState({
|
||||
width: 0,
|
||||
height: 0,
|
||||
});
|
||||
const { projectId } = useParams();
|
||||
|
||||
// Track canvas dimensions
|
||||
useEffect(() => {
|
||||
const canvas = document.getElementById("real-time-vis-canvas");
|
||||
if (!canvas) return;
|
||||
|
||||
const updateCanvasDimensions = () => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
setCanvasDimensions({
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
});
|
||||
};
|
||||
|
||||
updateCanvasDimensions();
|
||||
const resizeObserver = new ResizeObserver(updateCanvasDimensions);
|
||||
resizeObserver.observe(canvas);
|
||||
|
||||
return () => resizeObserver.unobserve(canvas);
|
||||
}, []);
|
||||
|
||||
// Calculate panel size
|
||||
const panelSize = Math.max(
|
||||
Math.min(canvasDimensions.width * 0.25, canvasDimensions.height * 0.25),
|
||||
170 // Min 170px
|
||||
);
|
||||
|
||||
// Define getPanelStyle
|
||||
const getPanelStyle = useMemo(
|
||||
() => (side: Side) => {
|
||||
const currentIndex = selectedZone.panelOrder.indexOf(side);
|
||||
const previousPanels = selectedZone.panelOrder.slice(0, currentIndex);
|
||||
const leftActive = previousPanels.includes("left");
|
||||
const rightActive = previousPanels.includes("right");
|
||||
const topActive = previousPanels.includes("top");
|
||||
const bottomActive = previousPanels.includes("bottom");
|
||||
|
||||
switch (side) {
|
||||
case "top":
|
||||
case "bottom":
|
||||
return {
|
||||
minWidth: "170px",
|
||||
width: `calc(100% - ${(leftActive ? panelSize : 0) + (rightActive ? panelSize : 0)
|
||||
}px)`,
|
||||
minHeight: "170px",
|
||||
height: `${panelSize}px`,
|
||||
left: leftActive ? `${panelSize}px` : "0",
|
||||
right: rightActive ? `${panelSize}px` : "0",
|
||||
[side]: "0",
|
||||
};
|
||||
case "left":
|
||||
case "right":
|
||||
return {
|
||||
minWidth: "170px",
|
||||
width: `${panelSize}px`,
|
||||
minHeight: "170px",
|
||||
height: `calc(100% - ${(topActive ? panelSize : 0) + (bottomActive ? panelSize : 0)
|
||||
}px)`,
|
||||
top: topActive ? `${panelSize}px` : "0",
|
||||
bottom: bottomActive ? `${panelSize}px` : "0",
|
||||
[side]: "0",
|
||||
};
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
},
|
||||
[selectedZone.panelOrder, panelSize]
|
||||
);
|
||||
|
||||
// Handle drop event
|
||||
const handleDrop = (e: React.DragEvent, panel: Side) => {
|
||||
e.preventDefault();
|
||||
const { draggedAsset } = useWidgetStore.getState();
|
||||
if (
|
||||
!draggedAsset ||
|
||||
isPanelLocked(panel) ||
|
||||
hiddenPanels[selectedZone.zoneUuid]?.includes(panel)
|
||||
)
|
||||
return;
|
||||
|
||||
const currentWidgetsCount = getCurrentWidgetCount(panel);
|
||||
const maxCapacity = calculatePanelCapacity(panel);
|
||||
|
||||
if (currentWidgetsCount < maxCapacity) {
|
||||
addWidgetToPanel(draggedAsset, panel);
|
||||
}
|
||||
};
|
||||
|
||||
// Check if panel is locked
|
||||
const isPanelLocked = (panel: Side) =>
|
||||
selectedZone.lockedPanels.includes(panel);
|
||||
|
||||
// Get current widget count in a panel
|
||||
const getCurrentWidgetCount = (panel: Side) =>
|
||||
selectedZone.widgets.filter((w) => w.panel === panel).length;
|
||||
|
||||
// Calculate panel capacity
|
||||
const calculatePanelCapacity = (panel: Side) => {
|
||||
const CHART_WIDTH = panelSize - 10;
|
||||
const CHART_HEIGHT = panelSize - 10;
|
||||
|
||||
const dimensions = panelDimensions[panel];
|
||||
if (!dimensions) {
|
||||
return panel === "top" || panel === "bottom" ? 5 : 3; // Fallback capacities
|
||||
}
|
||||
|
||||
return panel === "top" || panel === "bottom"
|
||||
? Math.max(1, Math.floor(dimensions.width / CHART_WIDTH))
|
||||
: Math.max(1, Math.floor(dimensions.height / CHART_HEIGHT));
|
||||
};
|
||||
|
||||
// Add widget to panel
|
||||
const addWidgetToPanel = async (asset: any, panel: Side) => {
|
||||
const email = localStorage.getItem("email") ?? "";
|
||||
const organization = email?.split("@")[1]?.split(".")[0];
|
||||
const userId = localStorage.getItem("userId");
|
||||
|
||||
const newWidget = {
|
||||
...asset,
|
||||
id: generateUniqueId(),
|
||||
panel,
|
||||
};
|
||||
|
||||
let addWidget = {
|
||||
organization: organization,
|
||||
zoneUuid: selectedZone.zoneUuid,
|
||||
widget: newWidget,
|
||||
projectId, userId
|
||||
};
|
||||
|
||||
if (visualizationSocket) {
|
||||
visualizationSocket.emit("v1:viz-widget:add", addWidget);
|
||||
}
|
||||
|
||||
setSelectedZone((prev) => ({
|
||||
...prev,
|
||||
widgets: [...prev.widgets, newWidget],
|
||||
}));
|
||||
};
|
||||
|
||||
// Observe panel dimensions
|
||||
useEffect(() => {
|
||||
const observers: ResizeObserver[] = [];
|
||||
const currentPanelRefs = panelRefs.current;
|
||||
|
||||
selectedZone.activeSides.forEach((side) => {
|
||||
const element = currentPanelRefs[side];
|
||||
if (element) {
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
const { width, height } = entry.contentRect;
|
||||
setPanelDimensions((prev) => ({
|
||||
...prev,
|
||||
[side]: { width, height },
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(element);
|
||||
observers.push(observer);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
observers.forEach((observer) => observer.disconnect());
|
||||
};
|
||||
}, [selectedZone.activeSides]);
|
||||
|
||||
// Handle widget reordering
|
||||
const handleReorder = (fromIndex: number, toIndex: number, panel: Side) => {
|
||||
setSelectedZone((prev) => {
|
||||
const widgetsInPanel = prev.widgets.filter((w) => w.panel === panel);
|
||||
const reorderedWidgets = arrayMove(widgetsInPanel, fromIndex, toIndex);
|
||||
|
||||
const updatedWidgets = prev.widgets
|
||||
.filter((widget) => widget.panel !== panel)
|
||||
.concat(reorderedWidgets);
|
||||
|
||||
return {
|
||||
...prev,
|
||||
widgets: updatedWidgets,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// Calculate capacities and dimensions
|
||||
const topWidth = getPanelStyle("top").width;
|
||||
const bottomWidth = getPanelStyle("bottom").height;
|
||||
const leftHeight = getPanelStyle("left").height;
|
||||
const rightHeight = getPanelStyle("right").height;
|
||||
|
||||
const topCapacity = calculatePanelCapacity("top");
|
||||
const bottomCapacity = calculatePanelCapacity("bottom");
|
||||
const leftCapacity = calculatePanelCapacity("left");
|
||||
const rightCapacity = calculatePanelCapacity("right");
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>
|
||||
{`
|
||||
:root {
|
||||
--topWidth: ${topWidth};
|
||||
--bottomWidth: ${bottomWidth} ;
|
||||
--leftHeight: ${leftHeight};
|
||||
--rightHeight: ${rightHeight};
|
||||
|
||||
--topCapacity: ${topCapacity};
|
||||
--bottomCapacity: ${bottomCapacity};
|
||||
--leftCapacity: ${leftCapacity};
|
||||
--rightCapacity: ${rightCapacity};
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
{selectedZone.activeSides.map((side) => (
|
||||
<div
|
||||
key={side}
|
||||
id={`panel-wrapper-${side}`}
|
||||
className={`panel ${side}-panel absolute ${hiddenPanels[selectedZone.zoneUuid]?.includes(side) ? "hidePanel" : ""
|
||||
}`}
|
||||
style={getPanelStyle(side)}
|
||||
onDrop={(e) => handleDrop(e, side)}
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
ref={(el) => {
|
||||
if (el) {
|
||||
panelRefs.current[side] = el;
|
||||
} else {
|
||||
delete panelRefs.current[side];
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`panel-content ${waitingPanels === side ? `${side}-closing` : ""
|
||||
}${!hiddenPanels[selectedZone.zoneUuid]?.includes(side) &&
|
||||
waitingPanels !== side
|
||||
? `${side}-opening`
|
||||
: ""
|
||||
} ${isPlaying ? "fullScreen" : ""}`}
|
||||
style={{
|
||||
pointerEvents:
|
||||
selectedZone.lockedPanels.includes(side) ||
|
||||
hiddenPanels[selectedZone.zoneUuid]?.includes(side)
|
||||
? "none"
|
||||
: "auto",
|
||||
opacity: selectedZone.lockedPanels.includes(side) ? "0.8" : "1",
|
||||
}}
|
||||
>
|
||||
{selectedZone.widgets
|
||||
.filter((w) => w.panel === side)
|
||||
.map((widget, index) => (
|
||||
<DraggableWidget
|
||||
hiddenPanels={hiddenPanels}
|
||||
widget={widget}
|
||||
key={widget.id}
|
||||
index={index}
|
||||
onReorder={(fromIndex, toIndex) =>
|
||||
handleReorder(fromIndex, toIndex, side)
|
||||
}
|
||||
openKebabId={openKebabId}
|
||||
setOpenKebabId={setOpenKebabId}
|
||||
selectedZone={selectedZone}
|
||||
setSelectedZone={setSelectedZone}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Panel;
|
||||
@@ -0,0 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
const WidgetPlaceHolder = () => {
|
||||
return <div className="widget-placeholder"></div>;
|
||||
};
|
||||
|
||||
export default WidgetPlaceHolder;
|
||||
277
app/src/modules/visualization/zone/DisplayZone.tsx
Normal file
277
app/src/modules/visualization/zone/DisplayZone.tsx
Normal file
@@ -0,0 +1,277 @@
|
||||
import React, { useEffect, useRef, useState, useCallback } from "react";
|
||||
import { useWidgetStore, Widget } from "../../../store/useWidgetStore";
|
||||
|
||||
import {
|
||||
useDroppedObjectsStore,
|
||||
useFloatingWidget,
|
||||
} from "../../../store/visualization/useDroppedObjectsStore";
|
||||
import { getSelect2dZoneData } from "../../../services/visulization/zone/getSelect2dZoneData";
|
||||
import { getFloatingZoneData } from "../../../services/visulization/zone/getFloatingData";
|
||||
import { get3dWidgetZoneData } from "../../../services/visulization/zone/get3dWidgetData";
|
||||
import {
|
||||
MoveArrowLeft,
|
||||
MoveArrowRight,
|
||||
} from "../../../components/icons/SimulationIcons";
|
||||
import { InfoIcon } from "../../../components/icons/ExportCommonIcons";
|
||||
import { usePlayButtonStore } from "../../../store/usePlayButtonStore";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
// Define the type for `Side`
|
||||
type Side = "top" | "bottom" | "left" | "right";
|
||||
|
||||
interface HiddenPanels {
|
||||
[zoneUuid: string]: Side[];
|
||||
}
|
||||
|
||||
interface DisplayZoneProps {
|
||||
zonesData: {
|
||||
[key: string]: {
|
||||
activeSides: Side[];
|
||||
panelOrder: Side[];
|
||||
lockedPanels: Side[];
|
||||
points: [];
|
||||
widgets: Widget[];
|
||||
zoneUuid: string;
|
||||
zoneViewPortTarget: number[];
|
||||
zoneViewPortPosition: number[];
|
||||
};
|
||||
};
|
||||
selectedZone: {
|
||||
zoneName: string;
|
||||
activeSides: Side[];
|
||||
panelOrder: Side[];
|
||||
lockedPanels: Side[];
|
||||
zoneUuid: string;
|
||||
points: [];
|
||||
zoneViewPortTarget: number[];
|
||||
zoneViewPortPosition: number[];
|
||||
widgets: {
|
||||
id: string;
|
||||
type: string;
|
||||
title: string;
|
||||
panel: Side;
|
||||
data: any;
|
||||
}[];
|
||||
};
|
||||
setSelectedZone: React.Dispatch<
|
||||
React.SetStateAction<{
|
||||
zoneName: string;
|
||||
activeSides: Side[];
|
||||
panelOrder: Side[];
|
||||
lockedPanels: Side[];
|
||||
points: [];
|
||||
zoneUuid: string;
|
||||
zoneViewPortTarget: number[];
|
||||
zoneViewPortPosition: number[];
|
||||
widgets: {
|
||||
id: string;
|
||||
type: string;
|
||||
title: string;
|
||||
panel: Side;
|
||||
data: any;
|
||||
}[];
|
||||
}>
|
||||
>;
|
||||
hiddenPanels: HiddenPanels; // Updated prop type
|
||||
setHiddenPanels: React.Dispatch<React.SetStateAction<HiddenPanels>>; // Updated prop type
|
||||
}
|
||||
|
||||
const DisplayZone: React.FC<DisplayZoneProps> = ({
|
||||
zonesData,
|
||||
selectedZone,
|
||||
setSelectedZone,
|
||||
hiddenPanels,
|
||||
}) => {
|
||||
// Ref for the container element
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
// State to track overflow visibility
|
||||
const [showLeftArrow, setShowLeftArrow] = useState(false);
|
||||
const [showRightArrow, setShowRightArrow] = useState(false);
|
||||
const { floatingWidget, setFloatingWidget } = useFloatingWidget();
|
||||
const { isPlaying } = usePlayButtonStore();
|
||||
|
||||
const { setSelectedChartId } = useWidgetStore();
|
||||
const { projectId } = useParams();
|
||||
// Function to calculate overflow state
|
||||
const updateOverflowState = useCallback(() => {
|
||||
const container = scrollContainerRef.current;
|
||||
if (container) {
|
||||
const isOverflowing = container.scrollWidth > container.clientWidth;
|
||||
const canScrollLeft = container.scrollLeft > 0;
|
||||
|
||||
const canScrollRight =
|
||||
container.scrollLeft + container.clientWidth + 1 <
|
||||
container.scrollWidth;
|
||||
setShowLeftArrow(isOverflowing && canScrollLeft);
|
||||
setShowRightArrow(isOverflowing && canScrollRight);
|
||||
|
||||
// console.log('canScrollRight: ', canScrollRight);
|
||||
// console.log('isOverflowing: ', isOverflowing);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const container = scrollContainerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
// Initial calculation after the DOM has been rendered
|
||||
const observer = new ResizeObserver(updateOverflowState);
|
||||
observer.observe(container);
|
||||
|
||||
// Update on scroll
|
||||
const handleScroll = () => updateOverflowState();
|
||||
container.addEventListener("scroll", handleScroll);
|
||||
|
||||
// Add mouse wheel listener for horizontal scrolling
|
||||
const handleWheel = (event: WheelEvent) => {
|
||||
if (Math.abs(event.deltaY) > Math.abs(event.deltaX)) {
|
||||
event.preventDefault();
|
||||
container.scrollBy({
|
||||
left: event.deltaY * 2,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
container.addEventListener("wheel", handleWheel, { passive: false });
|
||||
|
||||
// Initial check
|
||||
updateOverflowState();
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
container.removeEventListener("scroll", handleScroll);
|
||||
container.removeEventListener("wheel", handleWheel);
|
||||
};
|
||||
}, [updateOverflowState]);
|
||||
|
||||
// Handle scrolling with navigation arrows
|
||||
const handleScrollLeft = () => {
|
||||
const container = scrollContainerRef.current;
|
||||
if (container) {
|
||||
container.scrollBy({
|
||||
left: -200,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleScrollRight = () => {
|
||||
const container = scrollContainerRef.current;
|
||||
if (container) {
|
||||
container.scrollBy({
|
||||
left: 200,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
async function handleSelect2dZoneData(zoneUuid: string, zoneName: string) {
|
||||
try {
|
||||
if (selectedZone?.zoneUuid === zoneUuid) {
|
||||
return;
|
||||
}
|
||||
// setSelectedChartId(null);
|
||||
const email = localStorage.getItem("email") || "";
|
||||
const organization = email?.split("@")[1]?.split(".")[0];
|
||||
|
||||
let response = await getSelect2dZoneData(zoneUuid, organization, projectId);
|
||||
// console.log('response2d: ', response);
|
||||
let res = await getFloatingZoneData(zoneUuid, organization, projectId);
|
||||
// console.log("resFloating: ", res);
|
||||
|
||||
setFloatingWidget(res);
|
||||
// Set the selected zone in the store
|
||||
useDroppedObjectsStore.getState().setZone(zoneName, zoneUuid);
|
||||
if (Array.isArray(res)) {
|
||||
res.forEach((val) => {
|
||||
useDroppedObjectsStore.getState().addObject(zoneName, val);
|
||||
});
|
||||
}
|
||||
|
||||
setSelectedZone({
|
||||
zoneName,
|
||||
activeSides: response.activeSides || [],
|
||||
panelOrder: response.panelOrder || [],
|
||||
lockedPanels: response.lockedPanels || [],
|
||||
widgets: response.widgets || [],
|
||||
points: response.points || [],
|
||||
zoneUuid: zoneUuid,
|
||||
zoneViewPortTarget: response.viewPortCenter || {},
|
||||
zoneViewPortPosition: response.viewPortposition || {},
|
||||
});
|
||||
} catch (error) {
|
||||
echo.error("Failed to select zone");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
id="zone-container"
|
||||
className={`zone-container ${selectedZone?.activeSides?.includes("bottom") &&
|
||||
!hiddenPanels[selectedZone.zoneUuid]?.includes("bottom")
|
||||
? "bottom"
|
||||
: ""
|
||||
} ${isPlaying ? "visualization-playing" : ""}`}
|
||||
>
|
||||
{/* Left Arrow */}
|
||||
{showLeftArrow && (
|
||||
<button
|
||||
id="zone-preview-left-arrow"
|
||||
className="arrow left-arrow"
|
||||
onClick={handleScrollLeft}
|
||||
>
|
||||
<MoveArrowLeft />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Scrollable Zones Container */}
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className="zones-wrapper"
|
||||
style={{ overflowX: "auto", whiteSpace: "nowrap" }}
|
||||
>
|
||||
{Object.keys(zonesData).length !== 0 ? (
|
||||
<>
|
||||
{Object.keys(zonesData).map((zoneName, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`zone ${selectedZone.zoneName === zoneName ? "active" : ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
|
||||
console.log('zonesData: ', zonesData);
|
||||
handleSelect2dZoneData(zonesData[zoneName]?.zoneUuid, zoneName)
|
||||
}
|
||||
}
|
||||
>
|
||||
{zoneName}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<div className="no-zone">
|
||||
<InfoIcon />
|
||||
No zones? Create one!
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right Arrow */}
|
||||
{showRightArrow && (
|
||||
<button
|
||||
id="zone-preview-right-arrow"
|
||||
className="arrow right-arrow"
|
||||
onClick={handleScrollRight}
|
||||
>
|
||||
<MoveArrowRight />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DisplayZone;
|
||||
78
app/src/modules/visualization/zone/zoneAssets.tsx
Normal file
78
app/src/modules/visualization/zone/zoneAssets.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import { useSelectedFloorItem, useZoneAssetId } from '../../../store/builder/store';
|
||||
import * as THREE from "three";
|
||||
import { useThree } from '@react-three/fiber';
|
||||
import * as Types from "../../../types/world/worldTypes";
|
||||
export default function ZoneAssets() {
|
||||
const { zoneAssetId, setZoneAssetId } = useZoneAssetId();
|
||||
const { setSelectedFloorItem } = useSelectedFloorItem();
|
||||
const { raycaster, controls, scene }: any = useThree();
|
||||
|
||||
useEffect(() => {
|
||||
// console.log('zoneAssetId: ', zoneAssetId);
|
||||
if (!zoneAssetId) return
|
||||
let AssetMesh = scene.getObjectByProperty("uuid", zoneAssetId.id);
|
||||
if (AssetMesh) {
|
||||
const bbox = new THREE.Box3().setFromObject(AssetMesh);
|
||||
const size = bbox.getSize(new THREE.Vector3());
|
||||
const center = bbox.getCenter(new THREE.Vector3());
|
||||
|
||||
const front = new THREE.Vector3(0, 0, 1);
|
||||
AssetMesh.localToWorld(front);
|
||||
front.sub(AssetMesh.position).normalize();
|
||||
|
||||
const distance = Math.max(size.x, size.y, size.z) * 2;
|
||||
const newPosition = center.clone().addScaledVector(front, distance);
|
||||
|
||||
controls.setPosition(newPosition.x, newPosition.y, newPosition.z, true);
|
||||
controls.setTarget(center.x, center.y, center.z, true);
|
||||
controls.fitToBox(AssetMesh, true, { cover: true, paddingTop: 5, paddingLeft: 5, paddingBottom: 5, paddingRight: 5, });
|
||||
|
||||
setSelectedFloorItem(AssetMesh);
|
||||
} else {
|
||||
if (Array.isArray(zoneAssetId.position) && zoneAssetId.position.length >= 3) {
|
||||
let selectedAssetPosition = [
|
||||
zoneAssetId.position[0],
|
||||
10,
|
||||
zoneAssetId.position[2]
|
||||
];
|
||||
let selectedAssetTarget = [
|
||||
zoneAssetId.position[0],
|
||||
zoneAssetId.position[1],
|
||||
zoneAssetId.position[2]
|
||||
];
|
||||
const setCam = async () => {
|
||||
await controls?.setLookAt(...selectedAssetPosition, ...selectedAssetTarget, true);
|
||||
setTimeout(() => {
|
||||
let AssetMesh = scene.getObjectByProperty("uuid", zoneAssetId.id);
|
||||
if (AssetMesh) {
|
||||
const bbox = new THREE.Box3().setFromObject(AssetMesh);
|
||||
const size = bbox.getSize(new THREE.Vector3());
|
||||
const center = bbox.getCenter(new THREE.Vector3());
|
||||
|
||||
const front = new THREE.Vector3(0, 0, 1);
|
||||
AssetMesh.localToWorld(front);
|
||||
front.sub(AssetMesh.position).normalize();
|
||||
|
||||
const distance = Math.max(size.x, size.y, size.z) * 2;
|
||||
const newPosition = center.clone().addScaledVector(front, distance);
|
||||
|
||||
controls.setPosition(newPosition.x, newPosition.y, newPosition.z, true);
|
||||
controls.setTarget(center.x, center.y, center.z, true);
|
||||
controls.fitToBox(AssetMesh, true, { cover: true, paddingTop: 5, paddingLeft: 5, paddingBottom: 5, paddingRight: 5, });
|
||||
|
||||
setSelectedFloorItem(AssetMesh);
|
||||
}
|
||||
}, 500)
|
||||
|
||||
};
|
||||
setCam();
|
||||
}
|
||||
}
|
||||
}, [zoneAssetId, scene, controls])
|
||||
|
||||
return (
|
||||
<>
|
||||
</>
|
||||
)
|
||||
}
|
||||
102
app/src/modules/visualization/zone/zoneCameraTarget.tsx
Normal file
102
app/src/modules/visualization/zone/zoneCameraTarget.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import * as THREE from "three";
|
||||
import { useSelectedZoneStore } from "../../../store/visualization/useZoneStore";
|
||||
import {
|
||||
useEditPosition,
|
||||
usezonePosition,
|
||||
usezoneTarget,
|
||||
} from "../../../store/builder/store";
|
||||
|
||||
export default function ZoneCentreTarget() {
|
||||
const { selectedZone } = useSelectedZoneStore();
|
||||
const [previousZoneCentre, setPreviousZoneCentre] = useState<number[] | null>(
|
||||
null
|
||||
);
|
||||
const sphereRef = useRef<THREE.Mesh>(null);
|
||||
const { controls }: any = useThree();
|
||||
const { setZonePosition } = usezonePosition();
|
||||
const { setZoneTarget } = usezoneTarget();
|
||||
const { Edit } = useEditPosition();
|
||||
|
||||
const TRANSITION_SPEED = 2000;
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
selectedZone.zoneViewPortTarget &&
|
||||
JSON.stringify(previousZoneCentre) !==
|
||||
JSON.stringify(selectedZone.zoneViewPortTarget)
|
||||
) {
|
||||
setPreviousZoneCentre(selectedZone.zoneViewPortTarget);
|
||||
}
|
||||
}, [selectedZone.zoneViewPortTarget, previousZoneCentre]);
|
||||
|
||||
const centrePoint = useMemo(() => {
|
||||
if (!previousZoneCentre || !selectedZone.zoneViewPortTarget) return null;
|
||||
return previousZoneCentre.map(
|
||||
(value, index) => (value + selectedZone.zoneViewPortTarget[index]) / 2
|
||||
);
|
||||
}, [previousZoneCentre, selectedZone.zoneViewPortTarget]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedZone.zoneName !== "") {
|
||||
if (sphereRef.current) {
|
||||
sphereRef.current.position.set(
|
||||
selectedZone.zoneViewPortTarget[0],
|
||||
selectedZone.zoneViewPortTarget[1],
|
||||
selectedZone.zoneViewPortTarget[2]
|
||||
);
|
||||
}
|
||||
if (centrePoint) {
|
||||
if (centrePoint.length > 0) {
|
||||
const setCam = async () => {
|
||||
controls.setLookAt(
|
||||
centrePoint[0],
|
||||
26,
|
||||
centrePoint[2],
|
||||
...centrePoint,
|
||||
true,
|
||||
TRANSITION_SPEED
|
||||
);
|
||||
setTimeout(() => {
|
||||
controls?.setLookAt(
|
||||
...selectedZone.zoneViewPortPosition,
|
||||
...selectedZone.zoneViewPortTarget,
|
||||
true,
|
||||
TRANSITION_SPEED
|
||||
);
|
||||
}, 100);
|
||||
};
|
||||
setCam();
|
||||
} else {
|
||||
const setCam = async () => {
|
||||
controls?.setLookAt(
|
||||
...selectedZone.zoneViewPortPosition,
|
||||
...selectedZone.zoneViewPortTarget,
|
||||
true,
|
||||
TRANSITION_SPEED
|
||||
);
|
||||
};
|
||||
setCam();
|
||||
}
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedZone.zoneViewPortTarget]);
|
||||
|
||||
useFrame(() => {
|
||||
if (Edit) {
|
||||
setZonePosition([
|
||||
controls.getPosition().x,
|
||||
controls.getPosition().y,
|
||||
controls.getPosition().z,
|
||||
]);
|
||||
setZoneTarget([
|
||||
controls.getTarget().x,
|
||||
controls.getTarget().y,
|
||||
controls.getTarget().z,
|
||||
]);
|
||||
}
|
||||
});
|
||||
return <></>;
|
||||
}
|
||||
Reference in New Issue
Block a user