@c8y/ngx-components 1023.57.0 → 1023.59.1

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 (41) hide show
  1. package/ai/agents/html/index.d.ts +6 -0
  2. package/ai/agents/html/index.d.ts.map +1 -0
  3. package/assets-navigator/index.d.ts.map +1 -1
  4. package/computed-asset-properties/index.d.ts.map +1 -1
  5. package/context-dashboard/index.d.ts +7 -3
  6. package/context-dashboard/index.d.ts.map +1 -1
  7. package/fesm2022/c8y-ngx-components-ai-agents-html.mjs +1672 -0
  8. package/fesm2022/c8y-ngx-components-ai-agents-html.mjs.map +1 -0
  9. package/fesm2022/c8y-ngx-components-assets-navigator.mjs +4 -2
  10. package/fesm2022/c8y-ngx-components-assets-navigator.mjs.map +1 -1
  11. package/fesm2022/c8y-ngx-components-computed-asset-properties-c8y-ngx-components-computed-asset-properties-C5oS4Be-.mjs +790 -0
  12. package/fesm2022/c8y-ngx-components-computed-asset-properties-c8y-ngx-components-computed-asset-properties-C5oS4Be-.mjs.map +1 -0
  13. package/fesm2022/c8y-ngx-components-computed-asset-properties-fieldbus-item-status-config.component-Bg9mbBkF.mjs +120 -0
  14. package/fesm2022/c8y-ngx-components-computed-asset-properties-fieldbus-item-status-config.component-Bg9mbBkF.mjs.map +1 -0
  15. package/fesm2022/c8y-ngx-components-computed-asset-properties.mjs +1 -642
  16. package/fesm2022/c8y-ngx-components-computed-asset-properties.mjs.map +1 -1
  17. package/fesm2022/c8y-ngx-components-context-dashboard.mjs +19 -5
  18. package/fesm2022/c8y-ngx-components-context-dashboard.mjs.map +1 -1
  19. package/fesm2022/c8y-ngx-components-map.mjs +241 -24
  20. package/fesm2022/c8y-ngx-components-map.mjs.map +1 -1
  21. package/fesm2022/c8y-ngx-components-widgets-definitions-html-widget-ai-config.mjs +2 -1667
  22. package/fesm2022/c8y-ngx-components-widgets-definitions-html-widget-ai-config.mjs.map +1 -1
  23. package/fesm2022/c8y-ngx-components-widgets-implementations-datapoints-graph.mjs +32 -0
  24. package/fesm2022/c8y-ngx-components-widgets-implementations-datapoints-graph.mjs.map +1 -1
  25. package/fesm2022/c8y-ngx-components-widgets-implementations-map.mjs +13 -13
  26. package/fesm2022/c8y-ngx-components-widgets-implementations-map.mjs.map +1 -1
  27. package/locales/de.po +15 -6
  28. package/locales/es.po +15 -6
  29. package/locales/fr.po +15 -6
  30. package/locales/ja_JP.po +15 -6
  31. package/locales/ko.po +15 -6
  32. package/locales/locales.pot +21 -3
  33. package/locales/nl.po +15 -6
  34. package/locales/pl.po +15 -6
  35. package/locales/pt_BR.po +15 -6
  36. package/locales/zh_CN.po +15 -6
  37. package/locales/zh_TW.po +15 -6
  38. package/map/index.d.ts +66 -8
  39. package/map/index.d.ts.map +1 -1
  40. package/package.json +1 -1
  41. package/widgets/implementations/datapoints-graph/index.d.ts.map +1 -1
@@ -6,12 +6,12 @@ import * as i3 from '@ngx-translate/core';
6
6
  import * as i4 from 'rxjs';
7
7
  import { of, combineLatest, defer, firstValueFrom, NEVER, BehaviorSubject, Subject, fromEvent, merge, from } from 'rxjs';
8
8
  import { map, first, takeUntil, scan, filter, switchMap, tap, mergeMap, catchError, take, debounceTime, skip } from 'rxjs/operators';
9
- import { gettext } from '@c8y/ngx-components/gettext';
10
9
  import * as i1 from '@c8y/client';
11
10
  import { QueriesUtil } from '@c8y/client';
12
11
  import { latLng, latLngBounds } from 'leaflet';
13
12
  import { get, remove, isUndefined, flatten, every, isNull, isEmpty, cloneDeep } from 'lodash-es';
14
13
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
14
+ import { gettext } from '@c8y/ngx-components/gettext';
15
15
  import { NgClass, AsyncPipe, CommonModule } from '@angular/common';
16
16
  import { TooltipDirective, TooltipModule } from 'ngx-bootstrap/tooltip';
17
17
  import { FormsModule } from '@angular/forms';
@@ -123,6 +123,42 @@ class MapService {
123
123
  }
124
124
  return 'text-muted';
125
125
  }
126
+ /**
127
+ * Returns the status of a group (highest severity among its devices).
128
+ * @param group The group of devices.
129
+ * @returns The status string representing the highest severity in the group.
130
+ */
131
+ static getGroupStatus(group) {
132
+ let mostCriticalStatus = 'text-muted';
133
+ let highestSeverity = 0;
134
+ for (const device of group.items) {
135
+ const status = this.getStatus(device);
136
+ const severity = this.getStatusSeverity(status);
137
+ if (severity > highestSeverity) {
138
+ highestSeverity = severity;
139
+ mostCriticalStatus = status;
140
+ if (severity === 4) {
141
+ return status; // Critical is highest, return immediately
142
+ }
143
+ }
144
+ }
145
+ return mostCriticalStatus;
146
+ }
147
+ /**
148
+ * Maps status strings to severity levels for comparison.
149
+ * @param status The status string.
150
+ * @returns Numeric severity level (higher = more severe).
151
+ */
152
+ static getStatusSeverity(status) {
153
+ const severityMap = {
154
+ 'status critical': 4,
155
+ 'status major': 3,
156
+ 'status minor': 2,
157
+ 'status warning': 1,
158
+ 'text-muted': 0
159
+ };
160
+ return severityMap[status] ?? 0;
161
+ }
126
162
  /**
127
163
  * @ignore: Only DI.
128
164
  */
@@ -555,9 +591,8 @@ class ClusterMap {
555
591
  }
556
592
  setClusterToBigMarker(map, count, leaflet) {
557
593
  const bound = this.rect.getBounds();
558
- const text = this.translateService.instant(gettext('Zoom in'));
559
594
  const divMarker = leaflet.divIcon({
560
- html: `<div class="c8y-map-marker-count" data-count="${count}" title="${text}"></div>`
595
+ html: `<div><div class="popover top show"><span class="popover-content p-l-0 p-t-0 p-b-0 p-r-4 d-flex a-i-center"><i class="dlt-c8y-icon-marker"></i><span class="text-12 text-nowrap">${count}</span></span><span class="arrow" style="translate: -50% 0"></span></div></div>`
561
596
  });
562
597
  const labelIcon = leaflet.marker(bound.getCenter(), {
563
598
  icon: divMarker
@@ -580,17 +615,19 @@ class ClusterMap {
580
615
  }
581
616
  }
582
617
  trackBy(index, item) {
583
- const trackItems = [
584
- item.id,
585
- item.c8y_Position.lat,
586
- item.c8y_Position.lng,
587
- MapService.getStatus(item)
588
- ];
618
+ const trackItems = item.items.map(i => [
619
+ i.id,
620
+ i.c8y_Position.lat,
621
+ i.c8y_Position.lng,
622
+ MapService.getStatus(i)
623
+ ]);
589
624
  return trackItems.join('');
590
625
  }
591
- removeMarkerFromMap(device) {
592
- const markers = this.markers.filter((marker) => marker.asset?.id === device.id);
593
- markers.forEach(marker => marker.remove());
626
+ removeMarkerFromMap(group) {
627
+ group.items.forEach(device => {
628
+ const markers = this.markers.filter((marker) => marker.asset?.id === device.id);
629
+ markers.forEach(marker => marker.remove());
630
+ });
594
631
  }
595
632
  }
596
633
 
@@ -649,6 +686,7 @@ class MapComponent {
649
686
  this.unsubscribeTrigger$ = new Subject();
650
687
  this.destroy$ = new Subject();
651
688
  this.markerTitle = gettext('Marker at position {{lat}}, {{lng}}');
689
+ this.markerGroupTitle = gettext('Group of {{count}} assets');
652
690
  this.initOutputs();
653
691
  }
654
692
  /**
@@ -712,7 +750,7 @@ class MapComponent {
712
750
  }
713
751
  }
714
752
  /**
715
- * Finds a marker on the map by asset, event, or ID.
753
+ * Finds a marker on the map by asset, event, or ID. (only if it is a C8yMarker)
716
754
  * @param moOrId Asset, event, or string ID to search for.
717
755
  * @returns The found marker or undefined.
718
756
  */
@@ -734,9 +772,22 @@ class MapComponent {
734
772
  * @returns The created marker.
735
773
  */
736
774
  getAssetMarker(asset) {
737
- if (!asset) {
775
+ if (!asset || asset.items.length === 0) {
738
776
  return;
739
777
  }
778
+ if (asset.items.length === 1) {
779
+ return this.getAssetMarkerSingle(asset.items[0]);
780
+ }
781
+ else {
782
+ return this.getGroupedMarker(asset);
783
+ }
784
+ }
785
+ /**
786
+ * Creates and returns a marker for a single asset, including icon and popup.
787
+ * @param asset The asset to create a marker for.
788
+ * @returns The created marker.
789
+ */
790
+ getAssetMarkerSingle(asset) {
740
791
  const icon = this.getAssetIcon(asset);
741
792
  const { lat, lng } = asset.c8y_Position;
742
793
  const leafletMarker = this.leaflet.marker(this.geo.getLatLong(asset), {
@@ -747,6 +798,113 @@ class MapComponent {
747
798
  this.bindPopup(marker, asset);
748
799
  return marker;
749
800
  }
801
+ /**
802
+ * Returns a marker with popup for a group of assets (clustered).
803
+ * @param group The asset group.
804
+ * @returns A marker for a group of assets (clustered).
805
+ */
806
+ getGroupedMarker(group) {
807
+ const latLngs = group.items.map(item => this.leaflet.latLng(item.c8y_Position.lat, item.c8y_Position.lng));
808
+ const bounds = this.leaflet.latLngBounds(latLngs);
809
+ const center = bounds.getCenter();
810
+ const icon = this.getGroupAssetIcon(group);
811
+ const leafletMarker = this.leaflet.marker(center, {
812
+ icon,
813
+ title: this.translateService.instant(this.markerGroupTitle, { count: group.items.length })
814
+ });
815
+ const asset = group.items[0];
816
+ const marker = getC8yMarker(leafletMarker, asset);
817
+ marker.on('click', () => {
818
+ this.map.fitBounds(bounds, { padding: [50, 50] });
819
+ // Check if all markers are at the same position (within a small threshold)
820
+ const areAllSamePosition = latLngs.every(ll => Math.abs(ll.lat - center.lat) < 0.0001 && Math.abs(ll.lng - center.lng) < 0.0001);
821
+ // If all markers are at the same position and zoom is low, create spider layout
822
+ if (areAllSamePosition && this.map && this.map.getZoom() >= 10) {
823
+ this.createSpiderMarkers(group, center, marker);
824
+ }
825
+ });
826
+ return marker;
827
+ }
828
+ /**
829
+ * Creates spider markers for a group of assets at the same position.
830
+ * @param group The group of assets to create spider markers for.
831
+ * @param center The center position of the group.
832
+ * @param originalMarker The marker which was replaced by spider markers.
833
+ */
834
+ createSpiderMarkers(group, center, originalMarker) {
835
+ const markerGroup = this.leaflet.featureGroup();
836
+ const items = group.items;
837
+ const count = items.length;
838
+ const radius = 0.0002; // Adjust based on your needs
839
+ const angleStep = (2 * Math.PI) / count;
840
+ originalMarker.setOpacity(0); // Hide the original marker
841
+ items.forEach((item, index) => {
842
+ const angle = angleStep * index;
843
+ const offsetLat = center.lat + radius * Math.cos(angle);
844
+ const offsetLng = center.lng + radius * Math.sin(angle);
845
+ const markerPos = this.leaflet.latLng(offsetLat, offsetLng);
846
+ // Create spider leg (line from center to marker)
847
+ const polyline = this.leaflet.polyline([center, markerPos], {
848
+ color: '#666',
849
+ weight: 2,
850
+ opacity: 1
851
+ });
852
+ polyline.addTo(markerGroup);
853
+ // Create individual marker
854
+ const icon = this.getAssetIcon(item, [6, -12]);
855
+ const leafletMarker = this.leaflet.marker(markerPos, {
856
+ icon,
857
+ title: this.translateService.instant(this.markerTitle, {
858
+ lat: item.c8y_Position.lat,
859
+ lng: item.c8y_Position.lng
860
+ })
861
+ });
862
+ const marker = getC8yMarker(leafletMarker, item);
863
+ this.bindPopup(marker, item, new this.leaflet.Point(-2, -20));
864
+ marker.addTo(markerGroup);
865
+ });
866
+ // Create red center dot
867
+ const centerIcon = this.leaflet.divIcon({
868
+ html: '<div style="background-color: red; width: 12px; height: 12px; border-radius: 50%; border: 2px solid white;"></div>',
869
+ className: 'spider-center-marker',
870
+ iconSize: [12, 12],
871
+ iconAnchor: [6, 6]
872
+ });
873
+ const centerMarker = this.leaflet.marker(center, {
874
+ icon: centerIcon,
875
+ title: this.translateService.instant(gettext('Click to collapse'))
876
+ });
877
+ const restoreOriginalMarker = () => {
878
+ this.map.off('zoomstart', restoreOriginalMarker);
879
+ this.map.off('movestart', restoreOriginalMarker);
880
+ markerGroup.remove();
881
+ originalMarker.setOpacity(1);
882
+ };
883
+ // Add click handler to collapse spider and remove it
884
+ // if user navigates away
885
+ centerMarker.on('click', restoreOriginalMarker);
886
+ this.map.on('zoomstart', restoreOriginalMarker);
887
+ this.map.on('movestart', restoreOriginalMarker);
888
+ centerMarker.addTo(markerGroup);
889
+ markerGroup.addTo(this.map);
890
+ }
891
+ /**
892
+ * The icon for a group of assets (clustered).
893
+ * It shows the count and is colored based on the highest severity status in the group.
894
+ * @param group The group of assets.
895
+ * @returns The marker icon for the group.
896
+ */
897
+ getGroupAssetIcon(group) {
898
+ const status = MapService.getGroupStatus(group);
899
+ const color = this.config.color ? `style='color: ${this.config.color};'` : '';
900
+ const moreThan99 = group.items.length > 99 ? 'more-than-99' : '';
901
+ const icon = this.leaflet.divIcon({
902
+ html: `<div><div class="popover top show"><span class="popover-content p-l-0 p-t-0 p-b-0 p-r-4 d-flex a-i-center"><i class="dlt-c8y-icon-marker ${status}" ${color}></i><span class="text-12 text-nowrap ${moreThan99}">${group.items.length > 99 ? '99+' : group.items.length}</span></span><span class="arrow" style="translate: -50% 0"></span></div></div>`,
903
+ className: 'c8y-map-marker-icon',
904
+ iconAnchor: [21, 40]
905
+ });
906
+ return icon;
907
+ }
750
908
  /**
751
909
  * Creates and returns a marker for a tracking event, including icon and popup.
752
910
  * @param event The event to create a marker for.
@@ -769,9 +927,10 @@ class MapComponent {
769
927
  /**
770
928
  * Returns a Leaflet icon for the given asset, using config or asset icon and color.
771
929
  * @param asset The asset to get the icon for.
930
+ * @param anchor The icon anchor point.
772
931
  * @returns The Leaflet icon.
773
932
  */
774
- getAssetIcon(asset) {
933
+ getAssetIcon(asset, anchor = [8, 8]) {
775
934
  const assetTypeIcon = this.config.icon || asset.icon?.name;
776
935
  const status = MapService.getStatus(asset);
777
936
  const color = this.config.color ? `style='color: ${this.config.color};'` : '';
@@ -779,7 +938,7 @@ class MapComponent {
779
938
  html: `<div class="dlt-c8y-icon-marker icon-3x ${status}" ${color}><i class="dlt-c8y-icon-${assetTypeIcon || 'data-transfer'}" /></div>`,
780
939
  className: 'c8y-map-marker-icon',
781
940
  // iconAnchor is used to set the marker accurately on click
782
- iconAnchor: [8, 8]
941
+ iconAnchor: anchor
783
942
  });
784
943
  return icon;
785
944
  }
@@ -825,7 +984,7 @@ class MapComponent {
825
984
  assets = Array.isArray(this.assets) ? this.assets : [this.assets];
826
985
  }
827
986
  assets.forEach(asset => {
828
- const marker = this.getAssetMarker(asset);
987
+ const marker = this.getAssetMarkerSingle(asset);
829
988
  this.addMarkerToMap(marker);
830
989
  });
831
990
  if (!this.config.center) {
@@ -1064,7 +1223,7 @@ class MapComponent {
1064
1223
  this.map.fitBounds(bounds, this.config.fitBoundsOptions);
1065
1224
  }
1066
1225
  }
1067
- bindPopup(marker, context) {
1226
+ bindPopup(marker, context, offset = new this.leaflet.Point(-3, -40)) {
1068
1227
  if (this.popup) {
1069
1228
  marker.on('click', () => {
1070
1229
  this.popup.viewContainer.clear();
@@ -1075,7 +1234,7 @@ class MapComponent {
1075
1234
  marker
1076
1235
  .unbindPopup()
1077
1236
  .bindPopup(this.popup.elementRef.nativeElement.previousSibling, {
1078
- offset: [-3, -40],
1237
+ offset,
1079
1238
  maxWidth: 140,
1080
1239
  autoPan: true,
1081
1240
  closeButton: false
@@ -1275,7 +1434,7 @@ class ClusterMapComponent extends MapComponent {
1275
1434
  }
1276
1435
  clusters.forEach(cluster => cluster.render(this.map));
1277
1436
  if (clusters.length === 1 && clusters[0].positions?.length === 1 && this.config.follow) {
1278
- const position = clusters[0].positions[0];
1437
+ const position = clusters[0].positions[0].items[0];
1279
1438
  const newPos = { lat: position.c8y_Position.lat, lng: position.c8y_Position.lng };
1280
1439
  if (this.skipNextFollowMove) {
1281
1440
  this.skipNextFollowMove = false;
@@ -1324,7 +1483,7 @@ class ClusterMapComponent extends MapComponent {
1324
1483
  // Try to use current position from clusters if available (prevents jumping to old position)
1325
1484
  const hasClusterWithPosition = this.clusters?.length === 1 && this.clusters[0].positions?.length === 1;
1326
1485
  if (hasClusterWithPosition) {
1327
- this.assets = this.clusters[0].positions[0];
1486
+ this.assets = this.clusters[0].positions[0].items[0];
1328
1487
  }
1329
1488
  else if (this.rootNode?.c8y_Position && this.rootNode?.c8y_IsDevice) {
1330
1489
  this.assets = this.rootNode;
@@ -1436,7 +1595,7 @@ class ClusterMapComponent extends MapComponent {
1436
1595
  // Use current cluster position if available
1437
1596
  const hasClusterWithPosition = this.clusters?.length === 1 && this.clusters[0].positions?.length === 1;
1438
1597
  if (hasClusterWithPosition) {
1439
- this.assets = this.clusters[0].positions[0];
1598
+ this.assets = this.clusters[0].positions[0].items[0];
1440
1599
  }
1441
1600
  else if (this.rootNode?.c8y_Position && this.rootNode?.c8y_IsDevice) {
1442
1601
  this.assets = this.rootNode;
@@ -1452,7 +1611,7 @@ class ClusterMapComponent extends MapComponent {
1452
1611
  // Extract and preserve position from cluster for later use
1453
1612
  const hasClusterWithPosition = this.clusters?.length === 1 && this.clusters[0].positions?.length === 1;
1454
1613
  if (hasClusterWithPosition) {
1455
- const position = this.clusters[0].positions[0];
1614
+ const position = this.clusters[0].positions[0].items[0];
1456
1615
  this.assets = position; // Store for later use when resuming to realtime
1457
1616
  }
1458
1617
  }
@@ -1568,9 +1727,67 @@ class ClusterMapComponent extends MapComponent {
1568
1727
  return cluster;
1569
1728
  }
1570
1729
  cluster.removeClusterToBigMarker();
1571
- cluster.positions = await this.mapService.getPositionMOsFromBound(cluster.rect.getBounds(), this.rootNode);
1730
+ const positionMOs = await this.mapService.getPositionMOsFromBound(cluster.rect.getBounds(), this.rootNode);
1731
+ const NEARBY_GROUPING_THRESHOLD_PX = 25;
1732
+ const groupedPositions = this.groupNearbyAssets(positionMOs, NEARBY_GROUPING_THRESHOLD_PX);
1733
+ cluster.positions = groupedPositions;
1572
1734
  return cluster;
1573
1735
  }
1736
+ /**
1737
+ * Groups nearby assets based on pixel distance threshold.
1738
+ * @param assets Array of assets to group.
1739
+ * @param thresholdPixels Distance threshold in pixels.
1740
+ * @returns Array of asset groups.
1741
+ */
1742
+ groupNearbyAssets(assets, thresholdPixels) {
1743
+ // Get current zoom level and adjust threshold accordingly
1744
+ const currentZoom = this.map.getZoom();
1745
+ const zoomThreshold = 15; // Zoom level above which grouping is disabled
1746
+ const zoomFactor = 1 - currentZoom / zoomThreshold;
1747
+ const NEARBY_GROUPING_MIN_ZOOM_FACTOR = 0.3;
1748
+ const adjustedThreshold = thresholdPixels * Math.max(NEARBY_GROUPING_MIN_ZOOM_FACTOR, zoomFactor);
1749
+ // Sort assets by latitude first, then longitude for efficient proximity checks
1750
+ const sortedAssets = [...assets].sort((a, b) => {
1751
+ const latDiff = a.c8y_Position.lat - b.c8y_Position.lat;
1752
+ return latDiff !== 0 ? latDiff : a.c8y_Position.lng - b.c8y_Position.lng;
1753
+ });
1754
+ const groups = [];
1755
+ const processed = new Set();
1756
+ sortedAssets.forEach(asset => {
1757
+ if (processed.has(asset.id)) {
1758
+ return;
1759
+ }
1760
+ const group = [asset];
1761
+ processed.add(asset.id);
1762
+ sortedAssets.forEach(otherAsset => {
1763
+ if (processed.has(otherAsset.id)) {
1764
+ return;
1765
+ }
1766
+ const isClose = group.some(groupAsset => this.areMarkersClose(groupAsset, otherAsset, adjustedThreshold));
1767
+ if (isClose) {
1768
+ group.push(otherAsset);
1769
+ processed.add(otherAsset.id);
1770
+ }
1771
+ });
1772
+ groups.push({ items: group });
1773
+ });
1774
+ return groups;
1775
+ }
1776
+ /**
1777
+ * Checks if two assets are within the specified pixel distance.
1778
+ * @param asset1 First asset.
1779
+ * @param asset2 Second asset.
1780
+ * @param thresholdPixels Distance threshold in pixels.
1781
+ * @returns True if assets are close, false otherwise.
1782
+ */
1783
+ areMarkersClose(asset1, asset2, thresholdPixels) {
1784
+ const latLng1 = this.leaflet.latLng(asset1.c8y_Position.lat, asset1.c8y_Position.lng);
1785
+ const latLng2 = this.leaflet.latLng(asset2.c8y_Position.lat, asset2.c8y_Position.lng);
1786
+ const point1 = this.map.latLngToContainerPoint(latLng1);
1787
+ const point2 = this.map.latLngToContainerPoint(latLng2);
1788
+ const distance = point1.distanceTo(point2);
1789
+ return distance <= thresholdPixels;
1790
+ }
1574
1791
  createOrUpdateCluster(rects) {
1575
1792
  const isNew = rects.length !== this.clusters.length;
1576
1793
  if (isNew) {