kolaslab commited on
Commit
1e56734
Β·
verified Β·
1 Parent(s): 3cfdd58

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +285 -358
index.html CHANGED
@@ -2,11 +2,9 @@
2
  <html>
3
  <head>
4
  <meta charset="UTF-8">
5
- <title>Global SDR Network Monitor</title>
6
- <!-- Leaflet CSS -->
7
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css" />
8
  <style>
9
- /* 곡톡 μŠ€νƒ€μΌ */
10
  body {
11
  margin: 0;
12
  padding: 20px;
@@ -26,15 +24,14 @@
26
  border-radius: 8px;
27
  height: calc(100vh - 40px);
28
  overflow-y: auto;
29
- z-index: 1000;
30
  }
31
  #map {
32
- height: calc(100vh - 40px);
33
- border-radius: 8px;
34
  background: #111;
 
 
35
  }
36
 
37
- /* μˆ˜μ‹ κΈ°(Receiver) λͺ©λ‘ μŠ€νƒ€μΌ */
38
  .receiver {
39
  margin: 10px 0;
40
  padding: 10px;
@@ -79,7 +76,7 @@
79
  transition: width 0.3s;
80
  }
81
 
82
- /* 탐지(Detection) λͺ©λ‘ μŠ€νƒ€μΌ */
83
  .detection {
84
  padding: 5px;
85
  margin: 5px 0;
@@ -87,29 +84,13 @@
87
  border-left: 2px solid #0f0;
88
  }
89
 
90
- /* Leaflet 지도 λ°€(닀크) ν…Œλ§ˆ 일뢀 보정 */
91
- .leaflet-tile-pane {
92
- filter: invert(1) hue-rotate(180deg);
93
- }
94
- .leaflet-container {
95
- background: #111 !important;
96
- }
97
- .leaflet-control-attribution {
98
- background: #222 !important;
99
- color: #666 !important;
100
- }
101
- .leaflet-popup-content-wrapper,
102
- .leaflet-popup-tip {
103
- background: #222 !important;
104
- color: #0f0 !important;
105
- }
106
-
107
- /* μŠ€ν…Œμ΄μ…˜ λ²”μœ„ 원 ν‘œμ‹œ */
108
- .station-range {
109
- stroke: #0f0;
110
- stroke-width: 1;
111
- fill: #0f0;
112
- fill-opacity: 0.1;
113
  }
114
  </style>
115
  </head>
@@ -117,23 +98,25 @@
117
  <div class="container">
118
  <!-- μ‚¬μ΄λ“œλ°” -->
119
  <div class="sidebar">
120
- <h3>Active SDR Receivers</h3>
 
 
121
  <div id="receivers"></div>
122
 
123
  <h3>Real-time Detections</h3>
124
  <div id="detections"></div>
 
 
 
125
  </div>
126
 
127
- <!-- Leaflet 지도 μ˜μ—­ -->
128
- <div id="map"></div>
129
  </div>
130
 
131
- <!-- Leaflet JS -->
132
- <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js"></script>
133
  <script>
134
- // μ „ 세계 SDR μŠ€ν…Œμ΄μ…˜ 데이터 (첫 번째 μ½”λ“œ + 일뢀 μ˜ˆμ‹œ 병합)
135
  const sdrStations = [
136
- // Europe
137
  {
138
  name: "Twente WebSDR",
139
  url: "websdr.ewi.utwente.nl:8901",
@@ -142,22 +125,6 @@
142
  range: 200,
143
  active: true
144
  },
145
- {
146
- name: "TU Delft WebSDR",
147
- url: "websdr.tudelft.nl:8901",
148
- location: [51.9981, 4.3731],
149
- frequency: "0-29.160 MHz",
150
- range: 180,
151
- active: true
152
- },
153
- {
154
- name: "SUWS WebSDR UK",
155
- url: "websdr.suws.org.uk",
156
- location: [51.2785, -0.7642],
157
- frequency: "0-30 MHz",
158
- range: 150,
159
- active: true
160
- },
161
  {
162
  name: "KiwiSDR Switzerland",
163
  url: "hb9ryz.no-ip.org:8073",
@@ -166,235 +133,64 @@
166
  range: 160,
167
  active: true
168
  },
169
- // United States
170
- {
171
- name: "W6DRZ WebSDR",
172
- url: "w6drz.sdr.us:8901",
173
- location: [34.2847, -118.4429],
174
- frequency: "0-30 MHz",
175
- range: 170,
176
- active: true
177
- },
178
- {
179
- name: "K3FEF WebSDR",
180
- url: "k3fef.sdr.us:8901",
181
- location: [40.5697, -75.9363],
182
- frequency: "0-30 MHz",
183
- range: 160,
184
- active: true
185
- },
186
  {
187
- name: "WA2ZKD KiwiSDR",
188
- url: "wa2zkd.sdr.us:8073",
189
- location: [40.7128, -74.0060],
190
- frequency: "0-30 MHz",
191
- range: 150,
192
- active: true
193
- },
194
- {
195
- name: "W4AX WebSDR",
196
- url: "w4ax.sdr.us:8901",
197
- location: [33.7756, -84.3963],
198
- frequency: "0-30 MHz",
199
- range: 165,
200
- active: true
201
- },
202
- // Japan
203
- {
204
- name: "JH7VHZ WebSDR",
205
- url: "jh7vhz.sdr.jp:8901",
206
- location: [38.2682, 140.8694],
207
- frequency: "0-30 MHz",
208
- range: 155,
209
- active: true
210
- },
211
- {
212
- name: "JA1GJB KiwiSDR",
213
- url: "ja1gjb.sdr.jp:8073",
214
- location: [35.6762, 139.6503],
215
- frequency: "0-30 MHz",
216
- range: 145,
217
- active: true
218
- },
219
- {
220
- name: "JA3ZOH WebSDR",
221
- url: "ja3zoh.sdr.jp:8901",
222
- location: [34.6937, 135.5023],
223
- frequency: "0-30 MHz",
224
- range: 150,
225
- active: true
226
- },
227
- // Australia
228
- {
229
- name: "VK4YA KiwiSDR",
230
- url: "vk4ya.sdr.au:8073",
231
- location: [-27.4698, 153.0251],
232
- frequency: "0-30 MHz",
233
- range: 170,
234
- active: true
235
- },
236
- {
237
- name: "VK2RG WebSDR",
238
- url: "vk2rg.sdr.au:8901",
239
- location: [-33.8688, 151.2093],
240
- frequency: "0-30 MHz",
241
- range: 165,
242
- active: true
243
- },
244
- // Russia
245
- {
246
- name: "RZ3DJR WebSDR",
247
- url: "rz3djr.sdr.ru:8901",
248
- location: [55.7558, 37.6173],
249
- frequency: "0-30 MHz",
250
- range: 180,
251
- active: true
252
- },
253
- {
254
- name: "UA9UDX WebSDR",
255
- url: "ua9udx.sdr.ru:8901",
256
- location: [55.0084, 82.9357],
257
- frequency: "0-30 MHz",
258
- range: 175,
259
- active: true
260
- },
261
- // China
262
- {
263
- name: "BY1PK WebSDR",
264
- url: "by1pk.sdr.cn:8901",
265
- location: [39.9042, 116.4074],
266
- frequency: "0-30 MHz",
267
- range: 160,
268
- active: true
269
- },
270
- {
271
- name: "BG3MDO KiwiSDR",
272
- url: "bg3mdo.sdr.cn:8073",
273
- location: [23.1291, 113.2644],
274
- frequency: "0-30 MHz",
275
- range: 155,
276
- active: true
277
- },
278
- // South Korea
279
- {
280
- name: "HL2WA KiwiSDR",
281
- url: "hl2wa.sdr.kr:8073",
282
- location: [37.5665, 126.9780],
283
  frequency: "0-30 MHz",
284
  range: 150,
285
  active: true
286
- },
287
- {
288
- name: "DS1URB WebSDR",
289
- url: "ds1urb.sdr.kr:8901",
290
- location: [35.1796, 129.0756],
291
- frequency: "0-30 MHz",
292
- range: 145,
293
- active: true
294
- },
295
- // Canada
296
- {
297
- name: "VE3HOA WebSDR",
298
- url: "ve3hoa.sdr.ca:8901",
299
- location: [43.6532, -79.3832],
300
- frequency: "0-30 MHz",
301
- range: 165,
302
- active: true
303
- },
304
- {
305
- name: "VA3ROM KiwiSDR",
306
- url: "va3rom.sdr.ca:8073",
307
- location: [45.4215, -75.6972],
308
- frequency: "0-30 MHz",
309
- range: 160,
310
- active: true
311
- },
312
- // Brazil
313
- {
314
- name: "PY2RDZ WebSDR",
315
- url: "py2rdz.sdr.br:8901",
316
- location: [-23.5505, -46.6333],
317
- frequency: "0-30 MHz",
318
- range: 170,
319
- active: true
320
- },
321
- {
322
- name: "PY1ZV KiwiSDR",
323
- url: "py1zv.sdr.br:8073",
324
- location: [-22.9068, -43.1729],
325
- frequency: "0-30 MHz",
326
- range: 165,
327
- active: true
328
  }
329
  ];
330
 
331
- // Leaflet + 동적 νƒ€κ²Ÿ 좔적을 ν†΅ν•©ν•œ RadarSystem
332
  class RadarSystem {
333
  constructor() {
334
- // νƒ€κ²Ÿ(ν‘œμ ) 정보λ₯Ό μ €μž₯ν•  Set
 
 
 
 
335
  this.targets = new Set();
336
- // νƒ€κ²Ÿ λ§ˆμ»€μ™€ μ‹ ν˜Έ 라인을 μ €μž₯ν•  자료ꡬ쑰
337
- this.markers = new Map();
338
- this.signalLines = new Map();
339
 
340
- this.initializeMap();
 
 
 
 
 
 
 
 
 
 
 
341
  this.renderReceivers();
342
  this.startTracking();
343
  }
344
 
345
- // Leaflet 지도 μ΄ˆκΈ°ν™”
346
- initializeMap() {
347
- this.map = L.map('map', {
348
- center: [20, 0],
349
- zoom: 3,
350
- preferCanvas: true,
351
- worldCopyJump: true
352
- });
353
-
354
- // OpenStreetMap 타일 λ ˆμ΄μ–΄
355
- L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
356
- maxZoom: 19,
357
- attribution: 'Β© OpenStreetMap contributors'
358
- }).addTo(this.map);
359
-
360
- // 각 SDR μŠ€ν…Œμ΄μ…˜μ„ 지도에 ν‘œμ‹œ
361
- sdrStations.forEach(station => {
362
- // μŠ€ν…Œμ΄μ…˜ μœ„μΉ˜ 마컀
363
- const marker = L.circleMarker(station.location, {
364
- radius: 5,
365
- color: '#0f0',
366
- fillColor: '#0f0',
367
- fillOpacity: 1
368
- }).addTo(this.map);
369
-
370
- // μŠ€ν…Œμ΄μ…˜μ˜ κ°€μ²­ λ²”μœ„(coverage range)λ₯Ό μ›μœΌλ‘œ ν‘œμ‹œ
371
- L.circle(station.location, {
372
- radius: station.range * 1000, // km -> m
373
- className: 'station-range'
374
- }).addTo(this.map);
375
-
376
- // 마컀 툴팁(마우슀 μ˜€λ²„ μ‹œ 정보)
377
- marker.bindTooltip(`
378
- ${station.name}<br>
379
- Frequency: ${station.frequency}<br>
380
- Range: ${station.range}km
381
- `);
382
  });
383
  }
384
 
385
- // μ‚¬μ΄λ“œλ°”μ— μˆ˜μ‹ κΈ°(Receivers) λͺ©λ‘ λ Œλ”λ§
386
  renderReceivers() {
387
  const container = document.getElementById('receivers');
388
- container.innerHTML = sdrStations.map(station => `
389
- <div class="receiver" id="rx-${station.url.split(':')[0]}">
390
  <div class="status">
391
- <div class="led ${station.active ? 'active' : 'inactive'}"></div>
392
- <strong>${station.name}</strong>
393
  </div>
394
- <div>πŸ“‘ ${station.url}</div>
395
- <div>πŸ“» ${station.frequency}</div>
396
- <div>πŸ“ ${station.location.join(', ')}</div>
397
- <div>Range: ${station.range}km</div>
398
  <div class="signal-strength">
399
  <div class="signal-bar"></div>
400
  </div>
@@ -402,145 +198,276 @@
402
  `).join('');
403
  }
404
 
405
- // 랜덀 ν‘œμ (νƒ€κ²Ÿ)을 ν•˜λ‚˜ 생성
406
- // (두 번째 μ½”λ“œμ˜ 아이디어와, 첫 번째 μ½”λ“œμ˜ 'μŠ€ν…Œμ΄μ…˜ 근처' 방식을 κ²°ν•©)
407
  generateTarget() {
408
- // μž„μ˜ μŠ€ν…Œμ΄μ…˜ ν•˜λ‚˜ 선택
409
- const station = sdrStations[Math.floor(Math.random() * sdrStations.length)];
410
- // station κ·Όμ²˜μ— λ‚˜νƒ€λ‚˜λ„λ‘ μ‘°μ • (Β±5도 λ²”μœ„)
411
- const range = 5;
412
  return {
413
- type: Math.random() > 0.7 ? 'aircraft' : 'vehicle',
414
- position: {
415
- lat: station.location[0] + (Math.random() - 0.5) * range,
416
- lon: station.location[1] + (Math.random() - 0.5) * range
417
- },
418
- speed: Math.random() * 500 + 200, // kts
419
- altitude: Math.random() * 35000 + 5000, // ft
420
- heading: Math.random() * 360,
421
  id: Math.random().toString(36).substr(2, 6).toUpperCase(),
 
 
 
 
 
 
422
  signalStrength: Math.random()
423
  };
424
  }
425
 
426
- // ν˜„μž¬ νƒ€κ²Ÿλ“€μ„ 지도에 ν‘œμ‹œ / μ—…λ°μ΄νŠΈ
427
- updateTargets() {
428
- // 이전 ν”„λ ˆμž„μ—μ„œ 그렀진 μ‹ ν˜Έ 라인 제거
429
- this.signalLines.forEach(line => this.map.removeLayer(line));
430
- this.signalLines.clear();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
431
 
432
- // 이전 λ§ˆμ»€λ“€ 제거
433
- this.markers.forEach(marker => this.map.removeLayer(marker));
434
- this.markers.clear();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
435
 
436
- // ν˜„μž¬ νƒ€κ²Ÿλ§ˆλ‹€ μƒˆλ‘œ 지도에 ν‘œμ‹œ
 
437
  this.targets.forEach(target => {
438
- // νƒ€κ²Ÿ 마컀
439
- const marker = L.circleMarker([target.position.lat, target.position.lon], {
440
- radius: 3,
441
- color: target.type === 'aircraft' ? '#ff0' : '#0ff',
442
- fillColor: target.type === 'aircraft' ? '#ff0' : '#0ff',
443
- fillOpacity: 1
444
- }).addTo(this.map);
445
-
446
- // 마우슀 μ˜€λ²„ μ‹œ 툴팁
447
- marker.bindTooltip(`
448
- ${target.id}<br>
449
- Type: ${target.type}<br>
450
- Speed: ${target.speed.toFixed(0)}kts<br>
451
- ${
452
- target.type === 'aircraft'
453
- ? `Altitude: ${target.altitude.toFixed(0)}ft<br>`
454
- : ''
455
  }
456
- Signal: ${(target.signalStrength * 100).toFixed(0)}%
457
- `);
458
-
459
- // λ ˆμ΄μ–΄ κ΄€λ¦¬μš© μ €μž₯
460
- this.markers.set(target.id, marker);
461
-
462
- // νƒ€κ²Ÿκ³Ό κ°€κΉŒμš΄(λ²”μœ„ μ•ˆ) μŠ€ν…Œμ΄μ…˜κ³Όμ˜ μ‹ ν˜Έ 라인 ν‘œμ‹œ
463
- sdrStations.forEach(station => {
464
- if (station.active) {
465
- const distance = this.map.distance(
466
- [target.position.lat, target.position.lon],
467
- station.location
468
- ) / 1000; // m -> km
469
-
470
- if (distance <= station.range) {
471
- // μŠ€ν…Œμ΄μ…˜κ³Ό νƒ€κ²Ÿ 사이 라인
472
- const line = L.polyline([
473
- [target.position.lat, target.position.lon],
474
- station.location
475
- ], {
476
- color: '#0f0',
477
- opacity: target.signalStrength * 0.3,
478
- weight: 1
479
- }).addTo(this.map);
480
-
481
- // 라인도 Map에 μ €μž₯해두고, λ‹€μŒ μ—…λ°μ΄νŠΈ λ•Œ 제거
482
- this.signalLines.set(`${target.id}-${station.name}`, line);
 
 
483
  }
484
  }
485
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
486
  });
487
  }
488
 
489
  // μ‚¬μ΄λ“œλ°” 'Real-time Detections' μ—…λ°μ΄νŠΈ
490
  updateDetections() {
491
  const detections = document.getElementById('detections');
492
- detections.innerHTML = Array.from(this.targets)
493
- .map(target => `
 
494
  <div class="detection">
495
- ${target.type === 'aircraft' ? '✈️' : 'πŸš—'}
496
- ${target.id}
497
- ${target.speed.toFixed(0)}kts
498
- ${
499
- target.type === 'aircraft'
500
- ? `${target.altitude.toFixed(0)}ft `
501
- : ''
502
- }
503
- Signal: ${(target.signalStrength * 100).toFixed(0)}%
504
  </div>
505
- `).join('');
 
 
506
  }
507
 
508
- // μˆ˜μ‹ κΈ°(Receivers) μ‹ ν˜Έ 강도 λ°”(Bar) 동적 μ—…λ°μ΄νŠΈ
509
  updateSignalStrengths() {
510
- sdrStations.forEach(station => {
511
- const bar = document.querySelector(`#rx-${station.url.split(':')[0]} .signal-bar`);
512
  if (bar) {
513
- // 40% ~ 100% μ‚¬μ΄μ˜ λžœλ€κ°’
514
- const strength = 40 + Math.random() * 60;
515
  bar.style.width = `${strength}%`;
516
  }
517
  });
518
  }
519
 
520
- // 일정 주기둜 νƒ€κ²Ÿ μΆ”κ°€/제거 & 지도·UI μ—…λ°μ΄νŠΈ
521
  startTracking() {
 
522
  setInterval(() => {
523
- // (μ•½ 10% ν™•λ₯ ) νƒ€κ²Ÿ ν•˜λ‚˜ μΆ”κ°€, μ΅œλŒ€ 20κ°œκΉŒμ§€
524
- if (Math.random() < 0.1 && this.targets.size < 20) {
525
- this.targets.add(this.generateTarget());
526
  }
527
- // (μ•½ 10% ν™•λ₯ ) μ‘΄μž¬ν•˜λŠ” νƒ€κ²Ÿ ν•˜λ‚˜ 제거
 
 
 
 
 
 
 
 
 
 
528
  if (Math.random() < 0.1 && this.targets.size > 0) {
529
- // Set을 Array둜 λ§Œλ“€μ–΄ 첫 번째 μš”μ†Œ 제거
530
- this.targets.delete(Array.from(this.targets)[0]);
 
531
  }
532
 
533
- // 지도 μœ„ ν‘œμ  및 UI κ°±μ‹ 
534
- this.updateTargets();
 
 
 
 
535
  this.updateDetections();
536
  this.updateSignalStrengths();
537
- }, 100); // 0.1μ΄ˆλ§ˆλ‹€ κ°±μ‹ 
538
  }
539
  }
540
 
541
- // νŽ˜μ΄μ§€ λ‘œλ“œ μ™„λ£Œ ν›„ λ ˆμ΄λ” μ‹œμŠ€ν…œ μ΄ˆκΈ°ν™”
542
  window.addEventListener('load', () => {
543
- const radar = new RadarSystem();
544
  });
545
  </script>
546
  </body>
 
2
  <html>
3
  <head>
4
  <meta charset="UTF-8">
5
+ <title>Hyperscan: Global SDR Radar(Simul)</title>
 
 
6
  <style>
7
+ /* 전체 λ°°κ²½ 및 κΈ°λ³Έ μŠ€νƒ€μΌ */
8
  body {
9
  margin: 0;
10
  padding: 20px;
 
24
  border-radius: 8px;
25
  height: calc(100vh - 40px);
26
  overflow-y: auto;
 
27
  }
28
  #map {
 
 
29
  background: #111;
30
+ border-radius: 8px;
31
+ height: calc(100vh - 40px);
32
  }
33
 
34
+ /* μˆ˜μ‹ κΈ°(Receivers) λͺ©λ‘ μ˜μ—­ */
35
  .receiver {
36
  margin: 10px 0;
37
  padding: 10px;
 
76
  transition: width 0.3s;
77
  }
78
 
79
+ /* 탐지(Detections) λͺ©λ‘ */
80
  .detection {
81
  padding: 5px;
82
  margin: 5px 0;
 
84
  border-left: 2px solid #0f0;
85
  }
86
 
87
+ /* 이벀트 둜그 좜λ ₯ */
88
+ .alert {
89
+ background: #911;
90
+ padding: 5px;
91
+ margin: 5px 0;
92
+ border-left: 2px solid #f00;
93
+ color: #f66;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  }
95
  </style>
96
  </head>
 
98
  <div class="container">
99
  <!-- μ‚¬μ΄λ“œλ°” -->
100
  <div class="sidebar">
101
+ <h2>Hyperscan: Global SDR Radar(Simul)</h2>
102
+
103
+ <h3>SDR Receivers</h3>
104
  <div id="receivers"></div>
105
 
106
  <h3>Real-time Detections</h3>
107
  <div id="detections"></div>
108
+
109
+ <h3>Events</h3>
110
+ <div id="events"></div>
111
  </div>
112
 
113
+ <!-- μΊ”λ²„μŠ€ 지도 μ˜μ—­ -->
114
+ <canvas id="map"></canvas>
115
  </div>
116
 
 
 
117
  <script>
118
+ // μ˜ˆμ‹œ SDR μŠ€ν…Œμ΄μ…˜: μ›ν•˜λŠ” 만큼 μΆ”κ°€/μˆ˜μ • κ°€λŠ₯
119
  const sdrStations = [
 
120
  {
121
  name: "Twente WebSDR",
122
  url: "websdr.ewi.utwente.nl:8901",
 
125
  range: 200,
126
  active: true
127
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  {
129
  name: "KiwiSDR Switzerland",
130
  url: "hb9ryz.no-ip.org:8073",
 
133
  range: 160,
134
  active: true
135
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  {
137
+ name: "SUWS WebSDR UK",
138
+ url: "websdr.suws.org.uk",
139
+ location: [51.2785, -0.7642],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  frequency: "0-30 MHz",
141
  range: 150,
142
  active: true
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  }
144
  ];
145
 
 
146
  class RadarSystem {
147
  constructor() {
148
+ // Canvas μ€€λΉ„
149
+ this.canvas = document.getElementById('map');
150
+ this.ctx = this.canvas.getContext('2d');
151
+
152
+ // νƒ€κ²Ÿ 정보 μ €μž₯ (Set으둜 관리)
153
  this.targets = new Set();
 
 
 
154
 
155
+ // νƒ€κ²Ÿμ˜ 이동 ꢀ적(trail)을 μ €μž₯ (key: νƒ€κ²ŸID, value: {x,y} λ°°μ—΄)
156
+ this.trails = new Map();
157
+
158
+ // 이벀트 둜그
159
+ this.eventsLog = [];
160
+
161
+ // 폭풍/κ΅λž€ 이벀트 μƒνƒœ
162
+ this.stormActive = false; // 폭풍 ν† κΈ€
163
+ this.stormCenter = { lat: 50.5, lon: 5.0 }; // 폭풍 쀑심
164
+ this.stormRadius = 200; // 폭풍 반경 (km)
165
+
166
+ this.setupCanvas();
167
  this.renderReceivers();
168
  this.startTracking();
169
  }
170
 
171
+ // μΊ”λ²„μŠ€ 크기 λ§žμΆ”κΈ°
172
+ setupCanvas() {
173
+ this.canvas.width = this.canvas.offsetWidth;
174
+ this.canvas.height = this.canvas.offsetHeight;
175
+ window.addEventListener('resize', () => {
176
+ this.canvas.width = this.canvas.offsetWidth;
177
+ this.canvas.height = this.canvas.offsetHeight;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  });
179
  }
180
 
181
+ // μ‚¬μ΄λ“œλ°”μ— μˆ˜μ‹ κΈ° 리슀트 λ Œλ”λ§
182
  renderReceivers() {
183
  const container = document.getElementById('receivers');
184
+ container.innerHTML = sdrStations.map(st => `
185
+ <div class="receiver" id="rx-${st.url.split(':')[0]}">
186
  <div class="status">
187
+ <div class="led ${st.active ? 'active' : 'inactive'}"></div>
188
+ <strong>${st.name}</strong>
189
  </div>
190
+ <div>πŸ“‘ ${st.url}</div>
191
+ <div>πŸ“» ${st.frequency}</div>
192
+ <div>πŸ“ ${st.location.join(', ')}</div>
193
+ <div>Range: ${st.range}km</div>
194
  <div class="signal-strength">
195
  <div class="signal-bar"></div>
196
  </div>
 
198
  `).join('');
199
  }
200
 
201
+ // μž„μ˜λ‘œ νƒ€κ²Ÿ ν•˜λ‚˜ 생성
 
202
  generateTarget() {
203
+ const lat = 51.5 + (Math.random()-0.5)*4; // μž„μ˜ λ²”μœ„
204
+ const lon = 5.0 + (Math.random()-0.5)*8;
 
 
205
  return {
 
 
 
 
 
 
 
 
206
  id: Math.random().toString(36).substr(2, 6).toUpperCase(),
207
+ type: Math.random() > 0.7 ? 'aircraft' : 'vehicle', // 30%ν™•λ₯  aircraft
208
+ lat,
209
+ lon,
210
+ speed: (Math.random()*200 + 100).toFixed(0),
211
+ altitude: (Math.random()*30000 + 1000).toFixed(0),
212
+ heading: Math.random()*360,
213
  signalStrength: Math.random()
214
  };
215
  }
216
 
217
+ // νƒ€κ²Ÿ 이동(heading, speed 기반)
218
+ moveTarget(target) {
219
+ const speedKnots = parseFloat(target.speed);
220
+ // 1 knot μ•½ 0.0005 deg/sec κ°€μ • (λ‹¨μˆœν™”)
221
+ const speedFactor = 0.00005;
222
+ const rad = (target.heading * Math.PI) / 180;
223
+ // 뢁(μœ„λ„+), 남(μœ„λ„-), 동(경도+), μ„œ(경도-)
224
+ target.lat += Math.cos(rad) * speedKnots * speedFactor;
225
+ target.lon += Math.sin(rad) * speedKnots * speedFactor;
226
+ }
227
+
228
+ // 폭풍 On/Off
229
+ toggleStorm() {
230
+ this.stormActive = !this.stormActive;
231
+ const msg = this.stormActive
232
+ ? "폭풍 λ°œμƒ! μˆ˜μ‹  κ΅λž€ 우렀"
233
+ : "폭풍 μ†Œλ©Έ. μƒνƒœ 정상화";
234
+ this.addEventLog(msg);
235
+ }
236
+
237
+ // 둜그 μΆ”κ°€
238
+ addEventLog(msg) {
239
+ this.eventsLog.push(msg);
240
+ const eventsDiv = document.getElementById('events');
241
+ // μΆ”κ°€
242
+ eventsDiv.innerHTML += `<div class="alert">${msg}</div>`;
243
+ // λ„ˆλ¬΄ λ§Žμ•„μ§€λ©΄ 였래된 기둝 제거
244
+ if (this.eventsLog.length > 10) {
245
+ this.eventsLog.shift();
246
+ eventsDiv.removeChild(eventsDiv.firstChild);
247
+ }
248
+ }
249
+
250
+ // μœ„λ„κ²½λ„λ₯Ό μΊ”λ²„μŠ€ μ’Œν‘œλ‘œ λ³€ν™˜ (λ‹¨μˆœλ„)
251
+ latLongToXY(lat, lon) {
252
+ const centerLat = 51.5;
253
+ const centerLon = 5.0;
254
+ const scale = 100;
255
+ const x = (lon - centerLon) * scale + this.canvas.width / 2;
256
+ const y = (centerLat - lat) * scale + this.canvas.height / 2;
257
+ return { x, y };
258
+ }
259
+
260
+ // 두 점(μœ„λ„κ²½λ„) 사이 거리(km)
261
+ distanceKm(lat1, lon1, lat2, lon2) {
262
+ const R = 6371;
263
+ const dLat = (lat2 - lat1) * Math.PI/180;
264
+ const dLon = (lon2 - lon1) * Math.PI/180;
265
+ const a = Math.sin(dLat/2)*Math.sin(dLat/2)
266
+ + Math.cos(lat1*Math.PI/180)*Math.cos(lat2*Math.PI/180)
267
+ * Math.sin(dLon/2)*Math.sin(dLon/2);
268
+ return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
269
+ }
270
+
271
+ // λ°°κ²½+κ·Έλ¦¬λ“œ 그리기
272
+ drawBackground() {
273
+ this.ctx.fillStyle = '#111';
274
+ this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
275
+
276
+ this.ctx.strokeStyle = '#1a1a1a';
277
+ this.ctx.lineWidth = 1;
278
+ // μ„Έλ‘œ κ·Έλ¦¬λ“œ
279
+ for (let i=0; i<this.canvas.width; i+=50) {
280
+ this.ctx.beginPath();
281
+ this.ctx.moveTo(i, 0);
282
+ this.ctx.lineTo(i, this.canvas.height);
283
+ this.ctx.stroke();
284
+ }
285
+ // κ°€λ‘œ κ·Έλ¦¬λ“œ
286
+ for (let i=0; i<this.canvas.height; i+=50) {
287
+ this.ctx.beginPath();
288
+ this.ctx.moveTo(0, i);
289
+ this.ctx.lineTo(this.canvas.width, i);
290
+ this.ctx.stroke();
291
+ }
292
+ }
293
 
294
+ // μŠ€ν…Œμ΄μ…˜ + 폭풍 ν‘œμ‹œ
295
+ drawStations() {
296
+ // 폭풍 λ²”μœ„ μ‹œκ°ν™”
297
+ if (this.stormActive) {
298
+ const sc = this.latLongToXY(this.stormCenter.lat, this.stormCenter.lon);
299
+ this.ctx.beginPath();
300
+ this.ctx.arc(sc.x, sc.y, this.stormRadius, 0, Math.PI*2);
301
+ this.ctx.fillStyle = 'rgba(255,0,0,0.1)';
302
+ this.ctx.fill();
303
+ this.ctx.strokeStyle = 'rgba(255,0,0,0.5)';
304
+ this.ctx.stroke();
305
+ }
306
+
307
+ // μˆ˜μ‹ κΈ° μŠ€ν…Œμ΄μ…˜
308
+ sdrStations.forEach(st => {
309
+ const pos = this.latLongToXY(st.location[0], st.location[1]);
310
+ // λ²”μœ„ 원
311
+ this.ctx.beginPath();
312
+ this.ctx.arc(pos.x, pos.y, st.range, 0, Math.PI*2);
313
+ this.ctx.strokeStyle = st.active
314
+ ? 'rgba(0,255,0,0.2)'
315
+ : 'rgba(255,0,0,0.2)';
316
+ this.ctx.stroke();
317
+
318
+ // 쀑심 점
319
+ this.ctx.beginPath();
320
+ this.ctx.arc(pos.x, pos.y, 4, 0, Math.PI*2);
321
+ this.ctx.fillStyle = st.active ? '#0f0' : '#f00';
322
+ this.ctx.fill();
323
+
324
+ // 라벨
325
+ this.ctx.fillStyle = '#0f0';
326
+ this.ctx.font = '10px monospace';
327
+ this.ctx.fillText(st.name, pos.x+8, pos.y+4);
328
+ });
329
+ }
330
 
331
+ // νƒ€κ²Ÿ 및 ꢀ적 그리기
332
+ drawTargets() {
333
  this.targets.forEach(target => {
334
+ // νƒ€κ²Ÿ 이동
335
+ this.moveTarget(target);
336
+
337
+ // 폭풍 영ν–₯: λ²”μœ„ 내에 있으면 μ‹ ν˜Έκ°•λ„ κ°μ†Œ
338
+ if (this.stormActive) {
339
+ const distStorm = this.distanceKm(
340
+ target.lat, target.lon,
341
+ this.stormCenter.lat, this.stormCenter.lon
342
+ );
343
+ if (distStorm <= this.stormRadius) {
344
+ target.signalStrength = Math.max(0, target.signalStrength - 0.01);
 
 
 
 
 
 
345
  }
346
+ }
347
+
348
+ // νƒ€κ²Ÿ μœ„μΉ˜
349
+ const pos = this.latLongToXY(target.lat, target.lon);
350
+
351
+ // ꢀ적 μ €μž₯
352
+ if (!this.trails.has(target.id)) {
353
+ this.trails.set(target.id, []);
354
+ }
355
+ const trail = this.trails.get(target.id);
356
+ trail.push({x: pos.x, y: pos.y});
357
+ if (trail.length > 100) {
358
+ trail.shift(); // 였래된 μ’Œν‘œ 제거
359
+ }
360
+
361
+ // μŠ€ν…Œμ΄μ…˜ μ—°κ²°μ„ 
362
+ sdrStations.forEach(st => {
363
+ if (st.active) {
364
+ const dist = this.distanceKm(
365
+ target.lat, target.lon,
366
+ st.location[0], st.location[1]
367
+ );
368
+ if (dist <= st.range) {
369
+ const sp = this.latLongToXY(st.location[0], st.location[1]);
370
+ this.ctx.beginPath();
371
+ this.ctx.moveTo(sp.x, sp.y);
372
+ this.ctx.lineTo(pos.x, pos.y);
373
+ this.ctx.strokeStyle = `rgba(0,255,0,${target.signalStrength*0.3})`;
374
+ this.ctx.stroke();
375
  }
376
  }
377
  });
378
+
379
+ // νƒ€κ²Ÿ ꢀ적
380
+ this.ctx.beginPath();
381
+ this.ctx.strokeStyle = (target.type==='aircraft')
382
+ ? 'rgba(255,255,0,0.3)'
383
+ : 'rgba(0,255,255,0.3)';
384
+ for (let i=0; i<trail.length-1; i++) {
385
+ this.ctx.moveTo(trail[i].x, trail[i].y);
386
+ this.ctx.lineTo(trail[i+1].x, trail[i+1].y);
387
+ }
388
+ this.ctx.stroke();
389
+
390
+ // ν˜„μž¬ νƒ€κ²Ÿ 점
391
+ this.ctx.beginPath();
392
+ this.ctx.arc(pos.x, pos.y, 3, 0, Math.PI*2);
393
+ this.ctx.fillStyle = (target.type==='aircraft') ? '#ff0' : '#0ff';
394
+ this.ctx.fill();
395
+
396
+ // 식별 정보
397
+ this.ctx.fillStyle = '#666';
398
+ this.ctx.font = '10px monospace';
399
+ this.ctx.fillText(`${target.id} (${target.type})`, pos.x+8, pos.y+4);
400
  });
401
  }
402
 
403
  // μ‚¬μ΄λ“œλ°” 'Real-time Detections' μ—…λ°μ΄νŠΈ
404
  updateDetections() {
405
  const detections = document.getElementById('detections');
406
+ let html = '';
407
+ this.targets.forEach(t => {
408
+ html += `
409
  <div class="detection">
410
+ ${t.type==='aircraft' ? '✈️' : 'πŸš—'}
411
+ ${t.id}
412
+ Speed: ${t.speed}kts
413
+ ${t.type==='aircraft' ? 'Alt: '+t.altitude+'ft' : ''}
414
+ Sig: ${(t.signalStrength*100).toFixed(0)}%
 
 
 
 
415
  </div>
416
+ `;
417
+ });
418
+ detections.innerHTML = html;
419
  }
420
 
421
+ // μˆ˜μ‹ κΈ° μ‹ ν˜Έκ°•λ„ λ°” λ¬΄μž‘μœ„ μ—…λ°μ΄νŠΈ
422
  updateSignalStrengths() {
423
+ sdrStations.forEach(st => {
424
+ const bar = document.querySelector(`#rx-${st.url.split(':')[0]} .signal-bar`);
425
  if (bar) {
426
+ const strength = 40 + Math.random()*60;
 
427
  bar.style.width = `${strength}%`;
428
  }
429
  });
430
  }
431
 
432
+ // 메인 루프
433
  startTracking() {
434
+ // 폭풍 ν† κΈ€(μ•½ 20% ν™•λ₯ λ‘œ 10μ΄ˆλ§ˆλ‹€ ν•œ 번 λ°œμƒ)
435
  setInterval(() => {
436
+ if (Math.random() < 0.2) {
437
+ this.toggleStorm();
 
438
  }
439
+ }, 10000);
440
+
441
+ // 100msλ§ˆλ‹€ κ°±μ‹ 
442
+ setInterval(() => {
443
+ // 10% ν™•λ₯ λ‘œ νƒ€κ²Ÿ μΆ”κ°€
444
+ if (Math.random() < 0.1 && this.targets.size < 15) {
445
+ const newT = this.generateTarget();
446
+ this.targets.add(newT);
447
+ this.addEventLog(`μƒˆ νƒ€κ²Ÿ μΆœν˜„: ${newT.id}`);
448
+ }
449
+ // 10% ν™•λ₯ λ‘œ νƒ€κ²Ÿ ν•˜λ‚˜ 제거
450
  if (Math.random() < 0.1 && this.targets.size > 0) {
451
+ const first = Array.from(this.targets)[0];
452
+ this.targets.delete(first);
453
+ this.addEventLog(`νƒ€κ²Ÿ μ†Œλ©Έ: ${first.id}`);
454
  }
455
 
456
+ // 맀 ν”„λ ˆμž„ ν™”λ©΄ 그리기
457
+ this.drawBackground();
458
+ this.drawStations();
459
+ this.drawTargets();
460
+
461
+ // μ‚¬μ΄λ“œλ°” κ°±μ‹ 
462
  this.updateDetections();
463
  this.updateSignalStrengths();
464
+ }, 100);
465
  }
466
  }
467
 
468
+ // νŽ˜μ΄μ§€ λ‘œλ“œ μ‹œ μ‹œμž‘
469
  window.addEventListener('load', () => {
470
+ new RadarSystem();
471
  });
472
  </script>
473
  </body>