@collabdt/core 0.3.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/dist/core/components/ui/Comments/CollapsibleCommentItem.js.map +1 -1
  2. package/dist/core/components/ui/Comments/Comment.js.map +1 -1
  3. package/dist/core/components/ui/Comments/CommentActionButtons.d.ts +5 -1
  4. package/dist/core/components/ui/Comments/CommentActionButtons.d.ts.map +1 -1
  5. package/dist/core/components/ui/Comments/CommentActionButtons.js +37 -9
  6. package/dist/core/components/ui/Comments/CommentActionButtons.js.map +1 -1
  7. package/dist/core/components/ui/Sensors/CollapsibleSensorItem.d.ts +1 -2
  8. package/dist/core/components/ui/Sensors/CollapsibleSensorItem.d.ts.map +1 -1
  9. package/dist/core/components/ui/Sensors/CollapsibleSensorItem.js +66 -94
  10. package/dist/core/components/ui/Sensors/CollapsibleSensorItem.js.map +1 -1
  11. package/dist/core/components/ui/Sensors/Sensor.d.ts +16 -3
  12. package/dist/core/components/ui/Sensors/Sensor.d.ts.map +1 -1
  13. package/dist/core/components/ui/Sensors/Sensor.js +76 -37
  14. package/dist/core/components/ui/Sensors/Sensor.js.map +1 -1
  15. package/dist/core/components/ui/Sensors/SensorChart.d.ts +12 -2
  16. package/dist/core/components/ui/Sensors/SensorChart.d.ts.map +1 -1
  17. package/dist/core/components/ui/Sensors/SensorChart.js +49 -12
  18. package/dist/core/components/ui/Sensors/SensorChart.js.map +1 -1
  19. package/dist/core/components/ui/Sensors/SensorDetailDialog.d.ts +9 -0
  20. package/dist/core/components/ui/Sensors/SensorDetailDialog.d.ts.map +1 -0
  21. package/dist/core/components/ui/Sensors/SensorDetailDialog.js +128 -0
  22. package/dist/core/components/ui/Sensors/SensorDetailDialog.js.map +1 -0
  23. package/dist/core/components/ui/Sensors/SensorInput.d.ts +6 -1
  24. package/dist/core/components/ui/Sensors/SensorInput.d.ts.map +1 -1
  25. package/dist/core/components/ui/Sensors/SensorInput.js +94 -8
  26. package/dist/core/components/ui/Sensors/SensorInput.js.map +1 -1
  27. package/dist/core/components/ui/Sensors/SensorTagsSection.d.ts +3 -1
  28. package/dist/core/components/ui/Sensors/SensorTagsSection.d.ts.map +1 -1
  29. package/dist/core/components/ui/Sensors/SensorTagsSection.js +4 -1
  30. package/dist/core/components/ui/Sensors/SensorTagsSection.js.map +1 -1
  31. package/dist/core/components/ui/Sensors/SensorsSection.d.ts +1 -3
  32. package/dist/core/components/ui/Sensors/SensorsSection.d.ts.map +1 -1
  33. package/dist/core/components/ui/Sensors/SensorsSection.js +33 -8
  34. package/dist/core/components/ui/Sensors/SensorsSection.js.map +1 -1
  35. package/dist/core/components/ui/Sensors/TimeZoneSelect.d.ts +7 -0
  36. package/dist/core/components/ui/Sensors/TimeZoneSelect.d.ts.map +1 -0
  37. package/dist/core/components/ui/Sensors/TimeZoneSelect.js +56 -0
  38. package/dist/core/components/ui/Sensors/TimeZoneSelect.js.map +1 -0
  39. package/dist/core/components/ui/Sensors/sensorData.d.ts +42 -0
  40. package/dist/core/components/ui/Sensors/sensorData.d.ts.map +1 -0
  41. package/dist/core/components/ui/Sensors/sensorData.js +118 -0
  42. package/dist/core/components/ui/Sensors/sensorData.js.map +1 -0
  43. package/dist/core/components/ui/Sensors/sensorRange.d.ts +24 -0
  44. package/dist/core/components/ui/Sensors/sensorRange.d.ts.map +1 -0
  45. package/dist/core/components/ui/Sensors/sensorRange.js +34 -0
  46. package/dist/core/components/ui/Sensors/sensorRange.js.map +1 -0
  47. package/dist/core/components/ui/Sensors/useSensorSeries.d.ts +15 -0
  48. package/dist/core/components/ui/Sensors/useSensorSeries.d.ts.map +1 -0
  49. package/dist/core/components/ui/Sensors/useSensorSeries.js +63 -0
  50. package/dist/core/components/ui/Sensors/useSensorSeries.js.map +1 -0
  51. package/dist/core/components/viewers/Viewer.js +1 -1
  52. package/dist/core/components/viewers/Viewer.js.map +1 -1
  53. package/dist/core/components/viewers/bim/src/BimSidebar/src/SensorsTab/index.d.ts +1 -3
  54. package/dist/core/components/viewers/bim/src/BimSidebar/src/SensorsTab/index.d.ts.map +1 -1
  55. package/dist/core/components/viewers/bim/src/BimSidebar/src/SensorsTab/index.js +2 -2
  56. package/dist/core/components/viewers/bim/src/BimSidebar/src/SensorsTab/index.js.map +1 -1
  57. package/dist/core/components/viewers/bim/src/BimSidebar/src/index.js +1 -1
  58. package/dist/core/components/viewers/bim/src/BimSidebar/src/index.js.map +1 -1
  59. package/dist/core/components/viewers/bim/src/tools/AddToBim/index.d.ts +1 -2
  60. package/dist/core/components/viewers/bim/src/tools/AddToBim/index.d.ts.map +1 -1
  61. package/dist/core/components/viewers/bim/src/tools/AddToBim/index.js +25 -2
  62. package/dist/core/components/viewers/bim/src/tools/AddToBim/index.js.map +1 -1
  63. package/dist/core/components/viewers/bim/src/tools/AddToBim/src/BimComment.js.map +1 -1
  64. package/dist/core/components/viewers/bim/src/tools/AddToBim/src/BimCommentCluster.d.ts +1 -22
  65. package/dist/core/components/viewers/bim/src/tools/AddToBim/src/BimCommentCluster.d.ts.map +1 -1
  66. package/dist/core/components/viewers/bim/src/tools/AddToBim/src/BimCommentCluster.js +2 -105
  67. package/dist/core/components/viewers/bim/src/tools/AddToBim/src/BimCommentCluster.js.map +1 -1
  68. package/dist/core/components/viewers/bim/src/tools/AddToBim/src/BimSensor.d.ts +8 -1
  69. package/dist/core/components/viewers/bim/src/tools/AddToBim/src/BimSensor.d.ts.map +1 -1
  70. package/dist/core/components/viewers/bim/src/tools/AddToBim/src/BimSensor.js +15 -3
  71. package/dist/core/components/viewers/bim/src/tools/AddToBim/src/BimSensor.js.map +1 -1
  72. package/dist/core/components/viewers/bim/src/tools/AddToBim/src/clusterMarkersByScreenSpace.js.map +1 -1
  73. package/dist/core/components/viewers/bim/src/tools/AddToBim/src/commentCameraUtils.d.ts +2 -23
  74. package/dist/core/components/viewers/bim/src/tools/AddToBim/src/commentCameraUtils.d.ts.map +1 -1
  75. package/dist/core/components/viewers/bim/src/tools/AddToBim/src/commentCameraUtils.js +2 -24
  76. package/dist/core/components/viewers/bim/src/tools/AddToBim/src/commentCameraUtils.js.map +1 -1
  77. package/dist/core/components/viewers/bim/src/tools/AddToBim/src/commentMarkerConfig.d.ts +33 -0
  78. package/dist/core/components/viewers/bim/src/tools/AddToBim/src/commentMarkerConfig.d.ts.map +1 -0
  79. package/dist/core/components/viewers/bim/src/tools/AddToBim/src/commentMarkerConfig.js +90 -0
  80. package/dist/core/components/viewers/bim/src/tools/AddToBim/src/commentMarkerConfig.js.map +1 -0
  81. package/dist/core/components/viewers/bim/src/tools/AddToBim/src/useCommentMarkers.d.ts +7 -1
  82. package/dist/core/components/viewers/bim/src/tools/AddToBim/src/useCommentMarkers.d.ts.map +1 -1
  83. package/dist/core/components/viewers/bim/src/tools/AddToBim/src/useCommentMarkers.js +26 -230
  84. package/dist/core/components/viewers/bim/src/tools/AddToBim/src/useCommentMarkers.js.map +1 -1
  85. package/dist/core/components/viewers/bim/src/tools/AddToBim/src/useSensorMarkers.d.ts +7 -1
  86. package/dist/core/components/viewers/bim/src/tools/AddToBim/src/useSensorMarkers.d.ts.map +1 -1
  87. package/dist/core/components/viewers/bim/src/tools/AddToBim/src/useSensorMarkers.js +39 -11
  88. package/dist/core/components/viewers/bim/src/tools/AddToBim/src/useSensorMarkers.js.map +1 -1
  89. package/dist/core/components/viewers/map/MapViewer.d.ts +1 -2
  90. package/dist/core/components/viewers/map/MapViewer.d.ts.map +1 -1
  91. package/dist/core/components/viewers/map/MapViewer.js +2 -2
  92. package/dist/core/components/viewers/map/MapViewer.js.map +1 -1
  93. package/dist/core/components/viewers/map/src/MapLayers/index.d.ts +1 -2
  94. package/dist/core/components/viewers/map/src/MapLayers/index.d.ts.map +1 -1
  95. package/dist/core/components/viewers/map/src/MapLayers/index.js +2 -2
  96. package/dist/core/components/viewers/map/src/MapLayers/index.js.map +1 -1
  97. package/dist/core/components/viewers/map/src/MapLayers/src/SensorsLayer/index.d.ts +1 -3
  98. package/dist/core/components/viewers/map/src/MapLayers/src/SensorsLayer/index.d.ts.map +1 -1
  99. package/dist/core/components/viewers/map/src/MapLayers/src/SensorsLayer/index.js +74 -39
  100. package/dist/core/components/viewers/map/src/MapLayers/src/SensorsLayer/index.js.map +1 -1
  101. package/dist/core/components/viewers/map/src/MapSidebar/index.js +1 -1
  102. package/dist/core/components/viewers/map/src/MapSidebar/index.js.map +1 -1
  103. package/dist/core/components/viewers/map/src/MapSidebar/src/SensorsTab/index.d.ts +1 -3
  104. package/dist/core/components/viewers/map/src/MapSidebar/src/SensorsTab/index.d.ts.map +1 -1
  105. package/dist/core/components/viewers/map/src/MapSidebar/src/SensorsTab/index.js +2 -2
  106. package/dist/core/components/viewers/map/src/MapSidebar/src/SensorsTab/index.js.map +1 -1
  107. package/dist/core/components/viewers/shared/markers/BimMarkerCluster.d.ts +19 -0
  108. package/dist/core/components/viewers/shared/markers/BimMarkerCluster.d.ts.map +1 -0
  109. package/dist/core/components/viewers/shared/markers/BimMarkerCluster.js +108 -0
  110. package/dist/core/components/viewers/shared/markers/BimMarkerCluster.js.map +1 -0
  111. package/dist/core/components/viewers/shared/markers/computeMarkerLookAt.d.ts +24 -0
  112. package/dist/core/components/viewers/shared/markers/computeMarkerLookAt.d.ts.map +1 -0
  113. package/dist/core/components/viewers/shared/markers/computeMarkerLookAt.js +27 -0
  114. package/dist/core/components/viewers/shared/markers/computeMarkerLookAt.js.map +1 -0
  115. package/dist/core/components/viewers/shared/markers/types.d.ts +66 -0
  116. package/dist/core/components/viewers/shared/markers/types.d.ts.map +1 -0
  117. package/dist/core/components/viewers/shared/markers/types.js +1 -0
  118. package/dist/core/components/viewers/shared/markers/types.js.map +1 -0
  119. package/dist/core/components/viewers/shared/markers/useMarkerLayerBim.d.ts +22 -0
  120. package/dist/core/components/viewers/shared/markers/useMarkerLayerBim.d.ts.map +1 -0
  121. package/dist/core/components/viewers/shared/markers/useMarkerLayerBim.js +233 -0
  122. package/dist/core/components/viewers/shared/markers/useMarkerLayerBim.js.map +1 -0
  123. package/dist/core/hooks/ui/useResizableSidebarWidth.js.map +1 -1
  124. package/dist/core/i18n/messages/en.json +5 -1
  125. package/dist/core/i18n/messages/es.json +5 -1
  126. package/dist/core/i18n/messages/fr.json +5 -1
  127. package/dist/core/store/AppConfig/context.d.ts.map +1 -1
  128. package/dist/core/store/AppConfig/context.js +4 -1
  129. package/dist/core/store/AppConfig/context.js.map +1 -1
  130. package/dist/core/store/AppConfig/reducer.d.ts +10 -0
  131. package/dist/core/store/AppConfig/reducer.d.ts.map +1 -1
  132. package/dist/core/store/AppConfig/reducer.js +10 -0
  133. package/dist/core/store/AppConfig/reducer.js.map +1 -1
  134. package/dist/core/store/Menus/context.d.ts.map +1 -1
  135. package/dist/core/store/Menus/context.js +5 -2
  136. package/dist/core/store/Menus/context.js.map +1 -1
  137. package/dist/core/store/Menus/reducer.d.ts +45 -1
  138. package/dist/core/store/Menus/reducer.d.ts.map +1 -1
  139. package/dist/core/store/Menus/reducer.js +21 -3
  140. package/dist/core/store/Menus/reducer.js.map +1 -1
  141. package/dist/core/utils/timeUtils.d.ts +24 -1
  142. package/dist/core/utils/timeUtils.d.ts.map +1 -1
  143. package/dist/core/utils/timeUtils.js +52 -3
  144. package/dist/core/utils/timeUtils.js.map +1 -1
  145. package/package.json +1 -1
@@ -6,7 +6,8 @@ import * as React from "react";
6
6
  import { toast } from "sonner";
7
7
  import { useSensor, useSensors } from "../../../hooks/sensors/sensors";
8
8
  import { useSensorTypes } from "../../../hooks/sensorTypes/sensorTypes";
9
- import { BuildingsContext, MenusContext, ToolsContext } from "../../../store";
9
+ import { AppConfigContext, BuildingsContext, MenusContext, ToolsContext } from "../../../store";
10
+ import { resolveDefaultTimeZone } from "../../../utils/timeUtils";
10
11
  import { stringToColour } from "../../viewers/map/utils/stringToColour";
11
12
  import { Button } from "../Button";
12
13
  import { CollapsibleSection } from "../CollapsibleSection";
@@ -21,11 +22,12 @@ import {
21
22
  } from "../DropdownMenu";
22
23
  import { SearchInput } from "../SearchInput";
23
24
  import { CollapsibleSensorItem } from "./CollapsibleSensorItem";
25
+ import { SensorInput } from "./SensorInput";
24
26
  import { SensorsSectionSkeleton } from "./SensorsSectionSkeleton";
25
27
  import { resolveLucideIcon } from "./sensorUtils";
26
28
  const UNTAGGED_TAG = "__untagged__";
27
- function SensorsSection({ minioBaseUrl }) {
28
- var _a, _b;
29
+ function SensorsSection() {
30
+ var _a, _b, _c, _d, _e, _f, _g;
29
31
  const t = useTranslations("SensorsSection");
30
32
  const { dispatch: toolsDispatch } = React.useContext(ToolsContext);
31
33
  const { state: menusState, dispatch: menusDispatch } = React.useContext(MenusContext);
@@ -34,7 +36,9 @@ function SensorsSection({ minioBaseUrl }) {
34
36
  const { sensorTypes, isLoading } = useSensorTypes();
35
37
  const { state: buildingsState } = React.useContext(BuildingsContext);
36
38
  const buildingId = ((_b = (_a = buildingsState == null ? void 0 : buildingsState.buildings) == null ? void 0 : _a.building) == null ? void 0 : _b.id) || -1;
39
+ const { state: appConfigState, dispatch: appConfigDispatch } = React.useContext(AppConfigContext);
37
40
  const [sensorToDelete, setSensorToDelete] = React.useState(null);
41
+ const [editSensorId, setEditSensorId] = React.useState(null);
38
42
  const [searchQuery, setSearchQuery] = React.useState("");
39
43
  const [groupBy, setGroupBy] = React.useState("type");
40
44
  const { deleteSensor } = useSensor(sensorToDelete);
@@ -58,6 +62,7 @@ function SensorsSection({ minioBaseUrl }) {
58
62
  case "view":
59
63
  break;
60
64
  case "edit":
65
+ setEditSensorId(id);
61
66
  break;
62
67
  case "delete":
63
68
  toast.success(t("sensorDeleted"));
@@ -74,13 +79,24 @@ function SensorsSection({ minioBaseUrl }) {
74
79
  }
75
80
  }, [sensorToDelete, deleteSensor]);
76
81
  const currentSensors = allSensors.filter((sensor) => currentViewer === sensor.viewer && (!sensor.buildingId || sensor.buildingId === buildingId));
82
+ const buildingLongitude = (_e = (_d = (_c = buildingsState == null ? void 0 : buildingsState.buildings) == null ? void 0 : _c.building) == null ? void 0 : _d.buildingLongitude) != null ? _e : null;
83
+ const firstSensorLongitude = (_g = (_f = currentSensors.find((s) => typeof s.longitude === "number")) == null ? void 0 : _f.longitude) != null ? _g : null;
84
+ const { displayTimeZone, displayTimeZoneUserSet } = appConfigState.appConfig;
85
+ React.useEffect(() => {
86
+ if (displayTimeZoneUserSet) return;
87
+ const candidates = currentViewer === "bim" ? [buildingLongitude, firstSensorLongitude] : [firstSensorLongitude, buildingLongitude];
88
+ const zone = resolveDefaultTimeZone(candidates);
89
+ if (zone !== displayTimeZone) {
90
+ appConfigDispatch({ type: "SET_DEFAULT_TIME_ZONE", payload: { displayTimeZone: zone } });
91
+ }
92
+ }, [currentViewer, buildingLongitude, firstSensorLongitude, displayTimeZone, displayTimeZoneUserSet, appConfigDispatch]);
77
93
  const filteredSensors = React.useMemo(() => {
78
94
  if (!searchQuery.trim()) return currentSensors;
79
95
  const query = searchQuery.toLowerCase();
80
96
  return currentSensors.filter((sensor) => {
81
- var _a2, _b2, _c;
97
+ var _a2, _b2, _c2;
82
98
  const sensorTypeName = (_b2 = (_a2 = sensorTypes.find((type) => type.id === sensor.typeId)) == null ? void 0 : _a2.name) != null ? _b2 : "";
83
- return ((_c = sensor.name) == null ? void 0 : _c.toLowerCase().includes(query)) || sensorTypeName.toLowerCase().includes(query);
99
+ return ((_c2 = sensor.name) == null ? void 0 : _c2.toLowerCase().includes(query)) || sensorTypeName.toLowerCase().includes(query);
84
100
  });
85
101
  }, [currentSensors, searchQuery]);
86
102
  const toggleSensorTypeVisibility = React.useCallback((sensorTypeId) => {
@@ -208,7 +224,6 @@ function SensorsSection({ minioBaseUrl }) {
208
224
  sensorType: type,
209
225
  onAction: handleSensorAction,
210
226
  isVisible: isTypeVisible,
211
- minioBaseUrl,
212
227
  onMouseEnter: () => menusDispatch({ type: "SET_CURRENT_SENSOR_ID", payload: { currentSensorId: sensor.id } }),
213
228
  onMouseLeave: () => menusDispatch({ type: "SET_CURRENT_SENSOR_ID", payload: { currentSensorId: null } })
214
229
  },
@@ -242,7 +257,6 @@ function SensorsSection({ minioBaseUrl }) {
242
257
  sensorType: type,
243
258
  onAction: handleSensorAction,
244
259
  isVisible: isTagVisible,
245
- minioBaseUrl,
246
260
  onMouseEnter: () => menusDispatch({ type: "SET_CURRENT_SENSOR_ID", payload: { currentSensorId: sensor.id } }),
247
261
  onMouseLeave: () => menusDispatch({ type: "SET_CURRENT_SENSOR_ID", payload: { currentSensorId: null } })
248
262
  },
@@ -252,7 +266,18 @@ function SensorsSection({ minioBaseUrl }) {
252
266
  },
253
267
  tag
254
268
  );
255
- })
269
+ }),
270
+ editSensorId != null && allSensors.some((s) => s.id === editSensorId) && /* @__PURE__ */ jsx(
271
+ SensorInput,
272
+ {
273
+ viewer: currentViewer,
274
+ layout: "dialog",
275
+ isOpen: editSensorId != null,
276
+ editSensor: allSensors.find((s) => s.id === editSensorId),
277
+ onCancel: () => setEditSensorId(null),
278
+ onSaved: () => setEditSensorId(null)
279
+ }
280
+ )
256
281
  ] });
257
282
  }
258
283
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../src/core/components/ui/Sensors/SensorsSection.tsx"],"sourcesContent":["'use client'\r\n\r\n// SPDX-License-Identifier: AGPL-3.0-or-later\r\n// Copyright (C) 2025 Collab Digital Twins\r\n\r\nimport * as LR from 'lucide-react'\r\nimport { useTranslations } from 'next-intl'\r\nimport * as React from 'react'\r\nimport { toast } from 'sonner'\r\n\r\nimport { useSensor, useSensors } from '../../../hooks/sensors/sensors'\r\nimport { useSensorTypes } from '../../../hooks/sensorTypes/sensorTypes'\r\nimport { BuildingsContext, MenusContext, ToolsContext } from '../../../store'\r\nimport { stringToColour } from '../../viewers/map/utils/stringToColour'\r\nimport { Button } from '../Button'\r\nimport { CollapsibleSection } from '../CollapsibleSection'\r\nimport { DropdownMenu } from '../DropdownMenu'\r\nimport {\r\n DropdownMenuTrigger,\r\n DropdownMenuContent,\r\n DropdownMenuLabel,\r\n DropdownMenuSeparator,\r\n DropdownMenuRadioGroup,\r\n DropdownMenuRadioItem,\r\n} from '../DropdownMenu'\r\nimport { SearchInput } from '../SearchInput'\r\n\r\nimport { CollapsibleSensorItem } from './CollapsibleSensorItem'\r\nimport { SensorsSectionSkeleton } from './SensorsSectionSkeleton'\r\nimport { resolveLucideIcon } from './sensorUtils'\r\n\r\nimport type { Sensor } from '../../../types/dbTypes';\r\n\r\n\r\ntype SensorAction = 'view' | 'edit' | 'delete'\r\n\r\n/** Stable key used in state for sensors with no tags — locale-independent */\r\nexport const UNTAGGED_TAG = '__untagged__'\r\n\r\nexport function SensorsSection({ minioBaseUrl }: { minioBaseUrl?: string }) {\r\n // Translation\r\n const t = useTranslations('SensorsSection')\r\n\r\n const { dispatch: toolsDispatch } = React.useContext(ToolsContext)\r\n const { state: menusState, dispatch: menusDispatch } = React.useContext(MenusContext)\r\n const { currentViewer, visibleSensorTypes, visibleSensorTags } = menusState.menus\r\n\r\n const { sensors: allSensors }: { sensors: Sensor[] } = useSensors()\r\n const { sensorTypes, isLoading } = useSensorTypes()\r\n\r\n const {state: buildingsState} = React.useContext(BuildingsContext)\r\n const buildingId = buildingsState?.buildings?.building?.id || -1\r\n\r\n const [sensorToDelete, setSensorToDelete] = React.useState<number | null>(null)\r\n const [searchQuery, setSearchQuery] = React.useState('')\r\n const [groupBy, setGroupBy] = React.useState<'type' | 'tag'>('type')\r\n const { deleteSensor } = useSensor(sensorToDelete)\r\n\r\n const handleAddSensor = React.useCallback((currentSensorTypeId?: number) => {\r\n if (!currentViewer) return\r\n toolsDispatch({\r\n type: 'SET-TOOL',\r\n payload: {\r\n currentToolId:\r\n currentViewer === 'bim'\r\n ? 'bim-add-sensor'\r\n : currentViewer === 'map'\r\n ? 'map-add-sensor'\r\n : undefined,\r\n },\r\n })\r\n if (currentSensorTypeId) {\r\n menusDispatch({\r\n type: 'SET_CURRENT_SENSOR_TYPE_ID',\r\n payload: { currentSensorTypeId },\r\n })\r\n }\r\n }, [toolsDispatch, currentViewer])\r\n\r\n const handleSensorAction = React.useCallback((action: SensorAction, id: number) => {\r\n switch (action) {\r\n case 'view':\r\n // ⚠️ View sensor action not implemented\r\n break\r\n case 'edit':\r\n // ⚠️ Edit sensor action not implemented\r\n break\r\n case 'delete':\r\n toast.success(t('sensorDeleted'))\r\n setSensorToDelete(id)\r\n break\r\n default:\r\n break\r\n }\r\n }, [t])\r\n\r\n // Trigger deletion when sensorToDelete changes\r\n React.useEffect(() => {\r\n if (sensorToDelete !== null) {\r\n deleteSensor()\r\n setSensorToDelete(null)\r\n }\r\n }, [sensorToDelete, deleteSensor])\r\n\r\n const currentSensors: Sensor[] = allSensors.filter((sensor) => currentViewer === sensor.viewer && (!sensor.buildingId || sensor.buildingId === buildingId))\r\n\r\n // Filter sensors based on search query\r\n const filteredSensors = React.useMemo(() => {\r\n if (!searchQuery.trim()) return currentSensors\r\n\r\n const query = searchQuery.toLowerCase()\r\n return currentSensors.filter((sensor) => {\r\n const sensorTypeName =\r\n sensorTypes.find(type => type.id === sensor.typeId)?.name ?? ''\r\n return (\r\n sensor.name?.toLowerCase().includes(query) ||\r\n sensorTypeName.toLowerCase().includes(query)\r\n )\r\n })\r\n }, [currentSensors, searchQuery])\r\n\r\n const toggleSensorTypeVisibility = React.useCallback((sensorTypeId: number) => {\r\n menusDispatch({\r\n type: 'TOGGLE_SENSOR_TYPE_VISIBILITY',\r\n payload: {\r\n viewer: currentViewer,\r\n sensorTypeId: sensorTypeId,\r\n },\r\n })\r\n }, [currentViewer, menusDispatch])\r\n\r\n const handleGroupByChange = React.useCallback((newGroupBy: 'type' | 'tag') => {\r\n setGroupBy(newGroupBy)\r\n // Clear the opposing visibility state so markers from the old filter don't bleed through\r\n if (newGroupBy === 'tag') {\r\n menusDispatch({ type: 'HIDE_ALL_SENSORS_IN_VIEWER', payload: { viewer: currentViewer } })\r\n } else {\r\n menusDispatch({ type: 'HIDE_ALL_SENSOR_TAGS_IN_VIEWER', payload: { viewer: currentViewer } })\r\n }\r\n }, [currentViewer, menusDispatch])\r\n\r\n const toggleSensorTagVisibility = React.useCallback((tag: string) => {\r\n menusDispatch({\r\n type: 'TOGGLE_SENSOR_TAG_VISIBILITY',\r\n payload: {\r\n viewer: currentViewer,\r\n sensorTag: tag,\r\n },\r\n })\r\n }, [currentViewer, menusDispatch])\r\n\r\n // Group sensors by type, using sensorTypes as the source of truth for types\r\n const sensorsByType = React.useMemo(() => {\r\n const grouped: Record<string, Sensor[]> = {}\r\n sensorTypes.forEach(type => {\r\n grouped[type.name] = []\r\n })\r\n filteredSensors.forEach(sensor => {\r\n const type = sensorTypes.find(t => t.id === sensor.typeId)\r\n const typeName = type?.name ?? 'Other'\r\n if (!grouped[typeName]) {\r\n grouped[typeName] = []\r\n }\r\n grouped[typeName].push(sensor)\r\n })\r\n // Remove empty groups\r\n Object.keys(grouped).forEach(key => {\r\n if (grouped[key].length === 0) {\r\n delete grouped[key]\r\n }\r\n })\r\n return grouped\r\n }, [filteredSensors, sensorTypes])\r\n\r\n // Group sensors by tag\r\n const sensorsByTag = React.useMemo(() => {\r\n const grouped: Record<string, Sensor[]> = {}\r\n filteredSensors.forEach(sensor => {\r\n const tags = sensor.tags && sensor.tags.length > 0 ? sensor.tags : [UNTAGGED_TAG]\r\n tags.forEach(tag => {\r\n if (!grouped[tag]) grouped[tag] = []\r\n grouped[tag].push(sensor)\r\n })\r\n })\r\n return grouped\r\n }, [filteredSensors])\r\n\r\n return (\r\n <div className=\"flex-1 flex flex-col space-y-4 overflow-hidden\">\r\n <div className=\"px-4 flex items-center gap-2\">\r\n <SearchInput\r\n placeholder={t('searchSensors')}\r\n value={searchQuery}\r\n onChange={e => setSearchQuery(e.target.value)}\r\n />\r\n <DropdownMenu>\r\n <DropdownMenuTrigger asChild>\r\n <Button variant=\"outline\" size=\"icon\" title={t('filters')} className=\"flex-shrink-0\">\r\n <LR.Funnel size={16} />\r\n </Button>\r\n </DropdownMenuTrigger>\r\n <DropdownMenuContent align=\"end\">\r\n <DropdownMenuLabel>{t('groupBy')}</DropdownMenuLabel>\r\n <DropdownMenuSeparator />\r\n <DropdownMenuRadioGroup value={groupBy} onValueChange={v => handleGroupByChange(v as 'type' | 'tag')}>\r\n <DropdownMenuRadioItem value=\"type\">\r\n <LR.Layers className=\"mr-2 h-4 w-4\" />\r\n {t('groupByType')}\r\n </DropdownMenuRadioItem>\r\n <DropdownMenuRadioItem value=\"tag\">\r\n <LR.Tag className=\"mr-2 h-4 w-4\" />\r\n {t('groupByTag')}\r\n </DropdownMenuRadioItem>\r\n </DropdownMenuRadioGroup>\r\n </DropdownMenuContent>\r\n </DropdownMenu>\r\n </div>\r\n {isLoading ? (\r\n <SensorsSectionSkeleton />\r\n ) :\r\n filteredSensors.length === 0 ? (\r\n <div className=\"p-4 text-muted-foreground text-sm flex flex-col items-center\">\r\n <div>{t('noSensorsFound')}</div>\r\n <Button\r\n className=\"mt-2 px-4 \"\r\n variant='outline'\r\n onClick={() => handleAddSensor()}\r\n >\r\n {t('addSensorTitle')}\r\n </Button>\r\n </div>\r\n ) : groupBy === 'type' ? (\r\n Object.entries(sensorsByType).map(([typeName, sensorsOfType]) => {\r\n const type = sensorTypes.find(t => t.name === typeName)\r\n const typeId = type?.id ?? -1\r\n\r\n const viewerTypes = visibleSensorTypes?.[currentViewer]\r\n const isTypeVisible = viewerTypes?.length > 0 && viewerTypes.includes(typeId)\r\n\r\n const sensorIcon = resolveLucideIcon(type?.icon)\r\n\r\n return (\r\n <CollapsibleSection\r\n key={typeName}\r\n title={typeName.replace(/_/g, ' ')}\r\n icon={sensorIcon}\r\n className=\"overflow-y-auto\"\r\n itemCount={sensorsOfType.length}\r\n onAddItem={() => handleAddSensor(typeId)}\r\n addItemTitle={t('addSensorTitle')}\r\n defaultOpen={false}\r\n switchVariant={{\r\n checked: isTypeVisible,\r\n onCheckedChange: () => toggleSensorTypeVisibility(typeId),\r\n }}\r\n >\r\n <div className=\"space-y-2 mx-2 pr-2\">\r\n {sensorsOfType.map((sensor) => (\r\n <CollapsibleSensorItem\r\n key={sensor.id}\r\n sensor={sensor}\r\n sensorType={type}\r\n onAction={handleSensorAction}\r\n isVisible={isTypeVisible}\r\n minioBaseUrl={minioBaseUrl}\r\n onMouseEnter={() => menusDispatch({ type: 'SET_CURRENT_SENSOR_ID', payload: { currentSensorId: sensor.id } })}\r\n onMouseLeave={() => menusDispatch({ type: 'SET_CURRENT_SENSOR_ID', payload: { currentSensorId: null } })}\r\n />\r\n ))}\r\n </div>\r\n </CollapsibleSection>\r\n )\r\n })\r\n ) : (\r\n Object.entries(sensorsByTag).map(([tag, sensorsOfTag]) => {\r\n const isTagVisible = visibleSensorTags?.[currentViewer]?.includes(tag) ?? false\r\n return (\r\n <CollapsibleSection\r\n key={tag}\r\n title={tag === UNTAGGED_TAG ? t('untagged') : tag}\r\n icon={LR.Tag}\r\n colour={stringToColour(tag, 'max')}\r\n className=\"overflow-y-auto\"\r\n itemCount={sensorsOfTag.length}\r\n defaultOpen={false}\r\n switchVariant={{\r\n checked: isTagVisible,\r\n onCheckedChange: () => toggleSensorTagVisibility(tag),\r\n }}\r\n >\r\n <div className=\"space-y-2 mx-2 pr-2\">\r\n {sensorsOfTag.map((sensor) => {\r\n const type = sensorTypes.find(tp => tp.id === sensor.typeId)\r\n return (\r\n <CollapsibleSensorItem\r\n key={sensor.id}\r\n sensor={sensor}\r\n sensorType={type}\r\n onAction={handleSensorAction}\r\n isVisible={isTagVisible}\r\n minioBaseUrl={minioBaseUrl}\r\n onMouseEnter={() => menusDispatch({ type: 'SET_CURRENT_SENSOR_ID', payload: { currentSensorId: sensor.id } })}\r\n onMouseLeave={() => menusDispatch({ type: 'SET_CURRENT_SENSOR_ID', payload: { currentSensorId: null } })}\r\n />\r\n )\r\n })}\r\n </div>\r\n </CollapsibleSection>\r\n )\r\n })\r\n )}\r\n </div>\r\n )\r\n}\r\n"],"mappings":"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","names":["_a","_b","t"]}
1
+ {"version":3,"sources":["../../../../../src/core/components/ui/Sensors/SensorsSection.tsx"],"sourcesContent":["'use client'\r\n\r\n// SPDX-License-Identifier: AGPL-3.0-or-later\r\n// Copyright (C) 2025 Collab Digital Twins\r\n\r\nimport * as LR from 'lucide-react'\r\nimport { useTranslations } from 'next-intl'\r\nimport * as React from 'react'\r\nimport { toast } from 'sonner'\r\n\r\nimport { useSensor, useSensors } from '../../../hooks/sensors/sensors'\r\nimport { useSensorTypes } from '../../../hooks/sensorTypes/sensorTypes'\r\nimport { AppConfigContext, BuildingsContext, MenusContext, ToolsContext } from '../../../store'\r\nimport { resolveDefaultTimeZone } from '../../../utils/timeUtils'\r\nimport { stringToColour } from '../../viewers/map/utils/stringToColour'\r\nimport { Button } from '../Button'\r\nimport { CollapsibleSection } from '../CollapsibleSection'\r\nimport { DropdownMenu } from '../DropdownMenu'\r\nimport {\r\n DropdownMenuTrigger,\r\n DropdownMenuContent,\r\n DropdownMenuLabel,\r\n DropdownMenuSeparator,\r\n DropdownMenuRadioGroup,\r\n DropdownMenuRadioItem,\r\n} from '../DropdownMenu'\r\nimport { SearchInput } from '../SearchInput'\r\n\r\nimport { CollapsibleSensorItem } from './CollapsibleSensorItem'\r\nimport { SensorInput } from './SensorInput'\r\nimport { SensorsSectionSkeleton } from './SensorsSectionSkeleton'\r\nimport { resolveLucideIcon } from './sensorUtils'\r\n\r\nimport type { Sensor } from '../../../types/dbTypes';\r\n\r\n\r\ntype SensorAction = 'view' | 'edit' | 'delete'\r\n\r\n/** Stable key used in state for sensors with no tags — locale-independent */\r\nexport const UNTAGGED_TAG = '__untagged__'\r\n\r\nexport function SensorsSection() {\r\n // Translation\r\n const t = useTranslations('SensorsSection')\r\n\r\n const { dispatch: toolsDispatch } = React.useContext(ToolsContext)\r\n const { state: menusState, dispatch: menusDispatch } = React.useContext(MenusContext)\r\n const { currentViewer, visibleSensorTypes, visibleSensorTags } = menusState.menus\r\n\r\n const { sensors: allSensors }: { sensors: Sensor[] } = useSensors()\r\n const { sensorTypes, isLoading } = useSensorTypes()\r\n\r\n const {state: buildingsState} = React.useContext(BuildingsContext)\r\n const buildingId = buildingsState?.buildings?.building?.id || -1\r\n\r\n const { state: appConfigState, dispatch: appConfigDispatch } = React.useContext(AppConfigContext)\r\n\r\n const [sensorToDelete, setSensorToDelete] = React.useState<number | null>(null)\r\n const [editSensorId, setEditSensorId] = React.useState<number | null>(null)\r\n const [searchQuery, setSearchQuery] = React.useState('')\r\n const [groupBy, setGroupBy] = React.useState<'type' | 'tag'>('type')\r\n const { deleteSensor } = useSensor(sensorToDelete)\r\n\r\n const handleAddSensor = React.useCallback((currentSensorTypeId?: number) => {\r\n if (!currentViewer) return\r\n toolsDispatch({\r\n type: 'SET-TOOL',\r\n payload: {\r\n currentToolId:\r\n currentViewer === 'bim'\r\n ? 'bim-add-sensor'\r\n : currentViewer === 'map'\r\n ? 'map-add-sensor'\r\n : undefined,\r\n },\r\n })\r\n if (currentSensorTypeId) {\r\n menusDispatch({\r\n type: 'SET_CURRENT_SENSOR_TYPE_ID',\r\n payload: { currentSensorTypeId },\r\n })\r\n }\r\n }, [toolsDispatch, currentViewer])\r\n\r\n const handleSensorAction = React.useCallback((action: SensorAction, id: number) => {\r\n switch (action) {\r\n case 'view':\r\n // ⚠️ View sensor action not implemented\r\n break\r\n case 'edit':\r\n setEditSensorId(id)\r\n break\r\n case 'delete':\r\n toast.success(t('sensorDeleted'))\r\n setSensorToDelete(id)\r\n break\r\n default:\r\n break\r\n }\r\n }, [t])\r\n\r\n // Trigger deletion when sensorToDelete changes\r\n React.useEffect(() => {\r\n if (sensorToDelete !== null) {\r\n deleteSensor()\r\n setSensorToDelete(null)\r\n }\r\n }, [sensorToDelete, deleteSensor])\r\n\r\n const currentSensors: Sensor[] = allSensors.filter((sensor) => currentViewer === sensor.viewer && (!sensor.buildingId || sensor.buildingId === buildingId))\r\n\r\n // Default the display zone from the building's (else a sensor's) longitude, until the user overrides.\r\n // Depends on primitive longitudes (not the re-created currentSensors array) so it only runs on real change.\r\n const buildingLongitude = buildingsState?.buildings?.building?.buildingLongitude ?? null\r\n const firstSensorLongitude = currentSensors.find(s => typeof s.longitude === 'number')?.longitude ?? null\r\n const { displayTimeZone, displayTimeZoneUserSet } = appConfigState.appConfig\r\n React.useEffect(() => {\r\n if (displayTimeZoneUserSet) return\r\n // BIM sensors live in a building (use its longitude); map sensors carry their own.\r\n const candidates = currentViewer === 'bim'\r\n ? [buildingLongitude, firstSensorLongitude]\r\n : [firstSensorLongitude, buildingLongitude]\r\n const zone = resolveDefaultTimeZone(candidates)\r\n if (zone !== displayTimeZone) {\r\n appConfigDispatch({ type: 'SET_DEFAULT_TIME_ZONE', payload: { displayTimeZone: zone } })\r\n }\r\n }, [currentViewer, buildingLongitude, firstSensorLongitude, displayTimeZone, displayTimeZoneUserSet, appConfigDispatch])\r\n\r\n // Filter sensors based on search query\r\n const filteredSensors = React.useMemo(() => {\r\n if (!searchQuery.trim()) return currentSensors\r\n\r\n const query = searchQuery.toLowerCase()\r\n return currentSensors.filter((sensor) => {\r\n const sensorTypeName =\r\n sensorTypes.find(type => type.id === sensor.typeId)?.name ?? ''\r\n return (\r\n sensor.name?.toLowerCase().includes(query) ||\r\n sensorTypeName.toLowerCase().includes(query)\r\n )\r\n })\r\n }, [currentSensors, searchQuery])\r\n\r\n const toggleSensorTypeVisibility = React.useCallback((sensorTypeId: number) => {\r\n menusDispatch({\r\n type: 'TOGGLE_SENSOR_TYPE_VISIBILITY',\r\n payload: {\r\n viewer: currentViewer,\r\n sensorTypeId: sensorTypeId,\r\n },\r\n })\r\n }, [currentViewer, menusDispatch])\r\n\r\n const handleGroupByChange = React.useCallback((newGroupBy: 'type' | 'tag') => {\r\n setGroupBy(newGroupBy)\r\n // Clear the opposing visibility state so markers from the old filter don't bleed through\r\n if (newGroupBy === 'tag') {\r\n menusDispatch({ type: 'HIDE_ALL_SENSORS_IN_VIEWER', payload: { viewer: currentViewer } })\r\n } else {\r\n menusDispatch({ type: 'HIDE_ALL_SENSOR_TAGS_IN_VIEWER', payload: { viewer: currentViewer } })\r\n }\r\n }, [currentViewer, menusDispatch])\r\n\r\n const toggleSensorTagVisibility = React.useCallback((tag: string) => {\r\n menusDispatch({\r\n type: 'TOGGLE_SENSOR_TAG_VISIBILITY',\r\n payload: {\r\n viewer: currentViewer,\r\n sensorTag: tag,\r\n },\r\n })\r\n }, [currentViewer, menusDispatch])\r\n\r\n // Group sensors by type, using sensorTypes as the source of truth for types\r\n const sensorsByType = React.useMemo(() => {\r\n const grouped: Record<string, Sensor[]> = {}\r\n sensorTypes.forEach(type => {\r\n grouped[type.name] = []\r\n })\r\n filteredSensors.forEach(sensor => {\r\n const type = sensorTypes.find(t => t.id === sensor.typeId)\r\n const typeName = type?.name ?? 'Other'\r\n if (!grouped[typeName]) {\r\n grouped[typeName] = []\r\n }\r\n grouped[typeName].push(sensor)\r\n })\r\n // Remove empty groups\r\n Object.keys(grouped).forEach(key => {\r\n if (grouped[key].length === 0) {\r\n delete grouped[key]\r\n }\r\n })\r\n return grouped\r\n }, [filteredSensors, sensorTypes])\r\n\r\n // Group sensors by tag\r\n const sensorsByTag = React.useMemo(() => {\r\n const grouped: Record<string, Sensor[]> = {}\r\n filteredSensors.forEach(sensor => {\r\n const tags = sensor.tags && sensor.tags.length > 0 ? sensor.tags : [UNTAGGED_TAG]\r\n tags.forEach(tag => {\r\n if (!grouped[tag]) grouped[tag] = []\r\n grouped[tag].push(sensor)\r\n })\r\n })\r\n return grouped\r\n }, [filteredSensors])\r\n\r\n return (\r\n <div className=\"flex-1 flex flex-col space-y-4 overflow-hidden\">\r\n <div className=\"px-4 flex items-center gap-2\">\r\n <SearchInput\r\n placeholder={t('searchSensors')}\r\n value={searchQuery}\r\n onChange={e => setSearchQuery(e.target.value)}\r\n />\r\n <DropdownMenu>\r\n <DropdownMenuTrigger asChild>\r\n <Button variant=\"outline\" size=\"icon\" title={t('filters')} className=\"flex-shrink-0\">\r\n <LR.Funnel size={16} />\r\n </Button>\r\n </DropdownMenuTrigger>\r\n <DropdownMenuContent align=\"end\">\r\n <DropdownMenuLabel>{t('groupBy')}</DropdownMenuLabel>\r\n <DropdownMenuSeparator />\r\n <DropdownMenuRadioGroup value={groupBy} onValueChange={v => handleGroupByChange(v as 'type' | 'tag')}>\r\n <DropdownMenuRadioItem value=\"type\">\r\n <LR.Layers className=\"mr-2 h-4 w-4\" />\r\n {t('groupByType')}\r\n </DropdownMenuRadioItem>\r\n <DropdownMenuRadioItem value=\"tag\">\r\n <LR.Tag className=\"mr-2 h-4 w-4\" />\r\n {t('groupByTag')}\r\n </DropdownMenuRadioItem>\r\n </DropdownMenuRadioGroup>\r\n </DropdownMenuContent>\r\n </DropdownMenu>\r\n </div>\r\n {isLoading ? (\r\n <SensorsSectionSkeleton />\r\n ) :\r\n filteredSensors.length === 0 ? (\r\n <div className=\"p-4 text-muted-foreground text-sm flex flex-col items-center\">\r\n <div>{t('noSensorsFound')}</div>\r\n <Button\r\n className=\"mt-2 px-4 \"\r\n variant='outline'\r\n onClick={() => handleAddSensor()}\r\n >\r\n {t('addSensorTitle')}\r\n </Button>\r\n </div>\r\n ) : groupBy === 'type' ? (\r\n Object.entries(sensorsByType).map(([typeName, sensorsOfType]) => {\r\n const type = sensorTypes.find(t => t.name === typeName)\r\n const typeId = type?.id ?? -1\r\n\r\n const viewerTypes = visibleSensorTypes?.[currentViewer]\r\n const isTypeVisible = viewerTypes?.length > 0 && viewerTypes.includes(typeId)\r\n\r\n const sensorIcon = resolveLucideIcon(type?.icon)\r\n\r\n return (\r\n <CollapsibleSection\r\n key={typeName}\r\n title={typeName.replace(/_/g, ' ')}\r\n icon={sensorIcon}\r\n className=\"overflow-y-auto\"\r\n itemCount={sensorsOfType.length}\r\n onAddItem={() => handleAddSensor(typeId)}\r\n addItemTitle={t('addSensorTitle')}\r\n defaultOpen={false}\r\n switchVariant={{\r\n checked: isTypeVisible,\r\n onCheckedChange: () => toggleSensorTypeVisibility(typeId),\r\n }}\r\n >\r\n <div className=\"space-y-2 mx-2 pr-2\">\r\n {sensorsOfType.map((sensor) => (\r\n <CollapsibleSensorItem\r\n key={sensor.id}\r\n sensor={sensor}\r\n sensorType={type}\r\n onAction={handleSensorAction}\r\n isVisible={isTypeVisible}\r\n onMouseEnter={() => menusDispatch({ type: 'SET_CURRENT_SENSOR_ID', payload: { currentSensorId: sensor.id } })}\r\n onMouseLeave={() => menusDispatch({ type: 'SET_CURRENT_SENSOR_ID', payload: { currentSensorId: null } })}\r\n />\r\n ))}\r\n </div>\r\n </CollapsibleSection>\r\n )\r\n })\r\n ) : (\r\n Object.entries(sensorsByTag).map(([tag, sensorsOfTag]) => {\r\n const isTagVisible = visibleSensorTags?.[currentViewer]?.includes(tag) ?? false\r\n return (\r\n <CollapsibleSection\r\n key={tag}\r\n title={tag === UNTAGGED_TAG ? t('untagged') : tag}\r\n icon={LR.Tag}\r\n colour={stringToColour(tag, 'max')}\r\n className=\"overflow-y-auto\"\r\n itemCount={sensorsOfTag.length}\r\n defaultOpen={false}\r\n switchVariant={{\r\n checked: isTagVisible,\r\n onCheckedChange: () => toggleSensorTagVisibility(tag),\r\n }}\r\n >\r\n <div className=\"space-y-2 mx-2 pr-2\">\r\n {sensorsOfTag.map((sensor) => {\r\n const type = sensorTypes.find(tp => tp.id === sensor.typeId)\r\n return (\r\n <CollapsibleSensorItem\r\n key={sensor.id}\r\n sensor={sensor}\r\n sensorType={type}\r\n onAction={handleSensorAction}\r\n isVisible={isTagVisible}\r\n onMouseEnter={() => menusDispatch({ type: 'SET_CURRENT_SENSOR_ID', payload: { currentSensorId: sensor.id } })}\r\n onMouseLeave={() => menusDispatch({ type: 'SET_CURRENT_SENSOR_ID', payload: { currentSensorId: null } })}\r\n />\r\n )\r\n })}\r\n </div>\r\n </CollapsibleSection>\r\n )\r\n })\r\n )}\r\n\r\n {editSensorId != null && allSensors.some(s => s.id === editSensorId) && (\r\n <SensorInput\r\n viewer={currentViewer}\r\n layout=\"dialog\"\r\n isOpen={editSensorId != null}\r\n editSensor={allSensors.find(s => s.id === editSensorId)}\r\n onCancel={() => setEditSensorId(null)}\r\n onSaved={() => setEditSensorId(null)}\r\n />\r\n )}\r\n </div>\r\n )\r\n}\r\n"],"mappings":"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","names":["_a","_b","_c","t"]}
@@ -0,0 +1,7 @@
1
+ import * as React from 'react';
2
+ export declare function TimeZoneSelect({ value, onChange, placeholder, }: {
3
+ value: string;
4
+ onChange: (zone: string) => void;
5
+ placeholder?: string;
6
+ }): React.ReactElement;
7
+ //# sourceMappingURL=TimeZoneSelect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TimeZoneSelect.d.ts","sourceRoot":"","sources":["../../../../../src/core/components/ui/Sensors/TimeZoneSelect.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAkB9B,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,QAAQ,EACR,WAA+B,GAChC,EAAE;IACD,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAChC,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,GAAG,KAAK,CAAC,YAAY,CAmCrB"}
@@ -0,0 +1,56 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { Check, ChevronsUpDown } from "lucide-react";
4
+ import * as React from "react";
5
+ import { cn } from "../../../utils/utils";
6
+ import { Button } from "../Button";
7
+ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "../Command";
8
+ import { Popover, PopoverContent, PopoverTrigger } from "../Popover";
9
+ function supportedZones() {
10
+ const intl = Intl;
11
+ try {
12
+ if (typeof intl.supportedValuesOf === "function") return intl.supportedValuesOf("timeZone");
13
+ } catch (e) {
14
+ }
15
+ return ["UTC"];
16
+ }
17
+ function TimeZoneSelect({
18
+ value,
19
+ onChange,
20
+ placeholder = "Select timezone"
21
+ }) {
22
+ const [open, setOpen] = React.useState(false);
23
+ const zones = React.useMemo(supportedZones, []);
24
+ const options = React.useMemo(() => zones.includes(value) ? zones : [value, ...zones], [zones, value]);
25
+ return /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: setOpen, children: [
26
+ /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { variant: "outline", role: "combobox", "aria-expanded": open, className: "w-[240px] justify-between", children: [
27
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: value || placeholder }),
28
+ /* @__PURE__ */ jsx(ChevronsUpDown, { className: "ml-2 h-4 w-4 shrink-0 opacity-50" })
29
+ ] }) }),
30
+ /* @__PURE__ */ jsx(PopoverContent, { className: "w-[240px] p-0", children: /* @__PURE__ */ jsxs(Command, { children: [
31
+ /* @__PURE__ */ jsx(CommandInput, { placeholder }),
32
+ /* @__PURE__ */ jsxs(CommandList, { children: [
33
+ /* @__PURE__ */ jsx(CommandEmpty, { children: "No timezone found." }),
34
+ /* @__PURE__ */ jsx(CommandGroup, { children: options.map((zone) => /* @__PURE__ */ jsxs(
35
+ CommandItem,
36
+ {
37
+ value: zone,
38
+ onSelect: () => {
39
+ onChange(zone);
40
+ setOpen(false);
41
+ },
42
+ children: [
43
+ /* @__PURE__ */ jsx(Check, { className: cn("mr-2 h-4 w-4", zone === value ? "opacity-100" : "opacity-0") }),
44
+ zone
45
+ ]
46
+ },
47
+ zone
48
+ )) })
49
+ ] })
50
+ ] }) })
51
+ ] });
52
+ }
53
+ export {
54
+ TimeZoneSelect
55
+ };
56
+ //# sourceMappingURL=TimeZoneSelect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../src/core/components/ui/Sensors/TimeZoneSelect.tsx"],"sourcesContent":["'use client'\r\n\r\n// SPDX-License-Identifier: AGPL-3.0-or-later\r\n// Copyright (C) 2025 Collab Digital Twins\r\n\r\nimport { Check, ChevronsUpDown } from 'lucide-react'\r\nimport * as React from 'react'\r\n\r\nimport { cn } from '../../../utils/utils'\r\nimport { Button } from '../Button'\r\nimport { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '../Command'\r\nimport { Popover, PopoverContent, PopoverTrigger } from '../Popover'\r\n\r\nfunction supportedZones(): string[] {\r\n // Intl.supportedValuesOf is a runtime API not yet in this TS lib target; guard + narrow.\r\n const intl = Intl as unknown as { supportedValuesOf?: (k: string) => string[] }\r\n try {\r\n if (typeof intl.supportedValuesOf === 'function') return intl.supportedValuesOf('timeZone')\r\n } catch {\r\n // fall through\r\n }\r\n return ['UTC']\r\n}\r\n\r\nexport function TimeZoneSelect({\r\n value,\r\n onChange,\r\n placeholder = 'Select timezone',\r\n}: {\r\n value: string\r\n onChange: (zone: string) => void\r\n placeholder?: string\r\n}): React.ReactElement {\r\n const [open, setOpen] = React.useState(false)\r\n const zones = React.useMemo(supportedZones, [])\r\n const options = React.useMemo(() => (zones.includes(value) ? zones : [value, ...zones]), [zones, value])\r\n\r\n return (\r\n <Popover open={open} onOpenChange={setOpen}>\r\n <PopoverTrigger asChild>\r\n <Button variant=\"outline\" role=\"combobox\" aria-expanded={open} className=\"w-[240px] justify-between\">\r\n <span className=\"truncate\">{value || placeholder}</span>\r\n <ChevronsUpDown className=\"ml-2 h-4 w-4 shrink-0 opacity-50\" />\r\n </Button>\r\n </PopoverTrigger>\r\n <PopoverContent className=\"w-[240px] p-0\">\r\n <Command>\r\n <CommandInput placeholder={placeholder} />\r\n <CommandList>\r\n <CommandEmpty>No timezone found.</CommandEmpty>\r\n <CommandGroup>\r\n {options.map(zone => (\r\n <CommandItem\r\n key={zone}\r\n value={zone}\r\n onSelect={() => { onChange(zone); setOpen(false) }}\r\n >\r\n <Check className={cn('mr-2 h-4 w-4', zone === value ? 'opacity-100' : 'opacity-0')} />\r\n {zone}\r\n </CommandItem>\r\n ))}\r\n </CommandGroup>\r\n </CommandList>\r\n </Command>\r\n </PopoverContent>\r\n </Popover>\r\n )\r\n}\r\n"],"mappings":";AAwCQ,SACE,KADF;AAnCR,SAAS,OAAO,sBAAsB;AACtC,YAAY,WAAW;AAEvB,SAAS,UAAU;AACnB,SAAS,cAAc;AACvB,SAAS,SAAS,cAAc,cAAc,cAAc,aAAa,mBAAmB;AAC5F,SAAS,SAAS,gBAAgB,sBAAsB;AAExD,SAAS,iBAA2B;AAElC,QAAM,OAAO;AACb,MAAI;AACF,QAAI,OAAO,KAAK,sBAAsB,WAAY,QAAO,KAAK,kBAAkB,UAAU;AAAA,EAC5F,SAAQ;AAAA,EAER;AACA,SAAO,CAAC,KAAK;AACf;AAEO,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA,cAAc;AAChB,GAIuB;AACrB,QAAM,CAAC,MAAM,OAAO,IAAI,MAAM,SAAS,KAAK;AAC5C,QAAM,QAAQ,MAAM,QAAQ,gBAAgB,CAAC,CAAC;AAC9C,QAAM,UAAU,MAAM,QAAQ,MAAO,MAAM,SAAS,KAAK,IAAI,QAAQ,CAAC,OAAO,GAAG,KAAK,GAAI,CAAC,OAAO,KAAK,CAAC;AAEvG,SACE,qBAAC,WAAQ,MAAY,cAAc,SACjC;AAAA,wBAAC,kBAAe,SAAO,MACrB,+BAAC,UAAO,SAAQ,WAAU,MAAK,YAAW,iBAAe,MAAM,WAAU,6BACvE;AAAA,0BAAC,UAAK,WAAU,YAAY,mBAAS,aAAY;AAAA,MACjD,oBAAC,kBAAe,WAAU,oCAAmC;AAAA,OAC/D,GACF;AAAA,IACA,oBAAC,kBAAe,WAAU,iBACxB,+BAAC,WACC;AAAA,0BAAC,gBAAa,aAA0B;AAAA,MACxC,qBAAC,eACC;AAAA,4BAAC,gBAAa,gCAAkB;AAAA,QAChC,oBAAC,gBACE,kBAAQ,IAAI,UACX;AAAA,UAAC;AAAA;AAAA,YAEC,OAAO;AAAA,YACP,UAAU,MAAM;AAAE,uBAAS,IAAI;AAAG,sBAAQ,KAAK;AAAA,YAAE;AAAA,YAEjD;AAAA,kCAAC,SAAM,WAAW,GAAG,gBAAgB,SAAS,QAAQ,gBAAgB,WAAW,GAAG;AAAA,cACnF;AAAA;AAAA;AAAA,UALI;AAAA,QAMP,CACD,GACH;AAAA,SACF;AAAA,OACF,GACF;AAAA,KACF;AAEJ;","names":[]}
@@ -0,0 +1,42 @@
1
+ import { SensorDataFormat } from '../../../types/dbTypes';
2
+ export interface SensorSeriesMeta {
3
+ name?: string;
4
+ description?: string;
5
+ /** Short OGC observation type label, e.g. "OM_Measurement". */
6
+ observationType?: string;
7
+ unit?: {
8
+ name?: string;
9
+ symbol?: string;
10
+ definition?: string;
11
+ };
12
+ observedProperty?: {
13
+ name?: string;
14
+ definition?: string;
15
+ description?: string;
16
+ };
17
+ phenomenonTime?: string;
18
+ resultTime?: string;
19
+ observationCount?: number;
20
+ sensor?: {
21
+ name?: string;
22
+ description?: string;
23
+ metadata?: string;
24
+ };
25
+ properties?: Record<string, unknown>;
26
+ selfLink?: string;
27
+ }
28
+ export interface SensorSeries {
29
+ points: {
30
+ t: number;
31
+ value: number;
32
+ }[];
33
+ /** Unit symbol from an STA Datastream's unitOfMeasurement, when present. */
34
+ unit?: string;
35
+ /** For category sensors: ordinal index -> original label, for the chart tooltip. */
36
+ valueLabels?: Record<number, string>;
37
+ /** Full STA Datastream metadata, populated only for STA JSON (for the detail dialog). */
38
+ meta?: SensorSeriesMeta;
39
+ }
40
+ /** Parse a sensor data payload into a chart-ready series. CSV is header-less time,value; JSON auto-detects STA / dataArray / single reading. */
41
+ export declare function parseSensorSeries(raw: string, dataFormat: SensorDataFormat | `${SensorDataFormat}`): SensorSeries;
42
+ //# sourceMappingURL=sensorData.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sensorData.d.ts","sourceRoot":"","sources":["../../../../../src/core/components/ui/Sensors/sensorData.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAEzD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,+DAA+D;IAC/D,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,IAAI,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC9D,gBAAgB,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC/E,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,MAAM,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACnE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACpC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IACtC,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,oFAAoF;IACpF,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACpC,yFAAyF;IACzF,IAAI,CAAC,EAAE,gBAAgB,CAAA;CACxB;AA8ID,gJAAgJ;AAChJ,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,gBAAgB,GAAG,GAAG,gBAAgB,EAAE,GACnD,YAAY,CAOd"}
@@ -0,0 +1,118 @@
1
+ import { SensorDataFormat } from "../../../types/dbTypes";
2
+ function toEpoch(value) {
3
+ var _a;
4
+ const ms = Date.parse(value);
5
+ if (!Number.isNaN(ms)) return ms;
6
+ const m = /^(\d{1,2}):(\d{2})(?::(\d{2}))?$/.exec(value.trim());
7
+ if (!m) return NaN;
8
+ const now = /* @__PURE__ */ new Date();
9
+ return Date.UTC(
10
+ now.getUTCFullYear(),
11
+ now.getUTCMonth(),
12
+ now.getUTCDate(),
13
+ Number(m[1]),
14
+ Number(m[2]),
15
+ Number((_a = m[3]) != null ? _a : 0)
16
+ );
17
+ }
18
+ function coerceResult(result, labels, labelToIndex) {
19
+ if (typeof result === "number") return result;
20
+ if (typeof result === "boolean") return result ? 1 : 0;
21
+ if (typeof result === "string") {
22
+ const asNum = Number(result);
23
+ if (result.trim() !== "" && !Number.isNaN(asNum)) return asNum;
24
+ let idx = labelToIndex.get(result);
25
+ if (idx === void 0) {
26
+ idx = labelToIndex.size;
27
+ labelToIndex.set(result, idx);
28
+ labels[idx] = result;
29
+ }
30
+ return idx;
31
+ }
32
+ return NaN;
33
+ }
34
+ function parseCsv(raw) {
35
+ const points = raw.trim().split("\n").map((line) => {
36
+ const [time, valueStr] = line.split(",");
37
+ return { t: toEpoch((time != null ? time : "").trim()), value: parseFloat((valueStr != null ? valueStr : "").trim()) };
38
+ }).filter((p) => !Number.isNaN(p.value) && !Number.isNaN(p.t));
39
+ return { points };
40
+ }
41
+ function shortObservationType(iri) {
42
+ if (typeof iri !== "string" || !iri) return void 0;
43
+ const tail = iri.split("/").pop();
44
+ return tail || iri;
45
+ }
46
+ function extractStaMeta(doc) {
47
+ const uom = doc.unitOfMeasurement;
48
+ const op = doc.ObservedProperty;
49
+ const sensor = doc.Sensor;
50
+ return {
51
+ name: doc.name,
52
+ description: doc.description,
53
+ observationType: shortObservationType(doc.observationType),
54
+ unit: uom ? { name: uom.name, symbol: uom.symbol, definition: uom.definition } : void 0,
55
+ observedProperty: op ? { name: op.name, definition: op.definition, description: op.description } : void 0,
56
+ phenomenonTime: typeof doc.phenomenonTime === "string" ? doc.phenomenonTime : void 0,
57
+ resultTime: typeof doc.resultTime === "string" ? doc.resultTime : void 0,
58
+ observationCount: typeof doc["Observations@iot.count"] === "number" ? doc["Observations@iot.count"] : void 0,
59
+ sensor: sensor ? { name: sensor.name, description: sensor.description, metadata: sensor.metadata } : void 0,
60
+ properties: doc.properties && typeof doc.properties === "object" ? doc.properties : void 0,
61
+ selfLink: typeof doc["@iot.selfLink"] === "string" ? doc["@iot.selfLink"] : void 0
62
+ };
63
+ }
64
+ function parseJson(raw) {
65
+ var _a, _b, _c, _d, _e, _f, _g;
66
+ let doc;
67
+ try {
68
+ doc = JSON.parse(raw);
69
+ } catch (e) {
70
+ return { points: [] };
71
+ }
72
+ const labels = {};
73
+ const labelToIndex = /* @__PURE__ */ new Map();
74
+ const withLabels = (points, unit, meta) => ({
75
+ points,
76
+ unit,
77
+ valueLabels: Object.keys(labels).length ? labels : void 0,
78
+ meta
79
+ });
80
+ if (doc && Array.isArray(doc.dataArray)) {
81
+ const comps = Array.isArray(doc.components) ? doc.components : ["phenomenonTime", "result"];
82
+ const tIdx = comps.indexOf("phenomenonTime") === -1 ? 0 : comps.indexOf("phenomenonTime");
83
+ const rIdx = comps.indexOf("result") === -1 ? 1 : comps.indexOf("result");
84
+ const points = doc.dataArray.map((row) => ({
85
+ t: toEpoch(String(row[tIdx])),
86
+ value: coerceResult(row[rIdx], labels, labelToIndex)
87
+ })).filter((p) => !Number.isNaN(p.value) && !Number.isNaN(p.t));
88
+ return withLabels(points);
89
+ }
90
+ if (doc && Array.isArray(doc.Observations)) {
91
+ const unit = (_b = (_a = doc.unitOfMeasurement) == null ? void 0 : _a.symbol) != null ? _b : void 0;
92
+ const points = doc.Observations.map((obs) => ({
93
+ t: toEpoch(String(obs.phenomenonTime)),
94
+ value: coerceResult(obs.result, labels, labelToIndex)
95
+ })).filter((p) => !Number.isNaN(p.value) && !Number.isNaN(p.t));
96
+ return withLabels(points, unit, extractStaMeta(doc));
97
+ }
98
+ const single = (_c = doc == null ? void 0 : doc.result) != null ? _c : doc == null ? void 0 : doc.value;
99
+ if (single !== void 0) {
100
+ const value = coerceResult(single, labels, labelToIndex);
101
+ if (Number.isNaN(value)) return { points: [] };
102
+ const iso = (_d = doc.phenomenonTime) != null ? _d : doc.timestamp;
103
+ const t = iso ? toEpoch(String(iso)) : NaN;
104
+ const unit = (_g = (_f = doc.unit) != null ? _f : (_e = doc.unitOfMeasurement) == null ? void 0 : _e.symbol) != null ? _g : void 0;
105
+ if (Number.isNaN(t)) return { points: [] };
106
+ return withLabels([{ t, value }], unit);
107
+ }
108
+ return { points: [] };
109
+ }
110
+ function parseSensorSeries(raw, dataFormat) {
111
+ const series = dataFormat === SensorDataFormat.Csv ? parseCsv(raw) : parseJson(raw);
112
+ series.points.sort((a, b) => a.t - b.t);
113
+ return series;
114
+ }
115
+ export {
116
+ parseSensorSeries
117
+ };
118
+ //# sourceMappingURL=sensorData.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../src/core/components/ui/Sensors/sensorData.ts"],"sourcesContent":["// SPDX-License-Identifier: AGPL-3.0-or-later\r\n// Copyright (C) 2025 Collab Digital Twins\r\n\r\nimport { SensorDataFormat } from '../../../types/dbTypes'\r\n\r\nexport interface SensorSeriesMeta {\r\n name?: string\r\n description?: string\r\n /** Short OGC observation type label, e.g. \"OM_Measurement\". */\r\n observationType?: string\r\n unit?: { name?: string; symbol?: string; definition?: string }\r\n observedProperty?: { name?: string; definition?: string; description?: string }\r\n phenomenonTime?: string\r\n resultTime?: string\r\n observationCount?: number\r\n sensor?: { name?: string; description?: string; metadata?: string }\r\n properties?: Record<string, unknown>\r\n selfLink?: string\r\n}\r\n\r\nexport interface SensorSeries {\r\n points: { t: number; value: number }[]\r\n /** Unit symbol from an STA Datastream's unitOfMeasurement, when present. */\r\n unit?: string\r\n /** For category sensors: ordinal index -> original label, for the chart tooltip. */\r\n valueLabels?: Record<number, string>\r\n /** Full STA Datastream metadata, populated only for STA JSON (for the detail dialog). */\r\n meta?: SensorSeriesMeta\r\n}\r\n\r\n/**\r\n * ISO 8601 (or any Date-parseable) string -> epoch ms. Falls back for a bare\r\n * \"H:MM:SS\" clock (the synthetic CSV shape, no date) by assuming today's UTC date.\r\n * That fallback is a known limitation; STA/ISO inputs are exact.\r\n */\r\nfunction toEpoch(value: string): number {\r\n const ms = Date.parse(value)\r\n if (!Number.isNaN(ms)) return ms\r\n const m = /^(\\d{1,2}):(\\d{2})(?::(\\d{2}))?$/.exec(value.trim())\r\n if (!m) return NaN\r\n const now = new Date()\r\n return Date.UTC(\r\n now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(),\r\n Number(m[1]), Number(m[2]), Number(m[3] ?? 0),\r\n )\r\n}\r\n\r\nfunction coerceResult(\r\n result: unknown,\r\n labels: Record<number, string>,\r\n labelToIndex: Map<string, number>,\r\n): number {\r\n if (typeof result === 'number') return result\r\n if (typeof result === 'boolean') return result ? 1 : 0\r\n if (typeof result === 'string') {\r\n const asNum = Number(result)\r\n if (result.trim() !== '' && !Number.isNaN(asNum)) return asNum\r\n let idx = labelToIndex.get(result)\r\n if (idx === undefined) {\r\n idx = labelToIndex.size\r\n labelToIndex.set(result, idx)\r\n labels[idx] = result\r\n }\r\n return idx\r\n }\r\n return NaN\r\n}\r\n\r\nfunction parseCsv(raw: string): SensorSeries {\r\n const points = raw\r\n .trim()\r\n .split('\\n')\r\n .map(line => {\r\n const [time, valueStr] = line.split(',')\r\n return { t: toEpoch((time ?? '').trim()), value: parseFloat((valueStr ?? '').trim()) }\r\n })\r\n .filter(p => !Number.isNaN(p.value) && !Number.isNaN(p.t))\r\n return { points }\r\n}\r\n\r\n/** Short label from an OGC observationType IRI (\".../OM_Measurement\" -> \"OM_Measurement\"). */\r\nfunction shortObservationType(iri: unknown): string | undefined {\r\n if (typeof iri !== 'string' || !iri) return undefined\r\n const tail = iri.split('/').pop()\r\n return tail || iri\r\n}\r\n\r\nfunction extractStaMeta(doc: any): SensorSeriesMeta {\r\n const uom = doc.unitOfMeasurement\r\n const op = doc.ObservedProperty\r\n const sensor = doc.Sensor\r\n return {\r\n name: doc.name,\r\n description: doc.description,\r\n observationType: shortObservationType(doc.observationType),\r\n unit: uom ? { name: uom.name, symbol: uom.symbol, definition: uom.definition } : undefined,\r\n observedProperty: op ? { name: op.name, definition: op.definition, description: op.description } : undefined,\r\n phenomenonTime: typeof doc.phenomenonTime === 'string' ? doc.phenomenonTime : undefined,\r\n resultTime: typeof doc.resultTime === 'string' ? doc.resultTime : undefined,\r\n observationCount: typeof doc['Observations@iot.count'] === 'number' ? doc['Observations@iot.count'] : undefined,\r\n sensor: sensor ? { name: sensor.name, description: sensor.description, metadata: sensor.metadata } : undefined,\r\n properties: doc.properties && typeof doc.properties === 'object' ? doc.properties : undefined,\r\n selfLink: typeof doc['@iot.selfLink'] === 'string' ? doc['@iot.selfLink'] : undefined,\r\n }\r\n}\r\n\r\n// JSON shapes are external (OGC SensorThings); typed as `any` since we sniff by shape.\r\nfunction parseJson(raw: string): SensorSeries {\r\n let doc: any\r\n try {\r\n doc = JSON.parse(raw)\r\n } catch {\r\n return { points: [] }\r\n }\r\n\r\n const labels: Record<number, string> = {}\r\n const labelToIndex = new Map<string, number>()\r\n const withLabels = (\r\n points: SensorSeries['points'],\r\n unit?: string,\r\n meta?: SensorSeriesMeta,\r\n ): SensorSeries => ({\r\n points,\r\n unit,\r\n valueLabels: Object.keys(labels).length ? labels : undefined,\r\n meta,\r\n })\r\n\r\n // dataArray form: { components, dataArray: [[phenomenonTime, result], ...] }\r\n if (doc && Array.isArray(doc.dataArray)) {\r\n const comps: string[] = Array.isArray(doc.components) ? doc.components : ['phenomenonTime', 'result']\r\n const tIdx = comps.indexOf('phenomenonTime') === -1 ? 0 : comps.indexOf('phenomenonTime')\r\n const rIdx = comps.indexOf('result') === -1 ? 1 : comps.indexOf('result')\r\n const points = doc.dataArray\r\n .map((row: unknown[]) => ({\r\n t: toEpoch(String(row[tIdx])),\r\n value: coerceResult(row[rIdx], labels, labelToIndex),\r\n }))\r\n .filter((p: { t: number; value: number }) => !Number.isNaN(p.value) && !Number.isNaN(p.t))\r\n return withLabels(points)\r\n }\r\n\r\n // STA Datastream: { unitOfMeasurement, Observations: [{ phenomenonTime, result }] }\r\n if (doc && Array.isArray(doc.Observations)) {\r\n const unit: string | undefined = doc.unitOfMeasurement?.symbol ?? undefined\r\n const points = doc.Observations\r\n .map((obs: any) => ({\r\n t: toEpoch(String(obs.phenomenonTime)),\r\n value: coerceResult(obs.result, labels, labelToIndex),\r\n }))\r\n .filter((p: { t: number; value: number }) => !Number.isNaN(p.value) && !Number.isNaN(p.t))\r\n return withLabels(points, unit, extractStaMeta(doc))\r\n }\r\n\r\n // Single reading. ?format=reading emits { type, unit, timestamp, value, ... };\r\n // an STA-style single reading would use { phenomenonTime, result }. Handle both.\r\n const single = doc?.result ?? doc?.value\r\n if (single !== undefined) {\r\n const value = coerceResult(single, labels, labelToIndex)\r\n if (Number.isNaN(value)) return { points: [] }\r\n const iso = doc.phenomenonTime ?? doc.timestamp\r\n const t = iso ? toEpoch(String(iso)) : NaN\r\n const unit: string | undefined = doc.unit ?? doc.unitOfMeasurement?.symbol ?? undefined\r\n if (Number.isNaN(t)) return { points: [] }\r\n return withLabels([{ t, value }], unit)\r\n }\r\n\r\n return { points: [] }\r\n}\r\n\r\n/** Parse a sensor data payload into a chart-ready series. CSV is header-less time,value; JSON auto-detects STA / dataArray / single reading. */\r\nexport function parseSensorSeries(\r\n raw: string,\r\n dataFormat: SensorDataFormat | `${SensorDataFormat}`,\r\n): SensorSeries {\r\n const series = dataFormat === SensorDataFormat.Csv ? parseCsv(raw) : parseJson(raw)\r\n // Range filtering, the chart <Brush>, and the numeric time axis all assume points are\r\n // ascending by time. Source feeds are not guaranteed sorted (some OGC servers return\r\n // newest-first), so normalize here rather than depend on producer order.\r\n series.points.sort((a, b) => a.t - b.t)\r\n return series\r\n}\r\n"],"mappings":"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","names":[]}
@@ -0,0 +1,24 @@
1
+ export type RangePreset = 'all' | 'day' | 'hour' | 'custom';
2
+ export interface RangeBounds {
3
+ from: number;
4
+ to: number;
5
+ }
6
+ /** Bounds for a preset, computed from the latest point's time. `null` means "no filter" (all). */
7
+ export declare function rangeBounds(preset: RangePreset, points: {
8
+ t: number;
9
+ }[], custom?: RangeBounds | null): RangeBounds | null;
10
+ /** Inclusive filter. `null` bounds returns the points unchanged. */
11
+ export declare function filterByRange<T extends {
12
+ t: number;
13
+ }>(points: T[], bounds: RangeBounds | null): T[];
14
+ /**
15
+ * Start/end indices into `points` (assumed ascending by `t`) for a recharts <Brush>.
16
+ * `null` bounds spans the full series.
17
+ */
18
+ export declare function indicesForBounds(points: {
19
+ t: number;
20
+ }[], bounds: RangeBounds | null): {
21
+ startIndex: number;
22
+ endIndex: number;
23
+ };
24
+ //# sourceMappingURL=sensorRange.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sensorRange.d.ts","sourceRoot":"","sources":["../../../../../src/core/components/ui/Sensors/sensorRange.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,GAAG,QAAQ,CAAA;AAE3D,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,EAAE,EAAE,MAAM,CAAA;CACX;AAKD,kGAAkG;AAClG,wBAAgB,WAAW,CACzB,MAAM,EAAE,WAAW,EACnB,MAAM,EAAE;IAAE,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,EACvB,MAAM,CAAC,EAAE,WAAW,GAAG,IAAI,GAC1B,WAAW,GAAG,IAAI,CAMpB;AAED,oEAAoE;AACpE,wBAAgB,aAAa,CAAC,CAAC,SAAS;IAAE,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,WAAW,GAAG,IAAI,GAAG,CAAC,EAAE,CAGnG;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE;IAAE,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,EACvB,MAAM,EAAE,WAAW,GAAG,IAAI,GACzB;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAW1C"}
@@ -0,0 +1,34 @@
1
+ const HOUR = 36e5;
2
+ const DAY = 24 * HOUR;
3
+ function rangeBounds(preset, points, custom) {
4
+ if (preset === "custom") return custom != null ? custom : null;
5
+ if (preset === "all" || points.length === 0) return null;
6
+ const to = points.reduce((m, p) => Math.max(m, p.t), points[0].t);
7
+ const span = preset === "hour" ? HOUR : DAY;
8
+ return { from: to - span, to };
9
+ }
10
+ function filterByRange(points, bounds) {
11
+ if (!bounds) return points;
12
+ return points.filter((p) => p.t >= bounds.from && p.t <= bounds.to);
13
+ }
14
+ function indicesForBounds(points, bounds) {
15
+ if (points.length === 0) return { startIndex: 0, endIndex: 0 };
16
+ if (!bounds) return { startIndex: 0, endIndex: points.length - 1 };
17
+ let startIndex = points.findIndex((p) => p.t >= bounds.from);
18
+ if (startIndex < 0) startIndex = points.length - 1;
19
+ let endIndex = -1;
20
+ for (let i = points.length - 1; i >= 0; i--) {
21
+ if (points[i].t <= bounds.to) {
22
+ endIndex = i;
23
+ break;
24
+ }
25
+ }
26
+ if (endIndex < startIndex) endIndex = startIndex;
27
+ return { startIndex, endIndex };
28
+ }
29
+ export {
30
+ filterByRange,
31
+ indicesForBounds,
32
+ rangeBounds
33
+ };
34
+ //# sourceMappingURL=sensorRange.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../src/core/components/ui/Sensors/sensorRange.ts"],"sourcesContent":["// SPDX-License-Identifier: AGPL-3.0-or-later\r\n// Copyright (C) 2025 Collab Digital Twins\r\n\r\nexport type RangePreset = 'all' | 'day' | 'hour' | 'custom'\r\n\r\nexport interface RangeBounds {\r\n from: number\r\n to: number\r\n}\r\n\r\nconst HOUR = 3_600_000\r\nconst DAY = 24 * HOUR\r\n\r\n/** Bounds for a preset, computed from the latest point's time. `null` means \"no filter\" (all). */\r\nexport function rangeBounds(\r\n preset: RangePreset,\r\n points: { t: number }[],\r\n custom?: RangeBounds | null,\r\n): RangeBounds | null {\r\n if (preset === 'custom') return custom ?? null\r\n if (preset === 'all' || points.length === 0) return null\r\n const to = points.reduce((m, p) => Math.max(m, p.t), points[0].t)\r\n const span = preset === 'hour' ? HOUR : DAY\r\n return { from: to - span, to }\r\n}\r\n\r\n/** Inclusive filter. `null` bounds returns the points unchanged. */\r\nexport function filterByRange<T extends { t: number }>(points: T[], bounds: RangeBounds | null): T[] {\r\n if (!bounds) return points\r\n return points.filter(p => p.t >= bounds.from && p.t <= bounds.to)\r\n}\r\n\r\n/**\r\n * Start/end indices into `points` (assumed ascending by `t`) for a recharts <Brush>.\r\n * `null` bounds spans the full series.\r\n */\r\nexport function indicesForBounds(\r\n points: { t: number }[],\r\n bounds: RangeBounds | null,\r\n): { startIndex: number; endIndex: number } {\r\n if (points.length === 0) return { startIndex: 0, endIndex: 0 }\r\n if (!bounds) return { startIndex: 0, endIndex: points.length - 1 }\r\n let startIndex = points.findIndex(p => p.t >= bounds.from)\r\n if (startIndex < 0) startIndex = points.length - 1\r\n let endIndex = -1\r\n for (let i = points.length - 1; i >= 0; i--) {\r\n if (points[i].t <= bounds.to) { endIndex = i; break }\r\n }\r\n if (endIndex < startIndex) endIndex = startIndex\r\n return { startIndex, endIndex }\r\n}\r\n"],"mappings":"AAUA,MAAM,OAAO;AACb,MAAM,MAAM,KAAK;AAGV,SAAS,YACd,QACA,QACA,QACoB;AACpB,MAAI,WAAW,SAAU,QAAO,0BAAU;AAC1C,MAAI,WAAW,SAAS,OAAO,WAAW,EAAG,QAAO;AACpD,QAAM,KAAK,OAAO,OAAO,CAAC,GAAG,MAAM,KAAK,IAAI,GAAG,EAAE,CAAC,GAAG,OAAO,CAAC,EAAE,CAAC;AAChE,QAAM,OAAO,WAAW,SAAS,OAAO;AACxC,SAAO,EAAE,MAAM,KAAK,MAAM,GAAG;AAC/B;AAGO,SAAS,cAAuC,QAAa,QAAiC;AACnG,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO,OAAO,OAAO,OAAK,EAAE,KAAK,OAAO,QAAQ,EAAE,KAAK,OAAO,EAAE;AAClE;AAMO,SAAS,iBACd,QACA,QAC0C;AAC1C,MAAI,OAAO,WAAW,EAAG,QAAO,EAAE,YAAY,GAAG,UAAU,EAAE;AAC7D,MAAI,CAAC,OAAQ,QAAO,EAAE,YAAY,GAAG,UAAU,OAAO,SAAS,EAAE;AACjE,MAAI,aAAa,OAAO,UAAU,OAAK,EAAE,KAAK,OAAO,IAAI;AACzD,MAAI,aAAa,EAAG,cAAa,OAAO,SAAS;AACjD,MAAI,WAAW;AACf,WAAS,IAAI,OAAO,SAAS,GAAG,KAAK,GAAG,KAAK;AAC3C,QAAI,OAAO,CAAC,EAAE,KAAK,OAAO,IAAI;AAAE,iBAAW;AAAG;AAAA,IAAM;AAAA,EACtD;AACA,MAAI,WAAW,WAAY,YAAW;AACtC,SAAO,EAAE,YAAY,SAAS;AAChC;","names":[]}
@@ -0,0 +1,15 @@
1
+ import { type SensorSeries } from './sensorData';
2
+ import type { SensorDataFormat } from '../../../types/dbTypes';
3
+ export interface UseSensorSeriesResult extends SensorSeries {
4
+ isLoading: boolean;
5
+ }
6
+ /**
7
+ * Fetch `dataUrl`, parse it (CSV or OGC JSON via parseSensorSeries), and re-fetch on
8
+ * `updateFrequencyMs` (floored at 1s) while `enabled`. Each tick replaces the series (the
9
+ * synthetic API serves a rolling window ending "now"). Aborts in-flight fetches and clears
10
+ * the interval on disable/unmount/param change; a failed poll keeps the last good series.
11
+ */
12
+ export declare function useSensorSeries(dataUrl: string, dataFormat: SensorDataFormat | `${SensorDataFormat}`, updateFrequencyMs: number, opts: {
13
+ enabled: boolean;
14
+ }): UseSensorSeriesResult;
15
+ //# sourceMappingURL=useSensorSeries.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useSensorSeries.d.ts","sourceRoot":"","sources":["../../../../../src/core/components/ui/Sensors/useSensorSeries.ts"],"names":[],"mappings":"AAOA,OAAO,EAAqB,KAAK,YAAY,EAAE,MAAM,cAAc,CAAA;AAEnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAI9D,MAAM,WAAW,qBAAsB,SAAQ,YAAY;IACzD,SAAS,EAAE,OAAO,CAAA;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,gBAAgB,GAAG,GAAG,gBAAgB,EAAE,EACpD,iBAAiB,EAAE,MAAM,EACzB,IAAI,EAAE;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,GACzB,qBAAqB,CA0CvB"}