@smartnet360/svelte-components 0.0.125 → 0.0.127

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 (48) hide show
  1. package/dist/core/Auth/auth.svelte.js +47 -2
  2. package/dist/map-v3/demo/DemoMap.svelte +36 -0
  3. package/dist/map-v3/demo/demo-custom-cells.d.ts +21 -0
  4. package/dist/map-v3/demo/demo-custom-cells.js +48 -0
  5. package/dist/map-v3/features/cells/custom/components/CustomCellFilterControl.svelte +220 -0
  6. package/dist/map-v3/features/cells/custom/components/CustomCellFilterControl.svelte.d.ts +15 -0
  7. package/dist/map-v3/features/cells/custom/components/CustomCellSetManager.svelte +306 -0
  8. package/dist/map-v3/features/cells/custom/components/CustomCellSetManager.svelte.d.ts +10 -0
  9. package/dist/map-v3/features/cells/custom/components/index.d.ts +5 -0
  10. package/dist/map-v3/features/cells/custom/components/index.js +5 -0
  11. package/dist/map-v3/features/cells/custom/index.d.ts +32 -0
  12. package/dist/map-v3/features/cells/custom/index.js +35 -0
  13. package/dist/map-v3/features/cells/custom/layers/CustomCellsLayer.svelte +262 -0
  14. package/dist/map-v3/features/cells/custom/layers/CustomCellsLayer.svelte.d.ts +10 -0
  15. package/dist/map-v3/features/cells/custom/layers/index.d.ts +4 -0
  16. package/dist/map-v3/features/cells/custom/layers/index.js +4 -0
  17. package/dist/map-v3/features/cells/custom/logic/csv-parser.d.ts +20 -0
  18. package/dist/map-v3/features/cells/custom/logic/csv-parser.js +164 -0
  19. package/dist/map-v3/features/cells/custom/logic/index.d.ts +5 -0
  20. package/dist/map-v3/features/cells/custom/logic/index.js +5 -0
  21. package/dist/map-v3/features/cells/custom/logic/tree-adapter.d.ts +24 -0
  22. package/dist/map-v3/features/cells/custom/logic/tree-adapter.js +67 -0
  23. package/dist/map-v3/features/cells/custom/stores/custom-cell-sets.svelte.d.ts +78 -0
  24. package/dist/map-v3/features/cells/custom/stores/custom-cell-sets.svelte.js +242 -0
  25. package/dist/map-v3/features/cells/custom/stores/index.d.ts +4 -0
  26. package/dist/map-v3/features/cells/custom/stores/index.js +4 -0
  27. package/dist/map-v3/features/cells/custom/types.d.ts +83 -0
  28. package/dist/map-v3/features/cells/custom/types.js +23 -0
  29. package/dist/map-v3/features/sites/custom/components/CustomSiteFilterControl.svelte +203 -0
  30. package/dist/map-v3/features/sites/custom/components/CustomSiteFilterControl.svelte.d.ts +15 -0
  31. package/dist/map-v3/features/sites/custom/components/CustomSiteSetManager.svelte +261 -0
  32. package/dist/map-v3/features/sites/custom/components/CustomSiteSetManager.svelte.d.ts +10 -0
  33. package/dist/map-v3/features/sites/custom/index.d.ts +13 -0
  34. package/dist/map-v3/features/sites/custom/index.js +16 -0
  35. package/dist/map-v3/features/sites/custom/layers/CustomSitesLayer.svelte +201 -0
  36. package/dist/map-v3/features/sites/custom/layers/CustomSitesLayer.svelte.d.ts +8 -0
  37. package/dist/map-v3/features/sites/custom/logic/csv-parser.d.ts +12 -0
  38. package/dist/map-v3/features/sites/custom/logic/csv-parser.js +182 -0
  39. package/dist/map-v3/features/sites/custom/logic/tree-adapter.d.ts +16 -0
  40. package/dist/map-v3/features/sites/custom/logic/tree-adapter.js +59 -0
  41. package/dist/map-v3/features/sites/custom/stores/custom-site-sets.svelte.d.ts +78 -0
  42. package/dist/map-v3/features/sites/custom/stores/custom-site-sets.svelte.js +248 -0
  43. package/dist/map-v3/features/sites/custom/types.d.ts +74 -0
  44. package/dist/map-v3/features/sites/custom/types.js +8 -0
  45. package/dist/map-v3/index.d.ts +2 -0
  46. package/dist/map-v3/index.js +4 -0
  47. package/dist/map-v3/shared/controls/MapControl.svelte +27 -3
  48. package/package.json +1 -1
@@ -0,0 +1,201 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Custom Sites Layer
4
+ *
5
+ * Renders custom site points on the map as circle markers.
6
+ * Supports per-site sizing via sizeFactor and per-group coloring.
7
+ */
8
+ import { getContext, onDestroy } from 'svelte';
9
+ import mapboxgl, { type Map as MapboxMap } from 'mapbox-gl';
10
+ import type { MapStore } from '../../../../core/stores/map.store.svelte';
11
+ import type { CustomSiteSetsStore } from '../stores/custom-site-sets.svelte';
12
+ import type { CustomSiteSet } from '../types';
13
+
14
+ interface Props {
15
+ /** The custom site sets store */
16
+ setsStore: CustomSiteSetsStore;
17
+ }
18
+
19
+ let { setsStore }: Props = $props();
20
+
21
+ const mapStore = getContext<MapStore>('MAP_CONTEXT');
22
+ const SOURCE_PREFIX = 'custom-sites-source-';
23
+ const LAYER_PREFIX = 'custom-sites-layer-';
24
+
25
+ // Track which layers we've added
26
+ let addedLayers = new Set<string>();
27
+
28
+ // Update layers when data changes
29
+ $effect(() => {
30
+ const map = mapStore?.map;
31
+ const _version = setsStore.version;
32
+ const sets = setsStore.sets;
33
+
34
+ if (!map) return;
35
+
36
+ // Remove layers for deleted sets
37
+ const currentSetIds = new Set(sets.map(s => s.id));
38
+ for (const layerId of addedLayers) {
39
+ const setId = layerId.replace(LAYER_PREFIX, '');
40
+ if (!currentSetIds.has(setId)) {
41
+ removeLayer(map, setId);
42
+ }
43
+ }
44
+
45
+ // Update or create layers for each set
46
+ for (const set of sets) {
47
+ updateSetLayer(map, set);
48
+ }
49
+ });
50
+
51
+ onDestroy(() => {
52
+ const map = mapStore?.map;
53
+ if (map) {
54
+ for (const layerId of addedLayers) {
55
+ const setId = layerId.replace(LAYER_PREFIX, '');
56
+ removeLayer(map, setId);
57
+ }
58
+ }
59
+ });
60
+
61
+ function updateSetLayer(map: MapboxMap, set: CustomSiteSet) {
62
+ const sourceId = SOURCE_PREFIX + set.id;
63
+ const layerId = LAYER_PREFIX + set.id;
64
+
65
+ // Build GeoJSON features for visible sites
66
+ const features = buildFeatures(set);
67
+
68
+ // Check if source exists
69
+ const existingSource = map.getSource(sourceId);
70
+
71
+ if (existingSource) {
72
+ // Update existing source data
73
+ (existingSource as mapboxgl.GeoJSONSource).setData({
74
+ type: 'FeatureCollection',
75
+ features
76
+ });
77
+
78
+ // Update layer paint properties
79
+ if (map.getLayer(layerId)) {
80
+ map.setPaintProperty(layerId, 'circle-opacity', set.visible ? set.opacity : 0);
81
+ map.setPaintProperty(layerId, 'circle-stroke-opacity', set.visible ? set.opacity : 0);
82
+ }
83
+ } else {
84
+ // Create new source and layer
85
+ map.addSource(sourceId, {
86
+ type: 'geojson',
87
+ data: {
88
+ type: 'FeatureCollection',
89
+ features
90
+ }
91
+ });
92
+
93
+ map.addLayer({
94
+ id: layerId,
95
+ type: 'circle',
96
+ source: sourceId,
97
+ paint: {
98
+ 'circle-radius': ['get', 'radius'],
99
+ 'circle-color': ['get', 'color'],
100
+ 'circle-opacity': set.visible ? set.opacity : 0,
101
+ 'circle-stroke-width': 1,
102
+ 'circle-stroke-color': '#ffffff',
103
+ 'circle-stroke-opacity': set.visible ? set.opacity : 0
104
+ }
105
+ });
106
+
107
+ addedLayers.add(layerId);
108
+
109
+ // Add popup on click
110
+ map.on('click', layerId, (e) => {
111
+ if (!e.features || e.features.length === 0) return;
112
+
113
+ const feature = e.features[0];
114
+ const props = feature.properties;
115
+ if (!props) return;
116
+
117
+ // Build popup content
118
+ let html = `<div class="custom-site-popup">`;
119
+ html += `<strong>${props.id}</strong>`;
120
+ html += `<div class="small text-muted">${props.group}</div>`;
121
+
122
+ // Add extra fields
123
+ if (props.extraFields) {
124
+ try {
125
+ const extras = JSON.parse(props.extraFields);
126
+ for (const [key, value] of Object.entries(extras)) {
127
+ html += `<div class="small"><strong>${key}:</strong> ${value}</div>`;
128
+ }
129
+ } catch (e) {
130
+ // Ignore parse errors
131
+ }
132
+ }
133
+
134
+ html += `</div>`;
135
+
136
+ new mapboxgl.Popup()
137
+ .setLngLat(e.lngLat)
138
+ .setHTML(html)
139
+ .addTo(map);
140
+ });
141
+
142
+ // Change cursor on hover
143
+ map.on('mouseenter', layerId, () => {
144
+ map.getCanvas().style.cursor = 'pointer';
145
+ });
146
+
147
+ map.on('mouseleave', layerId, () => {
148
+ map.getCanvas().style.cursor = '';
149
+ });
150
+ }
151
+ }
152
+
153
+ function removeLayer(map: MapboxMap, setId: string) {
154
+ const sourceId = SOURCE_PREFIX + setId;
155
+ const layerId = LAYER_PREFIX + setId;
156
+
157
+ try {
158
+ if (map.getLayer(layerId)) {
159
+ map.removeLayer(layerId);
160
+ }
161
+ if (map.getSource(sourceId)) {
162
+ map.removeSource(sourceId);
163
+ }
164
+ addedLayers.delete(layerId);
165
+ } catch (e) {
166
+ console.warn(`[CustomSitesLayer] Error removing layer ${layerId}:`, e);
167
+ }
168
+ }
169
+
170
+ function buildFeatures(set: CustomSiteSet): GeoJSON.Feature[] {
171
+ const features: GeoJSON.Feature[] = [];
172
+
173
+ for (const site of set.sites) {
174
+ // Skip if group is not visible
175
+ if (!set.visibleGroups.has(site.customGroup)) continue;
176
+
177
+ const color = set.groupColors.get(site.customGroup) || '#666666';
178
+ const radius = set.baseSize * site.sizeFactor;
179
+
180
+ features.push({
181
+ type: 'Feature',
182
+ geometry: {
183
+ type: 'Point',
184
+ coordinates: [site.lon, site.lat]
185
+ },
186
+ properties: {
187
+ id: site.id,
188
+ group: site.customGroup,
189
+ radius,
190
+ color,
191
+ sizeFactor: site.sizeFactor,
192
+ extraFields: JSON.stringify(site.extraFields)
193
+ }
194
+ });
195
+ }
196
+
197
+ return features;
198
+ }
199
+ </script>
200
+
201
+ <!-- This component only manages map layers, no DOM output -->
@@ -0,0 +1,8 @@
1
+ import type { CustomSiteSetsStore } from '../stores/custom-site-sets.svelte';
2
+ interface Props {
3
+ /** The custom site sets store */
4
+ setsStore: CustomSiteSetsStore;
5
+ }
6
+ declare const CustomSitesLayer: import("svelte").Component<Props, {}, "">;
7
+ type CustomSitesLayer = ReturnType<typeof CustomSitesLayer>;
8
+ export default CustomSitesLayer;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Custom Sites CSV Parser
3
+ *
4
+ * Parses CSV files with site coordinates and grouping information.
5
+ * Required columns: id, lat, lon
6
+ * Optional columns: customGroup/subgroup/group, sizeFactor, any extras
7
+ */
8
+ import type { CustomSiteImportResult } from '../types';
9
+ /**
10
+ * Parse a CSV string into CustomSite objects
11
+ */
12
+ export declare function parseCustomSitesCsv(csvContent: string): CustomSiteImportResult;
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Custom Sites CSV Parser
3
+ *
4
+ * Parses CSV files with site coordinates and grouping information.
5
+ * Required columns: id, lat, lon
6
+ * Optional columns: customGroup/subgroup/group, sizeFactor, any extras
7
+ */
8
+ /** Column name variations we accept */
9
+ const ID_COLUMNS = ['id', 'site_id', 'siteid', 'site'];
10
+ const LAT_COLUMNS = ['lat', 'latitude', 'y'];
11
+ const LON_COLUMNS = ['lon', 'lng', 'longitude', 'long', 'x'];
12
+ const GROUP_COLUMNS = ['customgroup', 'custom_group', 'subgroup', 'sub_group', 'group'];
13
+ const SIZE_COLUMNS = ['sizefactor', 'size_factor', 'size'];
14
+ /**
15
+ * Parse a CSV string into CustomSite objects
16
+ */
17
+ export function parseCustomSitesCsv(csvContent) {
18
+ const lines = csvContent.trim().split(/\r?\n/);
19
+ if (lines.length < 2) {
20
+ return {
21
+ sites: [],
22
+ groups: [],
23
+ invalidRows: 0,
24
+ errors: ['CSV must have a header row and at least one data row']
25
+ };
26
+ }
27
+ // Parse header
28
+ const headerLine = lines[0];
29
+ const headers = parseCSVLine(headerLine).map(h => h.toLowerCase().trim());
30
+ // Find required column indices
31
+ const idIndex = findColumnIndex(headers, ID_COLUMNS);
32
+ const latIndex = findColumnIndex(headers, LAT_COLUMNS);
33
+ const lonIndex = findColumnIndex(headers, LON_COLUMNS);
34
+ // Validate required columns
35
+ const missingColumns = [];
36
+ if (idIndex === -1)
37
+ missingColumns.push('id');
38
+ if (latIndex === -1)
39
+ missingColumns.push('lat/latitude');
40
+ if (lonIndex === -1)
41
+ missingColumns.push('lon/longitude');
42
+ if (missingColumns.length > 0) {
43
+ return {
44
+ sites: [],
45
+ groups: [],
46
+ invalidRows: 0,
47
+ errors: [`Missing required columns: ${missingColumns.join(', ')}`]
48
+ };
49
+ }
50
+ // Find optional column indices
51
+ const groupIndex = findColumnIndex(headers, GROUP_COLUMNS);
52
+ const sizeIndex = findColumnIndex(headers, SIZE_COLUMNS);
53
+ // Identify extra columns (not id, lat, lon, group, size)
54
+ const usedIndices = new Set([idIndex, latIndex, lonIndex]);
55
+ if (groupIndex !== -1)
56
+ usedIndices.add(groupIndex);
57
+ if (sizeIndex !== -1)
58
+ usedIndices.add(sizeIndex);
59
+ const extraColumnIndices = [];
60
+ headers.forEach((header, idx) => {
61
+ if (!usedIndices.has(idx) && header) {
62
+ extraColumnIndices.push({ index: idx, name: header });
63
+ }
64
+ });
65
+ // Parse data rows
66
+ const sites = [];
67
+ const groupsSet = new Set();
68
+ const errors = [];
69
+ let invalidRows = 0;
70
+ for (let i = 1; i < lines.length; i++) {
71
+ const line = lines[i].trim();
72
+ if (!line)
73
+ continue;
74
+ const values = parseCSVLine(line);
75
+ // Extract required fields
76
+ const id = values[idIndex]?.trim();
77
+ const latStr = values[latIndex]?.trim();
78
+ const lonStr = values[lonIndex]?.trim();
79
+ if (!id) {
80
+ errors.push(`Row ${i + 1}: Missing id`);
81
+ invalidRows++;
82
+ continue;
83
+ }
84
+ const lat = parseFloat(latStr);
85
+ const lon = parseFloat(lonStr);
86
+ if (isNaN(lat) || isNaN(lon)) {
87
+ errors.push(`Row ${i + 1}: Invalid coordinates (lat: ${latStr}, lon: ${lonStr})`);
88
+ invalidRows++;
89
+ continue;
90
+ }
91
+ // Validate coordinate ranges
92
+ if (lat < -90 || lat > 90) {
93
+ errors.push(`Row ${i + 1}: Latitude out of range: ${lat}`);
94
+ invalidRows++;
95
+ continue;
96
+ }
97
+ if (lon < -180 || lon > 180) {
98
+ errors.push(`Row ${i + 1}: Longitude out of range: ${lon}`);
99
+ invalidRows++;
100
+ continue;
101
+ }
102
+ // Extract optional fields
103
+ const customGroup = groupIndex !== -1
104
+ ? (values[groupIndex]?.trim() || 'Ungrouped')
105
+ : 'Ungrouped';
106
+ const sizeFactorStr = sizeIndex !== -1 ? values[sizeIndex]?.trim() : '';
107
+ const sizeFactor = sizeFactorStr ? parseFloat(sizeFactorStr) : 1.0;
108
+ const finalSizeFactor = isNaN(sizeFactor) || sizeFactor <= 0 ? 1.0 : sizeFactor;
109
+ // Extract extra fields
110
+ const extraFields = {};
111
+ for (const { index, name } of extraColumnIndices) {
112
+ const value = values[index]?.trim();
113
+ if (value) {
114
+ extraFields[name] = value;
115
+ }
116
+ }
117
+ sites.push({
118
+ id,
119
+ lat,
120
+ lon,
121
+ customGroup,
122
+ sizeFactor: finalSizeFactor,
123
+ extraFields
124
+ });
125
+ groupsSet.add(customGroup);
126
+ }
127
+ return {
128
+ sites,
129
+ groups: Array.from(groupsSet).sort(),
130
+ invalidRows,
131
+ errors: errors.slice(0, 10) // Limit error messages
132
+ };
133
+ }
134
+ /**
135
+ * Find the index of a column by checking multiple possible names
136
+ */
137
+ function findColumnIndex(headers, possibleNames) {
138
+ for (const name of possibleNames) {
139
+ const index = headers.indexOf(name);
140
+ if (index !== -1)
141
+ return index;
142
+ }
143
+ return -1;
144
+ }
145
+ /**
146
+ * Parse a single CSV line, handling quoted values
147
+ */
148
+ function parseCSVLine(line) {
149
+ const result = [];
150
+ let current = '';
151
+ let inQuotes = false;
152
+ for (let i = 0; i < line.length; i++) {
153
+ const char = line[i];
154
+ const nextChar = line[i + 1];
155
+ if (inQuotes) {
156
+ if (char === '"' && nextChar === '"') {
157
+ current += '"';
158
+ i++;
159
+ }
160
+ else if (char === '"') {
161
+ inQuotes = false;
162
+ }
163
+ else {
164
+ current += char;
165
+ }
166
+ }
167
+ else {
168
+ if (char === '"') {
169
+ inQuotes = true;
170
+ }
171
+ else if (char === ',') {
172
+ result.push(current);
173
+ current = '';
174
+ }
175
+ else {
176
+ current += char;
177
+ }
178
+ }
179
+ }
180
+ result.push(current);
181
+ return result;
182
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Custom Sites Tree Adapter
3
+ *
4
+ * Builds TreeView nodes from a CustomSiteSet for hierarchical display.
5
+ * Structure: Root -> Groups -> Sites
6
+ */
7
+ import type { TreeNode } from '../../../../../core/TreeView';
8
+ import type { CustomSiteSet } from '../types';
9
+ /**
10
+ * Build a tree structure from a CustomSiteSet
11
+ */
12
+ export declare function buildCustomSiteTree(set: CustomSiteSet): TreeNode[];
13
+ /**
14
+ * Get site counts per group
15
+ */
16
+ export declare function getGroupCounts(set: CustomSiteSet): Map<string, number>;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Custom Sites Tree Adapter
3
+ *
4
+ * Builds TreeView nodes from a CustomSiteSet for hierarchical display.
5
+ * Structure: Root -> Groups -> Sites
6
+ */
7
+ /**
8
+ * Build a tree structure from a CustomSiteSet
9
+ */
10
+ export function buildCustomSiteTree(set) {
11
+ const rootId = `root-${set.id}`;
12
+ // Group sites by customGroup
13
+ const groupedSites = new Map();
14
+ for (const site of set.sites) {
15
+ const existing = groupedSites.get(site.customGroup) || [];
16
+ existing.push(site);
17
+ groupedSites.set(site.customGroup, existing);
18
+ }
19
+ // Build group nodes
20
+ const groupNodes = [];
21
+ for (const group of set.groups) {
22
+ const sites = groupedSites.get(group) || [];
23
+ const groupId = `group-${set.id}-${group}`;
24
+ groupNodes.push({
25
+ id: groupId,
26
+ label: `${group} (${sites.length})`,
27
+ metadata: {
28
+ type: 'group',
29
+ groupId: group,
30
+ setId: set.id,
31
+ count: sites.length
32
+ }
33
+ });
34
+ }
35
+ // Create root node
36
+ const rootNode = {
37
+ id: rootId,
38
+ label: set.name,
39
+ children: groupNodes,
40
+ metadata: {
41
+ type: 'root',
42
+ setId: set.id,
43
+ totalSites: set.sites.length,
44
+ groupCount: set.groups.length
45
+ }
46
+ };
47
+ return [rootNode];
48
+ }
49
+ /**
50
+ * Get site counts per group
51
+ */
52
+ export function getGroupCounts(set) {
53
+ const counts = new Map();
54
+ for (const site of set.sites) {
55
+ const current = counts.get(site.customGroup) || 0;
56
+ counts.set(site.customGroup, current + 1);
57
+ }
58
+ return counts;
59
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Custom Site Sets Store
3
+ *
4
+ * Manages multiple custom site sets with CRUD operations,
5
+ * localStorage persistence, and group visibility/color controls.
6
+ */
7
+ import type { CustomSite, CustomSiteSet, CustomSiteImportResult } from '../types';
8
+ /**
9
+ * Store for managing custom site sets
10
+ */
11
+ export declare class CustomSiteSetsStore {
12
+ /** All custom site sets */
13
+ private _sets;
14
+ /** Version counter for reactivity triggers */
15
+ private _version;
16
+ constructor();
17
+ get sets(): CustomSiteSet[];
18
+ get version(): number;
19
+ /**
20
+ * Import sites from CSV content
21
+ */
22
+ importFromCsv(csvContent: string, fileName: string): CustomSiteImportResult & {
23
+ setId?: string;
24
+ };
25
+ /**
26
+ * Create a new custom site set
27
+ */
28
+ createSet(name: string, sites: CustomSite[], groups: string[]): string;
29
+ /**
30
+ * Toggle visibility of entire set
31
+ */
32
+ toggleSetVisibility(setId: string): void;
33
+ /**
34
+ * Toggle visibility of a specific group within a set
35
+ */
36
+ toggleGroupVisibility(setId: string, groupId: string): void;
37
+ /**
38
+ * Set color for a group
39
+ */
40
+ setGroupColor(setId: string, groupId: string, color: string): void;
41
+ /**
42
+ * Update base size for a set
43
+ */
44
+ setBaseSize(setId: string, size: number): void;
45
+ /**
46
+ * Update opacity for a set
47
+ */
48
+ setOpacity(setId: string, opacity: number): void;
49
+ /**
50
+ * Rename a set
51
+ */
52
+ renameSet(setId: string, newName: string): void;
53
+ /**
54
+ * Remove a set
55
+ */
56
+ removeSet(setId: string): void;
57
+ /**
58
+ * Clear all sets
59
+ */
60
+ clearAll(): void;
61
+ private saveToStorage;
62
+ private loadFromStorage;
63
+ /**
64
+ * Get a specific set by ID
65
+ */
66
+ getSet(setId: string): CustomSiteSet | undefined;
67
+ /**
68
+ * Get visible sites for a set (filtered by visible groups)
69
+ */
70
+ getVisibleSites(setId: string): CustomSite[];
71
+ /**
72
+ * Get all visible sites across all sets
73
+ */
74
+ getAllVisibleSites(): Array<{
75
+ site: CustomSite;
76
+ set: CustomSiteSet;
77
+ }>;
78
+ }