@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.
- package/ai/agents/html/index.d.ts +6 -0
- package/ai/agents/html/index.d.ts.map +1 -0
- package/assets-navigator/index.d.ts.map +1 -1
- package/computed-asset-properties/index.d.ts.map +1 -1
- package/context-dashboard/index.d.ts +7 -3
- package/context-dashboard/index.d.ts.map +1 -1
- package/fesm2022/c8y-ngx-components-ai-agents-html.mjs +1672 -0
- package/fesm2022/c8y-ngx-components-ai-agents-html.mjs.map +1 -0
- package/fesm2022/c8y-ngx-components-assets-navigator.mjs +4 -2
- package/fesm2022/c8y-ngx-components-assets-navigator.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-computed-asset-properties-c8y-ngx-components-computed-asset-properties-C5oS4Be-.mjs +790 -0
- package/fesm2022/c8y-ngx-components-computed-asset-properties-c8y-ngx-components-computed-asset-properties-C5oS4Be-.mjs.map +1 -0
- package/fesm2022/c8y-ngx-components-computed-asset-properties-fieldbus-item-status-config.component-Bg9mbBkF.mjs +120 -0
- package/fesm2022/c8y-ngx-components-computed-asset-properties-fieldbus-item-status-config.component-Bg9mbBkF.mjs.map +1 -0
- package/fesm2022/c8y-ngx-components-computed-asset-properties.mjs +1 -642
- package/fesm2022/c8y-ngx-components-computed-asset-properties.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-context-dashboard.mjs +19 -5
- package/fesm2022/c8y-ngx-components-context-dashboard.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-map.mjs +241 -24
- package/fesm2022/c8y-ngx-components-map.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-widgets-definitions-html-widget-ai-config.mjs +2 -1667
- package/fesm2022/c8y-ngx-components-widgets-definitions-html-widget-ai-config.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-widgets-implementations-datapoints-graph.mjs +32 -0
- package/fesm2022/c8y-ngx-components-widgets-implementations-datapoints-graph.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-widgets-implementations-map.mjs +13 -13
- package/fesm2022/c8y-ngx-components-widgets-implementations-map.mjs.map +1 -1
- package/locales/de.po +15 -6
- package/locales/es.po +15 -6
- package/locales/fr.po +15 -6
- package/locales/ja_JP.po +15 -6
- package/locales/ko.po +15 -6
- package/locales/locales.pot +21 -3
- package/locales/nl.po +15 -6
- package/locales/pl.po +15 -6
- package/locales/pt_BR.po +15 -6
- package/locales/zh_CN.po +15 -6
- package/locales/zh_TW.po +15 -6
- package/map/index.d.ts +66 -8
- package/map/index.d.ts.map +1 -1
- package/package.json +1 -1
- 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-
|
|
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
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
MapService.getStatus(
|
|
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(
|
|
592
|
-
|
|
593
|
-
|
|
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:
|
|
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.
|
|
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
|
|
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
|
-
|
|
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) {
|