@smartnet360/svelte-components 0.0.119 → 0.0.120
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/dist/apps/antenna-tools/band-config.d.ts +53 -0
- package/dist/apps/antenna-tools/band-config.js +112 -0
- package/dist/apps/antenna-tools/components/AntennaControls.svelte +558 -0
- package/dist/apps/antenna-tools/components/AntennaControls.svelte.d.ts +16 -0
- package/dist/apps/antenna-tools/components/AntennaSettingsModal.svelte +304 -0
- package/dist/apps/antenna-tools/components/AntennaSettingsModal.svelte.d.ts +8 -0
- package/dist/apps/antenna-tools/components/AntennaTools.svelte +597 -0
- package/dist/apps/antenna-tools/components/AntennaTools.svelte.d.ts +42 -0
- package/dist/apps/antenna-tools/components/DatabaseViewer.svelte +278 -0
- package/dist/apps/antenna-tools/components/DatabaseViewer.svelte.d.ts +3 -0
- package/dist/apps/antenna-tools/components/DbNotification.svelte +67 -0
- package/dist/apps/antenna-tools/components/DbNotification.svelte.d.ts +18 -0
- package/dist/apps/antenna-tools/components/JsonImporter.svelte +115 -0
- package/dist/apps/antenna-tools/components/JsonImporter.svelte.d.ts +6 -0
- package/dist/apps/antenna-tools/components/MSIConverter.svelte +282 -0
- package/dist/apps/antenna-tools/components/MSIConverter.svelte.d.ts +6 -0
- package/dist/apps/antenna-tools/components/chart-engines/PolarAreaChart.svelte +123 -0
- package/dist/apps/antenna-tools/components/chart-engines/PolarAreaChart.svelte.d.ts +16 -0
- package/dist/apps/antenna-tools/components/chart-engines/PolarLineChart.svelte +123 -0
- package/dist/apps/antenna-tools/components/chart-engines/PolarLineChart.svelte.d.ts +16 -0
- package/dist/apps/antenna-tools/components/chart-engines/index.d.ts +9 -0
- package/dist/apps/antenna-tools/components/chart-engines/index.js +9 -0
- package/dist/apps/antenna-tools/components/index.d.ts +8 -0
- package/dist/apps/antenna-tools/components/index.js +10 -0
- package/dist/apps/antenna-tools/db.d.ts +28 -0
- package/dist/apps/antenna-tools/db.js +45 -0
- package/dist/apps/antenna-tools/index.d.ts +26 -0
- package/dist/apps/antenna-tools/index.js +40 -0
- package/dist/apps/antenna-tools/stores/antennas.d.ts +13 -0
- package/dist/apps/antenna-tools/stores/antennas.js +25 -0
- package/dist/apps/antenna-tools/stores/db-status.d.ts +32 -0
- package/dist/apps/antenna-tools/stores/db-status.js +38 -0
- package/dist/apps/antenna-tools/stores/index.d.ts +5 -0
- package/dist/apps/antenna-tools/stores/index.js +5 -0
- package/dist/apps/antenna-tools/types.d.ts +137 -0
- package/dist/apps/antenna-tools/types.js +16 -0
- package/dist/apps/antenna-tools/utils/antenna-helpers.d.ts +83 -0
- package/dist/apps/antenna-tools/utils/antenna-helpers.js +198 -0
- package/dist/apps/antenna-tools/utils/chart-engines/index.d.ts +5 -0
- package/dist/apps/antenna-tools/utils/chart-engines/index.js +5 -0
- package/dist/apps/antenna-tools/utils/chart-engines/polar-area-utils.d.ts +94 -0
- package/dist/apps/antenna-tools/utils/chart-engines/polar-area-utils.js +151 -0
- package/dist/apps/antenna-tools/utils/chart-engines/polar-line-utils.d.ts +93 -0
- package/dist/apps/antenna-tools/utils/chart-engines/polar-line-utils.js +139 -0
- package/dist/apps/antenna-tools/utils/db-utils.d.ts +50 -0
- package/dist/apps/antenna-tools/utils/db-utils.js +266 -0
- package/dist/apps/antenna-tools/utils/index.d.ts +7 -0
- package/dist/apps/antenna-tools/utils/index.js +7 -0
- package/dist/apps/antenna-tools/utils/msi-parser.d.ts +21 -0
- package/dist/apps/antenna-tools/utils/msi-parser.js +215 -0
- package/dist/apps/antenna-tools/utils/recent-antennas.d.ts +24 -0
- package/dist/apps/antenna-tools/utils/recent-antennas.js +64 -0
- package/package.json +1 -1
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Antenna Tools - Database Utilities
|
|
3
|
+
* CRUD operations for antenna data with automatic purge on import
|
|
4
|
+
*/
|
|
5
|
+
import { db } from '../db';
|
|
6
|
+
import { antennas } from '../stores/antennas';
|
|
7
|
+
import { updateDbStatus, trackDataOperation } from '../stores/db-status';
|
|
8
|
+
import { purgeAntennas, calculatePurgeStats } from '../band-config';
|
|
9
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
10
|
+
// LOAD OPERATIONS
|
|
11
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12
|
+
/**
|
|
13
|
+
* Load all antennas from database
|
|
14
|
+
*/
|
|
15
|
+
export async function loadAntennas() {
|
|
16
|
+
try {
|
|
17
|
+
updateDbStatus({ loading: true, error: null });
|
|
18
|
+
// Return empty array if db is not available (SSR environment)
|
|
19
|
+
if (!db) {
|
|
20
|
+
updateDbStatus({
|
|
21
|
+
initialized: false,
|
|
22
|
+
loading: false,
|
|
23
|
+
error: 'Database not available in this environment'
|
|
24
|
+
});
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
const data = await db.antennas.toArray();
|
|
28
|
+
antennas.set(data);
|
|
29
|
+
updateDbStatus({
|
|
30
|
+
initialized: true,
|
|
31
|
+
antennaCount: data.length,
|
|
32
|
+
lastUpdated: new Date(),
|
|
33
|
+
loading: false
|
|
34
|
+
});
|
|
35
|
+
return data;
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
console.error('Error loading antennas from IndexedDB:', error);
|
|
39
|
+
updateDbStatus({
|
|
40
|
+
loading: false,
|
|
41
|
+
error: error instanceof Error ? error.message : 'Unknown database error'
|
|
42
|
+
});
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Save raw antennas to database with automatic purge
|
|
48
|
+
*
|
|
49
|
+
* This is the main import function:
|
|
50
|
+
* 1. Takes raw antennas (with actual MHz frequencies)
|
|
51
|
+
* 2. Purges to keep only one per band (closest to center)
|
|
52
|
+
* 3. Saves purged antennas to database
|
|
53
|
+
*
|
|
54
|
+
* @param rawAntennas - Antennas parsed from MSI files
|
|
55
|
+
* @returns Import result with purge statistics
|
|
56
|
+
*/
|
|
57
|
+
export async function saveAntennasWithPurge(rawAntennas) {
|
|
58
|
+
try {
|
|
59
|
+
trackDataOperation('import', { inProgress: true, message: 'Purging frequencies...' });
|
|
60
|
+
// Step 1: Purge - keep only one antenna per band
|
|
61
|
+
const purgedAntennas = purgeAntennas(rawAntennas);
|
|
62
|
+
const purgeStats = calculatePurgeStats(rawAntennas.length, purgedAntennas);
|
|
63
|
+
console.log(`[saveAntennasWithPurge] Purge complete: ${purgeStats.totalBefore} → ${purgeStats.totalAfter} antennas (${purgeStats.reductionPercent}% reduction)`);
|
|
64
|
+
trackDataOperation('import', { inProgress: true, message: `Saving ${purgedAntennas.length} antennas...` });
|
|
65
|
+
// Step 2: Clear existing data
|
|
66
|
+
await db.antennas.clear();
|
|
67
|
+
// Step 3: Bulk add the purged antennas
|
|
68
|
+
await db.antennas.bulkAdd(purgedAntennas);
|
|
69
|
+
// Update the store
|
|
70
|
+
antennas.set(purgedAntennas);
|
|
71
|
+
// Remember that we've imported data
|
|
72
|
+
localStorage.setItem('antenna-tools-data-imported', 'true');
|
|
73
|
+
// Update database status
|
|
74
|
+
updateDbStatus({
|
|
75
|
+
initialized: true,
|
|
76
|
+
antennaCount: purgedAntennas.length,
|
|
77
|
+
lastUpdated: new Date()
|
|
78
|
+
});
|
|
79
|
+
trackDataOperation('import', {
|
|
80
|
+
inProgress: false,
|
|
81
|
+
success: true,
|
|
82
|
+
message: `Imported ${purgedAntennas.length} antennas (${purgeStats.reductionPercent}% reduction)`
|
|
83
|
+
});
|
|
84
|
+
return { success: true, purgeStats };
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
console.error('Error saving antennas to IndexedDB:', error);
|
|
88
|
+
trackDataOperation('import', {
|
|
89
|
+
inProgress: false,
|
|
90
|
+
success: false,
|
|
91
|
+
error: error instanceof Error ? error.message : 'Unknown error during import',
|
|
92
|
+
message: 'Import failed'
|
|
93
|
+
});
|
|
94
|
+
return {
|
|
95
|
+
success: false,
|
|
96
|
+
purgeStats: { totalBefore: rawAntennas.length, totalAfter: 0, removed: rawAntennas.length, reductionPercent: 100, perBand: {} },
|
|
97
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Save antennas directly (without purge) - for pre-purged data
|
|
103
|
+
*/
|
|
104
|
+
export async function saveAntennas(antennaData) {
|
|
105
|
+
try {
|
|
106
|
+
trackDataOperation('import', { inProgress: true, message: 'Saving antenna data...' });
|
|
107
|
+
// Clear existing data
|
|
108
|
+
await db.antennas.clear();
|
|
109
|
+
// Sanitize data to ensure it's cloneable for IndexedDB
|
|
110
|
+
const sanitizedData = antennaData.map(a => ({
|
|
111
|
+
name: a.name,
|
|
112
|
+
frequency: a.frequency,
|
|
113
|
+
originalFrequency: a.originalFrequency,
|
|
114
|
+
gain_dBd: a.gain_dBd,
|
|
115
|
+
tilt: a.tilt,
|
|
116
|
+
comment: a.comment,
|
|
117
|
+
horizontal: a.horizontal,
|
|
118
|
+
pattern: Array.isArray(a.pattern) ? [...a.pattern] : [],
|
|
119
|
+
vertical_pattern: Array.isArray(a.vertical_pattern) ? [...a.vertical_pattern] : [],
|
|
120
|
+
polarization: a.polarization,
|
|
121
|
+
x_pol: a.x_pol,
|
|
122
|
+
co: a.co,
|
|
123
|
+
tilt_from_filename: a.tilt_from_filename,
|
|
124
|
+
sourcePath: a.sourcePath,
|
|
125
|
+
}));
|
|
126
|
+
// Bulk add the new antennas
|
|
127
|
+
await db.antennas.bulkAdd(sanitizedData);
|
|
128
|
+
// Update the store
|
|
129
|
+
antennas.set(sanitizedData);
|
|
130
|
+
// Remember that we've imported data
|
|
131
|
+
localStorage.setItem('antenna-tools-data-imported', 'true');
|
|
132
|
+
// Update database status
|
|
133
|
+
updateDbStatus({
|
|
134
|
+
initialized: true,
|
|
135
|
+
antennaCount: sanitizedData.length,
|
|
136
|
+
lastUpdated: new Date()
|
|
137
|
+
});
|
|
138
|
+
trackDataOperation('import', {
|
|
139
|
+
inProgress: false,
|
|
140
|
+
success: true,
|
|
141
|
+
message: `Successfully imported ${sanitizedData.length} antennas`
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
console.error('Error saving antennas to IndexedDB:', error);
|
|
146
|
+
trackDataOperation('import', {
|
|
147
|
+
inProgress: false,
|
|
148
|
+
success: false,
|
|
149
|
+
error: error instanceof Error ? error.message : 'Unknown error during import',
|
|
150
|
+
message: 'Import failed'
|
|
151
|
+
});
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
156
|
+
// IMPORT FROM FILES
|
|
157
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
158
|
+
/**
|
|
159
|
+
* Import from JSON file (expects pre-purged data)
|
|
160
|
+
*/
|
|
161
|
+
export async function importFromJson(jsonFile) {
|
|
162
|
+
try {
|
|
163
|
+
trackDataOperation('import', {
|
|
164
|
+
inProgress: true,
|
|
165
|
+
message: `Reading file: ${jsonFile.name}...`
|
|
166
|
+
});
|
|
167
|
+
const text = await jsonFile.text();
|
|
168
|
+
const data = JSON.parse(text);
|
|
169
|
+
trackDataOperation('import', {
|
|
170
|
+
inProgress: true,
|
|
171
|
+
message: `Importing ${data.length} antennas...`
|
|
172
|
+
});
|
|
173
|
+
await saveAntennas(data);
|
|
174
|
+
return data;
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
console.error('Error importing antennas from JSON:', error);
|
|
178
|
+
trackDataOperation('import', {
|
|
179
|
+
inProgress: false,
|
|
180
|
+
success: false,
|
|
181
|
+
error: error instanceof Error ? error.message : 'Invalid JSON file or format',
|
|
182
|
+
message: 'Import failed'
|
|
183
|
+
});
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
188
|
+
// EXPORT OPERATIONS
|
|
189
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
190
|
+
/**
|
|
191
|
+
* Export all antennas to JSON file download
|
|
192
|
+
*/
|
|
193
|
+
export async function exportAntennas() {
|
|
194
|
+
try {
|
|
195
|
+
trackDataOperation('export', { inProgress: true, message: 'Preparing data export...' });
|
|
196
|
+
const data = await db.antennas.toArray();
|
|
197
|
+
const jsonData = JSON.stringify(data, null, 2);
|
|
198
|
+
// Create and download file
|
|
199
|
+
const blob = new Blob([jsonData], { type: 'application/json' });
|
|
200
|
+
const url = URL.createObjectURL(blob);
|
|
201
|
+
const link = document.createElement('a');
|
|
202
|
+
link.href = url;
|
|
203
|
+
link.download = `antenna-tools-${new Date().toISOString().split('T')[0]}.json`;
|
|
204
|
+
document.body.appendChild(link);
|
|
205
|
+
link.click();
|
|
206
|
+
document.body.removeChild(link);
|
|
207
|
+
URL.revokeObjectURL(url);
|
|
208
|
+
trackDataOperation('export', {
|
|
209
|
+
inProgress: false,
|
|
210
|
+
success: true,
|
|
211
|
+
message: `Successfully exported ${data.length} antennas`
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
console.error('Error exporting antennas:', error);
|
|
216
|
+
trackDataOperation('export', {
|
|
217
|
+
inProgress: false,
|
|
218
|
+
success: false,
|
|
219
|
+
error: error instanceof Error ? error.message : 'Error exporting data',
|
|
220
|
+
message: 'Export failed'
|
|
221
|
+
});
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
226
|
+
// CLEAR OPERATIONS
|
|
227
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
228
|
+
/**
|
|
229
|
+
* Clear all antenna data from database
|
|
230
|
+
*/
|
|
231
|
+
export async function clearAllAntennas() {
|
|
232
|
+
try {
|
|
233
|
+
trackDataOperation('clear', { inProgress: true, message: 'Clearing database...' });
|
|
234
|
+
await db.antennas.clear();
|
|
235
|
+
antennas.set([]);
|
|
236
|
+
localStorage.removeItem('antenna-tools-data-imported');
|
|
237
|
+
updateDbStatus({
|
|
238
|
+
antennaCount: 0,
|
|
239
|
+
lastUpdated: new Date()
|
|
240
|
+
});
|
|
241
|
+
trackDataOperation('clear', {
|
|
242
|
+
inProgress: false,
|
|
243
|
+
success: true,
|
|
244
|
+
message: 'Database cleared successfully'
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
console.error('Error clearing database:', error);
|
|
249
|
+
trackDataOperation('clear', {
|
|
250
|
+
inProgress: false,
|
|
251
|
+
success: false,
|
|
252
|
+
error: error instanceof Error ? error.message : 'Unknown error clearing database',
|
|
253
|
+
message: 'Failed to clear database'
|
|
254
|
+
});
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
259
|
+
// UTILITY FUNCTIONS
|
|
260
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
261
|
+
/**
|
|
262
|
+
* Check if data has been imported before
|
|
263
|
+
*/
|
|
264
|
+
export function hasImportedData() {
|
|
265
|
+
return localStorage.getItem('antenna-tools-data-imported') === 'true';
|
|
266
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Antenna Tools - MSI Parser
|
|
3
|
+
* Parses MSI antenna pattern files
|
|
4
|
+
*
|
|
5
|
+
* This is adapted from antenna-pattern/utils/msi-parser.ts
|
|
6
|
+
* Kept the same parsing logic as requested
|
|
7
|
+
*/
|
|
8
|
+
import type { RawAntenna } from '../types';
|
|
9
|
+
/**
|
|
10
|
+
* Parse a single MSI file and return raw antenna data
|
|
11
|
+
* @param file - MSI or PNT file
|
|
12
|
+
* @returns Parsed antenna with actual MHz frequency
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseMSIFile(file: File): Promise<RawAntenna>;
|
|
15
|
+
/**
|
|
16
|
+
* Parse a folder of MSI files recursively
|
|
17
|
+
* @param directoryHandle - File system directory handle
|
|
18
|
+
* @param recursive - Whether to process subdirectories
|
|
19
|
+
* @returns Array of parsed antennas
|
|
20
|
+
*/
|
|
21
|
+
export declare function parseFolder(directoryHandle: FileSystemDirectoryHandle, recursive?: boolean): Promise<RawAntenna[]>;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Antenna Tools - MSI Parser
|
|
3
|
+
* Parses MSI antenna pattern files
|
|
4
|
+
*
|
|
5
|
+
* This is adapted from antenna-pattern/utils/msi-parser.ts
|
|
6
|
+
* Kept the same parsing logic as requested
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Parse a single MSI file and return raw antenna data
|
|
10
|
+
* @param file - MSI or PNT file
|
|
11
|
+
* @returns Parsed antenna with actual MHz frequency
|
|
12
|
+
*/
|
|
13
|
+
export async function parseMSIFile(file) {
|
|
14
|
+
const text = await file.text();
|
|
15
|
+
const lines = text.split('\n');
|
|
16
|
+
// Initialize the result object with default values
|
|
17
|
+
const result = {
|
|
18
|
+
name: '',
|
|
19
|
+
frequency: 0,
|
|
20
|
+
gain_dBd: 0.0,
|
|
21
|
+
tilt: 'NO',
|
|
22
|
+
horizontal: 360,
|
|
23
|
+
pattern: Array(360).fill(0),
|
|
24
|
+
vertical_pattern: Array(360).fill(0)
|
|
25
|
+
};
|
|
26
|
+
// Parse filename for metadata
|
|
27
|
+
const filename = file.name.replace(/\.(msi|pnt)$/i, '');
|
|
28
|
+
const filenameParts = filename.split('_');
|
|
29
|
+
// Initialize filename metadata
|
|
30
|
+
const filenameMetadata = {
|
|
31
|
+
name_from_filename: '',
|
|
32
|
+
frequency_from_filename: 0,
|
|
33
|
+
x_pol: '',
|
|
34
|
+
co: '',
|
|
35
|
+
polarization: '',
|
|
36
|
+
tilt_from_filename: ''
|
|
37
|
+
};
|
|
38
|
+
if (filenameParts.length >= 5) {
|
|
39
|
+
filenameMetadata.name_from_filename = filenameParts[0];
|
|
40
|
+
// Extract frequency from filename (e.g., "0699" -> 699)
|
|
41
|
+
if (filenameParts.length > 1 && !isNaN(parseInt(filenameParts[1]))) {
|
|
42
|
+
filenameMetadata.frequency_from_filename = parseInt(filenameParts[1]);
|
|
43
|
+
}
|
|
44
|
+
// Extract X polarization
|
|
45
|
+
if (filenameParts.length > 2) {
|
|
46
|
+
filenameMetadata.x_pol = filenameParts[2];
|
|
47
|
+
}
|
|
48
|
+
// Extract CO
|
|
49
|
+
if (filenameParts.length > 3) {
|
|
50
|
+
filenameMetadata.co = filenameParts[3];
|
|
51
|
+
}
|
|
52
|
+
// Extract polarization (e.g., "M45" -> "-45")
|
|
53
|
+
if (filenameParts.length > 4 && filenameParts[4].startsWith('M')) {
|
|
54
|
+
filenameMetadata.polarization = filenameParts[4].replace('M', '-');
|
|
55
|
+
}
|
|
56
|
+
// Extract tilt (e.g., "03T" -> "03")
|
|
57
|
+
if (filenameParts.length > 5 && (filenameParts[5].endsWith('T') || filenameParts[5].endsWith('t'))) {
|
|
58
|
+
filenameMetadata.tilt_from_filename = filenameParts[5].replace(/[Tt]$/, '');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
let currentSection = null;
|
|
62
|
+
// Process each line
|
|
63
|
+
for (const line of lines) {
|
|
64
|
+
const trimmedLine = line.trim();
|
|
65
|
+
// Skip empty lines
|
|
66
|
+
if (!trimmedLine)
|
|
67
|
+
continue;
|
|
68
|
+
// Check for metadata lines
|
|
69
|
+
if (trimmedLine.startsWith('NAME ')) {
|
|
70
|
+
if (!filenameMetadata.name_from_filename) {
|
|
71
|
+
result.name = trimmedLine.substring(5).trim();
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
result.name = filenameMetadata.name_from_filename;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else if (trimmedLine.startsWith('FREQUENCY ')) {
|
|
78
|
+
if (!filenameMetadata.frequency_from_filename) {
|
|
79
|
+
const freqText = trimmedLine.substring(10).trim();
|
|
80
|
+
const freqParts = freqText.split(' ');
|
|
81
|
+
if (freqParts.length > 0) {
|
|
82
|
+
try {
|
|
83
|
+
result.frequency = parseInt(freqParts[0]);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
result.frequency = 0;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
result.frequency = filenameMetadata.frequency_from_filename;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else if (trimmedLine.startsWith('GAIN ')) {
|
|
95
|
+
const gainParts = trimmedLine.substring(5).trim().split(' ');
|
|
96
|
+
result.gain_dBd = parseFloat(gainParts[0]);
|
|
97
|
+
}
|
|
98
|
+
else if (trimmedLine.startsWith('TILT ')) {
|
|
99
|
+
if (!filenameMetadata.tilt_from_filename) {
|
|
100
|
+
result.tilt = trimmedLine.substring(5).trim();
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
result.tilt = filenameMetadata.tilt_from_filename;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else if (trimmedLine.startsWith('COMMENT ')) {
|
|
107
|
+
result.comment = trimmedLine.substring(8).trim();
|
|
108
|
+
}
|
|
109
|
+
else if (trimmedLine.startsWith('HORIZONTAL ')) {
|
|
110
|
+
currentSection = 'horizontal';
|
|
111
|
+
result.horizontal = parseInt(trimmedLine.substring(11).trim());
|
|
112
|
+
}
|
|
113
|
+
else if (trimmedLine.startsWith('VERTICAL ')) {
|
|
114
|
+
currentSection = 'vertical';
|
|
115
|
+
}
|
|
116
|
+
// Parse pattern data lines (pattern values represent attenuation where 0 is max signal)
|
|
117
|
+
else if (currentSection === 'horizontal' && trimmedLine.includes(' ')) {
|
|
118
|
+
try {
|
|
119
|
+
const parts = trimmedLine.split(' ').filter(part => part.trim() !== '');
|
|
120
|
+
if (parts.length >= 2) {
|
|
121
|
+
const angleStr = parts[0];
|
|
122
|
+
const attenuationStr = parts[1];
|
|
123
|
+
const angle = parseInt(angleStr);
|
|
124
|
+
const attenuation = parseFloat(attenuationStr);
|
|
125
|
+
// Ensure the angle is within bounds
|
|
126
|
+
if (angle >= 0 && angle < 360) {
|
|
127
|
+
// Store as attenuation values (0 = max signal, higher values = more attenuation)
|
|
128
|
+
result.pattern[angle] = attenuation;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
// Skip lines that can't be parsed
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else if (currentSection === 'vertical' && trimmedLine.includes(' ')) {
|
|
137
|
+
try {
|
|
138
|
+
const parts = trimmedLine.split(' ').filter(part => part.trim() !== '');
|
|
139
|
+
if (parts.length >= 2) {
|
|
140
|
+
const angleStr = parts[0];
|
|
141
|
+
const attenuationStr = parts[1];
|
|
142
|
+
const angle = parseInt(angleStr);
|
|
143
|
+
const attenuation = parseFloat(attenuationStr);
|
|
144
|
+
// Ensure the angle is within bounds
|
|
145
|
+
if (angle >= 0 && angle < 360) {
|
|
146
|
+
// Store as attenuation values (0 = max signal, higher values = more attenuation)
|
|
147
|
+
result.vertical_pattern[angle] = attenuation;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
// Skip lines that can't be parsed
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Ensure filename metadata is included in the final result
|
|
157
|
+
if (!result.name && filenameMetadata.name_from_filename) {
|
|
158
|
+
result.name = filenameMetadata.name_from_filename;
|
|
159
|
+
}
|
|
160
|
+
if (!result.frequency && filenameMetadata.frequency_from_filename) {
|
|
161
|
+
result.frequency = filenameMetadata.frequency_from_filename;
|
|
162
|
+
}
|
|
163
|
+
if (!result.tilt && filenameMetadata.tilt_from_filename) {
|
|
164
|
+
result.tilt = filenameMetadata.tilt_from_filename;
|
|
165
|
+
}
|
|
166
|
+
// Always include these fields from the filename
|
|
167
|
+
result.x_pol = filenameMetadata.x_pol;
|
|
168
|
+
result.co = filenameMetadata.co;
|
|
169
|
+
result.polarization = filenameMetadata.polarization;
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Parse a folder of MSI files recursively
|
|
174
|
+
* @param directoryHandle - File system directory handle
|
|
175
|
+
* @param recursive - Whether to process subdirectories
|
|
176
|
+
* @returns Array of parsed antennas
|
|
177
|
+
*/
|
|
178
|
+
export async function parseFolder(directoryHandle, recursive = true) {
|
|
179
|
+
const antennas = [];
|
|
180
|
+
// Process directory recursively
|
|
181
|
+
async function processDirectory(dirHandle, path = '') {
|
|
182
|
+
const iterator = dirHandle.values();
|
|
183
|
+
for await (const entry of iterator) {
|
|
184
|
+
if (entry.kind === 'file') {
|
|
185
|
+
try {
|
|
186
|
+
// Type assertion to access getFile method
|
|
187
|
+
const fileHandle = entry;
|
|
188
|
+
const file = await fileHandle.getFile();
|
|
189
|
+
const ext = file.name.split('.').pop()?.toLowerCase();
|
|
190
|
+
if (ext === 'msi' || ext === 'pnt') {
|
|
191
|
+
try {
|
|
192
|
+
const antenna = await parseMSIFile(file);
|
|
193
|
+
// Add the path info to help identify where the file was found
|
|
194
|
+
antenna.sourcePath = path ? `${path}/${file.name}` : file.name;
|
|
195
|
+
antennas.push(antenna);
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
console.error(`Error parsing file ${path ? `${path}/` : ''}${file.name}:`, error);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
console.error('Error accessing file:', error);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
else if (entry.kind === 'directory' && recursive) {
|
|
207
|
+
// Process subdirectory if recursive flag is true
|
|
208
|
+
const subDirPath = path ? `${path}/${entry.name}` : entry.name;
|
|
209
|
+
await processDirectory(entry, subDirPath);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
await processDirectory(directoryHandle);
|
|
214
|
+
return antennas;
|
|
215
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Antenna Tools - Recent Antennas
|
|
3
|
+
* Track recently selected antennas in localStorage
|
|
4
|
+
*/
|
|
5
|
+
import type { Antenna } from '../types';
|
|
6
|
+
/**
|
|
7
|
+
* Get recently selected antennas from localStorage
|
|
8
|
+
*/
|
|
9
|
+
export declare function getRecentAntennas(): Antenna[];
|
|
10
|
+
/**
|
|
11
|
+
* Add an antenna to the recent list
|
|
12
|
+
*/
|
|
13
|
+
export declare function addToRecentAntennas(antenna: Antenna): void;
|
|
14
|
+
/**
|
|
15
|
+
* Clear recent antennas list
|
|
16
|
+
*/
|
|
17
|
+
export declare function clearRecentAntennas(): void;
|
|
18
|
+
/**
|
|
19
|
+
* Sort antennas to show recent ones first, then alphabetical
|
|
20
|
+
*/
|
|
21
|
+
export declare function sortAntennasWithRecent(antennas: Antenna[]): {
|
|
22
|
+
recent: Antenna[];
|
|
23
|
+
others: Antenna[];
|
|
24
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Antenna Tools - Recent Antennas
|
|
3
|
+
* Track recently selected antennas in localStorage
|
|
4
|
+
*/
|
|
5
|
+
const RECENT_ANTENNAS_KEY = 'antenna-tools-recent';
|
|
6
|
+
const MAX_RECENT_ANTENNAS = 10;
|
|
7
|
+
/**
|
|
8
|
+
* Get recently selected antennas from localStorage
|
|
9
|
+
*/
|
|
10
|
+
export function getRecentAntennas() {
|
|
11
|
+
try {
|
|
12
|
+
const stored = localStorage.getItem(RECENT_ANTENNAS_KEY);
|
|
13
|
+
if (!stored)
|
|
14
|
+
return [];
|
|
15
|
+
const recent = JSON.parse(stored);
|
|
16
|
+
return Array.isArray(recent) ? recent : [];
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
console.warn('Failed to load recent antennas:', error);
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Add an antenna to the recent list
|
|
25
|
+
*/
|
|
26
|
+
export function addToRecentAntennas(antenna) {
|
|
27
|
+
try {
|
|
28
|
+
let recent = getRecentAntennas();
|
|
29
|
+
// Remove if already exists (to move to top)
|
|
30
|
+
recent = recent.filter(a => a.id !== antenna.id);
|
|
31
|
+
// Add to beginning
|
|
32
|
+
recent.unshift(antenna);
|
|
33
|
+
// Keep only the max number
|
|
34
|
+
recent = recent.slice(0, MAX_RECENT_ANTENNAS);
|
|
35
|
+
// Save to localStorage
|
|
36
|
+
localStorage.setItem(RECENT_ANTENNAS_KEY, JSON.stringify(recent));
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
console.warn('Failed to save recent antenna:', error);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Clear recent antennas list
|
|
44
|
+
*/
|
|
45
|
+
export function clearRecentAntennas() {
|
|
46
|
+
try {
|
|
47
|
+
localStorage.removeItem(RECENT_ANTENNAS_KEY);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
console.warn('Failed to clear recent antennas:', error);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Sort antennas to show recent ones first, then alphabetical
|
|
55
|
+
*/
|
|
56
|
+
export function sortAntennasWithRecent(antennas) {
|
|
57
|
+
const recentAntennas = getRecentAntennas();
|
|
58
|
+
const recentIds = new Set(recentAntennas.map(a => a.id));
|
|
59
|
+
// Filter antennas that exist in current list and are recent
|
|
60
|
+
const recent = recentAntennas.filter(recentAntenna => antennas.some(antenna => antenna.id === recentAntenna.id));
|
|
61
|
+
// Get other antennas (not in recent list)
|
|
62
|
+
const others = antennas.filter(antenna => !recentIds.has(antenna.id));
|
|
63
|
+
return { recent, others };
|
|
64
|
+
}
|