@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,282 @@
|
|
|
1
|
+
<svelte:options runes={true} />
|
|
2
|
+
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
import { parseFolder } from '../utils/msi-parser';
|
|
5
|
+
import { saveAntennasWithPurge, type ImportResult } from '../utils/db-utils';
|
|
6
|
+
import { STANDARD_BANDS } from '../band-config';
|
|
7
|
+
import type { RawAntenna, Antenna } from '../types';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
onDataRefresh?: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let { onDataRefresh }: Props = $props();
|
|
14
|
+
|
|
15
|
+
let rawAntennas = $state<RawAntenna[]>([]);
|
|
16
|
+
let isLoading = $state(false);
|
|
17
|
+
let error = $state('');
|
|
18
|
+
let message = $state('');
|
|
19
|
+
let importResult = $state<ImportResult | null>(null);
|
|
20
|
+
|
|
21
|
+
let recursiveScan = $state(true);
|
|
22
|
+
|
|
23
|
+
async function selectFolder() {
|
|
24
|
+
try {
|
|
25
|
+
// Check if File System Access API is supported
|
|
26
|
+
if (!('showDirectoryPicker' in window)) {
|
|
27
|
+
error = 'Your browser does not support the File System Access API. Please use a Chromium-based browser.';
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
isLoading = true;
|
|
32
|
+
error = '';
|
|
33
|
+
message = 'Selecting folder...';
|
|
34
|
+
importResult = null;
|
|
35
|
+
|
|
36
|
+
// Ask user to select a folder
|
|
37
|
+
const directoryPicker = window.showDirectoryPicker as () => Promise<FileSystemDirectoryHandle>;
|
|
38
|
+
const directoryHandle = await directoryPicker();
|
|
39
|
+
message = recursiveScan ?
|
|
40
|
+
'Reading and parsing files recursively (including all subfolders)...' :
|
|
41
|
+
'Reading and parsing files (top-level folder only)...';
|
|
42
|
+
|
|
43
|
+
// Parse all MSI/PNT files in the folder, with recursive option
|
|
44
|
+
rawAntennas = await parseFolder(directoryHandle, recursiveScan);
|
|
45
|
+
|
|
46
|
+
if (rawAntennas.length === 0) {
|
|
47
|
+
message = 'No MSI or PNT files found in the selected folder.';
|
|
48
|
+
} else {
|
|
49
|
+
message = `Successfully parsed ${rawAntennas.length} antenna files${recursiveScan ? ' from all folders' : ''}.`;
|
|
50
|
+
}
|
|
51
|
+
} catch (e) {
|
|
52
|
+
if (e instanceof Error) {
|
|
53
|
+
if (e.name === 'AbortError') {
|
|
54
|
+
// User cancelled the folder picker
|
|
55
|
+
error = '';
|
|
56
|
+
message = 'Folder selection cancelled.';
|
|
57
|
+
} else {
|
|
58
|
+
error = `Error: ${e.message}`;
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
error = 'An unknown error occurred.';
|
|
62
|
+
}
|
|
63
|
+
} finally {
|
|
64
|
+
isLoading = false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function saveToDatabase() {
|
|
69
|
+
if (rawAntennas.length === 0) {
|
|
70
|
+
error = 'No antenna data to save.';
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
isLoading = true;
|
|
76
|
+
message = 'Purging frequencies and saving to database...';
|
|
77
|
+
|
|
78
|
+
// Save with automatic purge
|
|
79
|
+
importResult = await saveAntennasWithPurge(rawAntennas);
|
|
80
|
+
|
|
81
|
+
if (importResult.success) {
|
|
82
|
+
const stats = importResult.purgeStats;
|
|
83
|
+
message = `Successfully imported ${stats.totalAfter} antennas (${stats.reductionPercent}% reduction from ${stats.totalBefore} raw files).`;
|
|
84
|
+
|
|
85
|
+
// Notify parent of data refresh
|
|
86
|
+
onDataRefresh?.();
|
|
87
|
+
} else {
|
|
88
|
+
error = importResult.error || 'Failed to save antennas to database';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
} catch (e) {
|
|
92
|
+
error = e instanceof Error ? e.message : 'Failed to save antennas to database';
|
|
93
|
+
} finally {
|
|
94
|
+
isLoading = false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function downloadJson() {
|
|
99
|
+
if (rawAntennas.length === 0) {
|
|
100
|
+
error = 'No antenna data to download.';
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Download raw (unpurged) data
|
|
105
|
+
const jsonData = JSON.stringify(rawAntennas, null, 2);
|
|
106
|
+
const blob = new Blob([jsonData], { type: 'application/json' });
|
|
107
|
+
const url = URL.createObjectURL(blob);
|
|
108
|
+
const link = document.createElement('a');
|
|
109
|
+
link.href = url;
|
|
110
|
+
link.download = `antenna-raw-${new Date().toISOString().split('T')[0]}.json`;
|
|
111
|
+
document.body.appendChild(link);
|
|
112
|
+
link.click();
|
|
113
|
+
document.body.removeChild(link);
|
|
114
|
+
URL.revokeObjectURL(url);
|
|
115
|
+
|
|
116
|
+
message = 'JSON file downloaded (raw data, not purged).';
|
|
117
|
+
}
|
|
118
|
+
</script>
|
|
119
|
+
|
|
120
|
+
<div class="card border-0 shadow-sm">
|
|
121
|
+
<div class="card-body">
|
|
122
|
+
<header class="d-flex flex-column flex-lg-row align-items-lg-center justify-content-between gap-3 mb-4">
|
|
123
|
+
<div>
|
|
124
|
+
<h1 class="h3 mb-2">Convert MSI / PNT to JSON</h1>
|
|
125
|
+
<p class="text-muted mb-0">Scan a folder of antenna definitions. Files will be purged to keep only standard bands.</p>
|
|
126
|
+
</div>
|
|
127
|
+
<span class="badge text-bg-success">Local processing</span>
|
|
128
|
+
</header>
|
|
129
|
+
|
|
130
|
+
<!-- Standard Bands Info -->
|
|
131
|
+
<div class="alert alert-info border-0 bg-info-subtle mb-4">
|
|
132
|
+
<strong><i class="bi bi-info-circle me-2"></i>Standard Bands:</strong>
|
|
133
|
+
<div class="d-flex flex-wrap gap-2 mt-2">
|
|
134
|
+
{#each STANDARD_BANDS as band}
|
|
135
|
+
<span class="badge text-bg-primary">{band.name} MHz ({band.dlMin}-{band.dlMax})</span>
|
|
136
|
+
{/each}
|
|
137
|
+
</div>
|
|
138
|
+
<small class="d-block mt-2 text-muted">Only antennas within these frequency ranges will be kept (closest to center frequency).</small>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<div class="d-flex flex-column gap-4">
|
|
142
|
+
<div class="text-center">
|
|
143
|
+
<button class="btn btn-primary btn-lg px-4" onclick={selectFolder} disabled={isLoading}>
|
|
144
|
+
{#if isLoading}
|
|
145
|
+
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
|
146
|
+
Processing…
|
|
147
|
+
{:else}
|
|
148
|
+
<i class="bi bi-folder-plus me-2"></i>
|
|
149
|
+
Choose folder with MSI / PNT files
|
|
150
|
+
{/if}
|
|
151
|
+
</button>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<div class="d-flex justify-content-center">
|
|
155
|
+
<div class="form-check form-switch">
|
|
156
|
+
<input class="form-check-input" type="checkbox" bind:checked={recursiveScan} id="recursiveScan" />
|
|
157
|
+
<label class="form-check-label" for="recursiveScan">
|
|
158
|
+
<i class="bi bi-arrow-down-circle me-1"></i>
|
|
159
|
+
Include sub-folders during scan
|
|
160
|
+
</label>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
{#if rawAntennas.length > 0}
|
|
165
|
+
<div class="row g-3">
|
|
166
|
+
<div class="col-md-6">
|
|
167
|
+
<button class="btn btn-success w-100" onclick={saveToDatabase} disabled={isLoading}>
|
|
168
|
+
{#if isLoading}
|
|
169
|
+
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
|
170
|
+
Saving…
|
|
171
|
+
{:else}
|
|
172
|
+
<i class="bi bi-database-add me-2"></i>
|
|
173
|
+
Import to database (with purge)
|
|
174
|
+
{/if}
|
|
175
|
+
</button>
|
|
176
|
+
</div>
|
|
177
|
+
<div class="col-md-6">
|
|
178
|
+
<button class="btn btn-outline-secondary w-100" onclick={downloadJson}>
|
|
179
|
+
<i class="bi bi-download me-2"></i>
|
|
180
|
+
Download raw JSON
|
|
181
|
+
</button>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
{/if}
|
|
185
|
+
|
|
186
|
+
{#if message && !error}
|
|
187
|
+
<div class="alert alert-info border-0 bg-info-subtle text-info-emphasis" role="alert">
|
|
188
|
+
<i class="bi bi-info-circle me-2"></i>
|
|
189
|
+
{message}
|
|
190
|
+
</div>
|
|
191
|
+
{/if}
|
|
192
|
+
|
|
193
|
+
{#if error}
|
|
194
|
+
<div class="alert alert-danger border-0 bg-danger-subtle text-danger-emphasis" role="alert">
|
|
195
|
+
<i class="bi bi-exclamation-octagon me-2"></i>
|
|
196
|
+
{error}
|
|
197
|
+
</div>
|
|
198
|
+
{/if}
|
|
199
|
+
|
|
200
|
+
<!-- Purge Results -->
|
|
201
|
+
{#if importResult?.success}
|
|
202
|
+
<div class="card border-success">
|
|
203
|
+
<div class="card-header bg-success text-white">
|
|
204
|
+
<i class="bi bi-check-circle me-2"></i>Import Complete
|
|
205
|
+
</div>
|
|
206
|
+
<div class="card-body">
|
|
207
|
+
<div class="row text-center">
|
|
208
|
+
<div class="col-4">
|
|
209
|
+
<div class="h3 text-muted">{importResult.purgeStats.totalBefore}</div>
|
|
210
|
+
<small class="text-muted">Raw files</small>
|
|
211
|
+
</div>
|
|
212
|
+
<div class="col-4">
|
|
213
|
+
<div class="h3 text-success">{importResult.purgeStats.totalAfter}</div>
|
|
214
|
+
<small class="text-muted">Kept</small>
|
|
215
|
+
</div>
|
|
216
|
+
<div class="col-4">
|
|
217
|
+
<div class="h3 text-danger">{importResult.purgeStats.removed}</div>
|
|
218
|
+
<small class="text-muted">Removed</small>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<hr>
|
|
223
|
+
|
|
224
|
+
<h6>Antennas per band:</h6>
|
|
225
|
+
<div class="d-flex flex-wrap gap-2">
|
|
226
|
+
{#each Object.entries(importResult.purgeStats.perBand) as [band, count]}
|
|
227
|
+
<span class="badge text-bg-secondary">{band} MHz: {count}</span>
|
|
228
|
+
{/each}
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
{/if}
|
|
233
|
+
|
|
234
|
+
{#if rawAntennas.length > 0 && !importResult}
|
|
235
|
+
<div class="card border border-light-subtle">
|
|
236
|
+
<div class="card-body">
|
|
237
|
+
<div class="d-flex flex-column flex-md-row align-items-md-center justify-content-between gap-3 mb-3">
|
|
238
|
+
<div class="d-flex align-items-center gap-3">
|
|
239
|
+
<span class="badge text-bg-warning fs-6">{rawAntennas.length}</span>
|
|
240
|
+
<h2 class="h5 mb-0">Raw files parsed (before purge)</h2>
|
|
241
|
+
</div>
|
|
242
|
+
<button
|
|
243
|
+
class="btn btn-link text-decoration-none p-0"
|
|
244
|
+
type="button"
|
|
245
|
+
data-bs-toggle="collapse"
|
|
246
|
+
data-bs-target="#parsedAntennaList"
|
|
247
|
+
aria-expanded="false"
|
|
248
|
+
>
|
|
249
|
+
<i class="bi bi-list-ul me-2"></i>
|
|
250
|
+
View details
|
|
251
|
+
</button>
|
|
252
|
+
</div>
|
|
253
|
+
<div class="collapse" id="parsedAntennaList">
|
|
254
|
+
<div class="list-group overflow-auto" style="max-height: 320px;">
|
|
255
|
+
{#each rawAntennas as antenna, index}
|
|
256
|
+
<div class="list-group-item border-0 border-bottom">
|
|
257
|
+
<div class="d-flex justify-content-between align-items-start">
|
|
258
|
+
<div>
|
|
259
|
+
<h3 class="h6 mb-1">{antenna.name}</h3>
|
|
260
|
+
<div class="d-flex flex-wrap gap-3 text-muted small">
|
|
261
|
+
<span><i class="bi bi-broadcast me-1"></i>{antenna.frequency} MHz</span>
|
|
262
|
+
<span><i class="bi bi-arrow-clockwise me-1"></i>Tilt {antenna.tilt}°</span>
|
|
263
|
+
</div>
|
|
264
|
+
{#if antenna.sourcePath}
|
|
265
|
+
<div class="text-muted small mt-1">
|
|
266
|
+
<i class="bi bi-folder me-1"></i>
|
|
267
|
+
{antenna.sourcePath}
|
|
268
|
+
</div>
|
|
269
|
+
{/if}
|
|
270
|
+
</div>
|
|
271
|
+
<span class="badge text-bg-light">#{index + 1}</span>
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
{/each}
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
{/if}
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<svelte:options runes={true} />
|
|
2
|
+
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
import { onMount } from 'svelte';
|
|
5
|
+
import { browser } from '$app/environment';
|
|
6
|
+
import type { Antenna } from '../../types';
|
|
7
|
+
import {
|
|
8
|
+
createPolarAreaChartData,
|
|
9
|
+
createPolarAreaLayout,
|
|
10
|
+
type PolarAreaTraceData
|
|
11
|
+
} from '../../utils/chart-engines/polar-area-utils';
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
selectedAntenna?: Antenna | null;
|
|
15
|
+
selectedAntenna2?: Antenna | null;
|
|
16
|
+
viewMode?: 'single' | 'compare';
|
|
17
|
+
patternType?: 'horizontal' | 'vertical';
|
|
18
|
+
patternDisplayMode?: 'normalized' | 'gain-adjusted';
|
|
19
|
+
ant1ElectricalTilt?: number;
|
|
20
|
+
ant1MechanicalTilt?: number;
|
|
21
|
+
ant2ElectricalTilt?: number;
|
|
22
|
+
ant2MechanicalTilt?: number;
|
|
23
|
+
title?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let {
|
|
27
|
+
selectedAntenna = null,
|
|
28
|
+
selectedAntenna2 = null,
|
|
29
|
+
viewMode = 'single',
|
|
30
|
+
patternType = 'vertical',
|
|
31
|
+
patternDisplayMode = 'normalized',
|
|
32
|
+
ant1ElectricalTilt = 0,
|
|
33
|
+
ant1MechanicalTilt = 0,
|
|
34
|
+
ant2ElectricalTilt = 0,
|
|
35
|
+
ant2MechanicalTilt = 0,
|
|
36
|
+
title = 'Antenna Pattern - Polar Area Chart'
|
|
37
|
+
}: Props = $props();
|
|
38
|
+
|
|
39
|
+
let Plotly: any;
|
|
40
|
+
let chartDiv = $state<HTMLDivElement>();
|
|
41
|
+
let chartInitialized = $state(false);
|
|
42
|
+
|
|
43
|
+
// Initialize Plotly on mount
|
|
44
|
+
onMount(async () => {
|
|
45
|
+
if (browser) {
|
|
46
|
+
try {
|
|
47
|
+
Plotly = await import('plotly.js-dist-min');
|
|
48
|
+
await initializeChart();
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error('Failed to load Plotly:', error);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Update chart when data changes
|
|
56
|
+
$effect(() => {
|
|
57
|
+
if (chartInitialized) {
|
|
58
|
+
updateChart();
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
async function initializeChart() {
|
|
63
|
+
if (!Plotly || !chartDiv) return;
|
|
64
|
+
|
|
65
|
+
const data = createPolarAreaChartData(
|
|
66
|
+
selectedAntenna,
|
|
67
|
+
selectedAntenna2,
|
|
68
|
+
viewMode,
|
|
69
|
+
patternType === 'horizontal',
|
|
70
|
+
patternType === 'vertical',
|
|
71
|
+
ant1ElectricalTilt,
|
|
72
|
+
ant1MechanicalTilt,
|
|
73
|
+
ant2ElectricalTilt,
|
|
74
|
+
ant2MechanicalTilt,
|
|
75
|
+
patternDisplayMode
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const layout = createPolarAreaLayout(title);
|
|
79
|
+
const config = {
|
|
80
|
+
responsive: true,
|
|
81
|
+
displayModeBar: true,
|
|
82
|
+
displaylogo: false,
|
|
83
|
+
modeBarButtonsToRemove: [
|
|
84
|
+
'zoom2d', 'pan2d', 'select2d', 'lasso2d',
|
|
85
|
+
'zoomIn2d', 'zoomOut2d', 'autoScale2d',
|
|
86
|
+
'resetScale2d', 'toggleSpikelines', 'toImage'
|
|
87
|
+
]
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
await Plotly.newPlot(chartDiv, data, layout, config);
|
|
92
|
+
chartInitialized = true;
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error('Failed to create polar area chart:', error);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function updateChart() {
|
|
99
|
+
if (!Plotly || !chartDiv || !chartInitialized) return;
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const data = createPolarAreaChartData(
|
|
103
|
+
selectedAntenna,
|
|
104
|
+
selectedAntenna2,
|
|
105
|
+
viewMode,
|
|
106
|
+
patternType === 'horizontal',
|
|
107
|
+
patternType === 'vertical',
|
|
108
|
+
ant1ElectricalTilt,
|
|
109
|
+
ant1MechanicalTilt,
|
|
110
|
+
ant2ElectricalTilt,
|
|
111
|
+
ant2MechanicalTilt,
|
|
112
|
+
patternDisplayMode
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const layout = createPolarAreaLayout(title);
|
|
116
|
+
await Plotly.react(chartDiv, data, layout);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
console.error('Failed to update polar area chart:', error);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
</script>
|
|
122
|
+
|
|
123
|
+
<div bind:this={chartDiv} style="height: 700px;"></div>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Antenna } from '../../types';
|
|
2
|
+
interface Props {
|
|
3
|
+
selectedAntenna?: Antenna | null;
|
|
4
|
+
selectedAntenna2?: Antenna | null;
|
|
5
|
+
viewMode?: 'single' | 'compare';
|
|
6
|
+
patternType?: 'horizontal' | 'vertical';
|
|
7
|
+
patternDisplayMode?: 'normalized' | 'gain-adjusted';
|
|
8
|
+
ant1ElectricalTilt?: number;
|
|
9
|
+
ant1MechanicalTilt?: number;
|
|
10
|
+
ant2ElectricalTilt?: number;
|
|
11
|
+
ant2MechanicalTilt?: number;
|
|
12
|
+
title?: string;
|
|
13
|
+
}
|
|
14
|
+
declare const PolarAreaChart: import("svelte").Component<Props, {}, "">;
|
|
15
|
+
type PolarAreaChart = ReturnType<typeof PolarAreaChart>;
|
|
16
|
+
export default PolarAreaChart;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<svelte:options runes={true} />
|
|
2
|
+
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
import { onMount } from 'svelte';
|
|
5
|
+
import { browser } from '$app/environment';
|
|
6
|
+
import type { Antenna } from '../../types';
|
|
7
|
+
import {
|
|
8
|
+
createPolarLineChartData,
|
|
9
|
+
createPolarLineLayout,
|
|
10
|
+
type PolarLineTraceData
|
|
11
|
+
} from '../../utils/chart-engines/polar-line-utils';
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
selectedAntenna?: Antenna | null;
|
|
15
|
+
selectedAntenna2?: Antenna | null;
|
|
16
|
+
viewMode?: 'single' | 'compare';
|
|
17
|
+
patternType?: 'horizontal' | 'vertical';
|
|
18
|
+
patternDisplayMode?: 'normalized' | 'gain-adjusted';
|
|
19
|
+
ant1ElectricalTilt?: number;
|
|
20
|
+
ant1MechanicalTilt?: number;
|
|
21
|
+
ant2ElectricalTilt?: number;
|
|
22
|
+
ant2MechanicalTilt?: number;
|
|
23
|
+
title?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let {
|
|
27
|
+
selectedAntenna = null,
|
|
28
|
+
selectedAntenna2 = null,
|
|
29
|
+
viewMode = 'single',
|
|
30
|
+
patternType = 'vertical',
|
|
31
|
+
patternDisplayMode = 'normalized',
|
|
32
|
+
ant1ElectricalTilt = 0,
|
|
33
|
+
ant1MechanicalTilt = 0,
|
|
34
|
+
ant2ElectricalTilt = 0,
|
|
35
|
+
ant2MechanicalTilt = 0,
|
|
36
|
+
title = 'Antenna Pattern - Polar Line Chart'
|
|
37
|
+
}: Props = $props();
|
|
38
|
+
|
|
39
|
+
let Plotly: any;
|
|
40
|
+
let chartDiv = $state<HTMLDivElement>();
|
|
41
|
+
let chartInitialized = $state(false);
|
|
42
|
+
|
|
43
|
+
// Initialize Plotly on mount
|
|
44
|
+
onMount(async () => {
|
|
45
|
+
if (browser) {
|
|
46
|
+
try {
|
|
47
|
+
Plotly = await import('plotly.js-dist-min');
|
|
48
|
+
await initializeChart();
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error('Failed to load Plotly:', error);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Update chart when data changes
|
|
56
|
+
$effect(() => {
|
|
57
|
+
if (chartInitialized) {
|
|
58
|
+
updateChart();
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
async function initializeChart() {
|
|
63
|
+
if (!Plotly || !chartDiv) return;
|
|
64
|
+
|
|
65
|
+
const data = createPolarLineChartData(
|
|
66
|
+
selectedAntenna,
|
|
67
|
+
selectedAntenna2,
|
|
68
|
+
viewMode,
|
|
69
|
+
patternType === 'horizontal',
|
|
70
|
+
patternType === 'vertical',
|
|
71
|
+
ant1ElectricalTilt,
|
|
72
|
+
ant1MechanicalTilt,
|
|
73
|
+
ant2ElectricalTilt,
|
|
74
|
+
ant2MechanicalTilt,
|
|
75
|
+
patternDisplayMode
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const layout = createPolarLineLayout(title);
|
|
79
|
+
const config = {
|
|
80
|
+
responsive: true,
|
|
81
|
+
displayModeBar: true,
|
|
82
|
+
displaylogo: false,
|
|
83
|
+
modeBarButtonsToRemove: [
|
|
84
|
+
'zoom2d', 'pan2d', 'select2d', 'lasso2d',
|
|
85
|
+
'zoomIn2d', 'zoomOut2d', 'autoScale2d',
|
|
86
|
+
'resetScale2d', 'toggleSpikelines', 'toImage'
|
|
87
|
+
]
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
await Plotly.newPlot(chartDiv, data, layout, config);
|
|
92
|
+
chartInitialized = true;
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error('Failed to create polar line chart:', error);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function updateChart() {
|
|
99
|
+
if (!Plotly || !chartDiv || !chartInitialized) return;
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const data = createPolarLineChartData(
|
|
103
|
+
selectedAntenna,
|
|
104
|
+
selectedAntenna2,
|
|
105
|
+
viewMode,
|
|
106
|
+
patternType === 'horizontal',
|
|
107
|
+
patternType === 'vertical',
|
|
108
|
+
ant1ElectricalTilt,
|
|
109
|
+
ant1MechanicalTilt,
|
|
110
|
+
ant2ElectricalTilt,
|
|
111
|
+
ant2MechanicalTilt,
|
|
112
|
+
patternDisplayMode
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const layout = createPolarLineLayout(title);
|
|
116
|
+
await Plotly.react(chartDiv, data, layout);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
console.error('Failed to update polar line chart:', error);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
</script>
|
|
122
|
+
|
|
123
|
+
<div bind:this={chartDiv} style="height: 700px;"></div>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Antenna } from '../../types';
|
|
2
|
+
interface Props {
|
|
3
|
+
selectedAntenna?: Antenna | null;
|
|
4
|
+
selectedAntenna2?: Antenna | null;
|
|
5
|
+
viewMode?: 'single' | 'compare';
|
|
6
|
+
patternType?: 'horizontal' | 'vertical';
|
|
7
|
+
patternDisplayMode?: 'normalized' | 'gain-adjusted';
|
|
8
|
+
ant1ElectricalTilt?: number;
|
|
9
|
+
ant1MechanicalTilt?: number;
|
|
10
|
+
ant2ElectricalTilt?: number;
|
|
11
|
+
ant2MechanicalTilt?: number;
|
|
12
|
+
title?: string;
|
|
13
|
+
}
|
|
14
|
+
declare const PolarLineChart: import("svelte").Component<Props, {}, "">;
|
|
15
|
+
type PolarLineChart = ReturnType<typeof PolarLineChart>;
|
|
16
|
+
export default PolarLineChart;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Antenna Tools - Chart Engine Components Index
|
|
3
|
+
*/
|
|
4
|
+
export { default as PolarLineChart } from './PolarLineChart.svelte';
|
|
5
|
+
export { default as PolarAreaChart } from './PolarAreaChart.svelte';
|
|
6
|
+
export type { PolarLineTraceData } from '../../utils/chart-engines/polar-line-utils';
|
|
7
|
+
export type { PolarAreaTraceData } from '../../utils/chart-engines/polar-area-utils';
|
|
8
|
+
export * from '../../utils/chart-engines/polar-line-utils';
|
|
9
|
+
export * from '../../utils/chart-engines/polar-area-utils';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Antenna Tools - Chart Engine Components Index
|
|
3
|
+
*/
|
|
4
|
+
// Chart Components
|
|
5
|
+
export { default as PolarLineChart } from './PolarLineChart.svelte';
|
|
6
|
+
export { default as PolarAreaChart } from './PolarAreaChart.svelte';
|
|
7
|
+
// Chart Utilities
|
|
8
|
+
export * from '../../utils/chart-engines/polar-line-utils';
|
|
9
|
+
export * from '../../utils/chart-engines/polar-area-utils';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { default as AntennaTools } from './AntennaTools.svelte';
|
|
2
|
+
export { default as AntennaControls } from './AntennaControls.svelte';
|
|
3
|
+
export { default as AntennaSettingsModal } from './AntennaSettingsModal.svelte';
|
|
4
|
+
export { default as DatabaseViewer } from './DatabaseViewer.svelte';
|
|
5
|
+
export { default as DbNotification } from './DbNotification.svelte';
|
|
6
|
+
export { default as JsonImporter } from './JsonImporter.svelte';
|
|
7
|
+
export { default as MSIConverter } from './MSIConverter.svelte';
|
|
8
|
+
export { PolarLineChart, PolarAreaChart } from './chart-engines/index';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Component exports for antenna-tools module
|
|
2
|
+
export { default as AntennaTools } from './AntennaTools.svelte';
|
|
3
|
+
export { default as AntennaControls } from './AntennaControls.svelte';
|
|
4
|
+
export { default as AntennaSettingsModal } from './AntennaSettingsModal.svelte';
|
|
5
|
+
export { default as DatabaseViewer } from './DatabaseViewer.svelte';
|
|
6
|
+
export { default as DbNotification } from './DbNotification.svelte';
|
|
7
|
+
export { default as JsonImporter } from './JsonImporter.svelte';
|
|
8
|
+
export { default as MSIConverter } from './MSIConverter.svelte';
|
|
9
|
+
// Chart engine exports
|
|
10
|
+
export { PolarLineChart, PolarAreaChart } from './chart-engines/index';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Antenna Tools - Database
|
|
3
|
+
* Dexie-based IndexedDB for antenna pattern storage
|
|
4
|
+
*/
|
|
5
|
+
import Dexie, { type Table } from 'dexie';
|
|
6
|
+
import type { Antenna } from './types';
|
|
7
|
+
declare class AntennaToolsDatabase extends Dexie {
|
|
8
|
+
antennas: Table<Antenna, number>;
|
|
9
|
+
constructor();
|
|
10
|
+
/**
|
|
11
|
+
* Check if database has any antenna data
|
|
12
|
+
*/
|
|
13
|
+
hasData(): Promise<boolean>;
|
|
14
|
+
/**
|
|
15
|
+
* Get total antenna count
|
|
16
|
+
*/
|
|
17
|
+
getCount(): Promise<number>;
|
|
18
|
+
/**
|
|
19
|
+
* Get unique antenna model names
|
|
20
|
+
*/
|
|
21
|
+
getUniqueAntennaNames(): Promise<string[]>;
|
|
22
|
+
/**
|
|
23
|
+
* Get unique frequency bands in database
|
|
24
|
+
*/
|
|
25
|
+
getUniqueBands(): Promise<number[]>;
|
|
26
|
+
}
|
|
27
|
+
export declare const db: AntennaToolsDatabase;
|
|
28
|
+
export type { Antenna } from './types';
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Antenna Tools - Database
|
|
3
|
+
* Dexie-based IndexedDB for antenna pattern storage
|
|
4
|
+
*/
|
|
5
|
+
import Dexie, {} from 'dexie';
|
|
6
|
+
class AntennaToolsDatabase extends Dexie {
|
|
7
|
+
antennas;
|
|
8
|
+
constructor() {
|
|
9
|
+
super('antennaToolsDB');
|
|
10
|
+
// Version 1: Initial schema with band-based frequency
|
|
11
|
+
this.version(1).stores({
|
|
12
|
+
antennas: '++id, name, frequency, originalFrequency, tilt'
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Check if database has any antenna data
|
|
17
|
+
*/
|
|
18
|
+
async hasData() {
|
|
19
|
+
const count = await this.antennas.count();
|
|
20
|
+
return count > 0;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Get total antenna count
|
|
24
|
+
*/
|
|
25
|
+
async getCount() {
|
|
26
|
+
return await this.antennas.count();
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Get unique antenna model names
|
|
30
|
+
*/
|
|
31
|
+
async getUniqueAntennaNames() {
|
|
32
|
+
const antennas = await this.antennas.toArray();
|
|
33
|
+
const names = new Set(antennas.map(a => a.name));
|
|
34
|
+
return Array.from(names).sort();
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Get unique frequency bands in database
|
|
38
|
+
*/
|
|
39
|
+
async getUniqueBands() {
|
|
40
|
+
const antennas = await this.antennas.toArray();
|
|
41
|
+
const bands = new Set(antennas.map(a => a.frequency));
|
|
42
|
+
return Array.from(bands).sort((a, b) => a - b);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export const db = new AntennaToolsDatabase();
|