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

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +343 -240
index.html CHANGED
@@ -3,8 +3,10 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <title>Global SDR Network Monitor</title>
6
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.css" />
 
7
  <style>
 
8
  body {
9
  margin: 0;
10
  padding: 20px;
@@ -13,23 +15,26 @@
13
  font-family: monospace;
14
  overflow: hidden;
15
  }
16
-
17
  .container {
18
  display: grid;
19
  grid-template-columns: 300px 1fr;
20
  gap: 20px;
21
- height: calc(100vh - 40px);
22
  }
23
-
24
  .sidebar {
25
  background: #111;
26
  padding: 15px;
27
  border-radius: 8px;
28
- height: 100%;
29
  overflow-y: auto;
30
  z-index: 1000;
31
  }
 
 
 
 
 
32
 
 
33
  .receiver {
34
  margin: 10px 0;
35
  padding: 10px;
@@ -37,141 +42,80 @@
37
  border-radius: 4px;
38
  position: relative;
39
  }
40
-
41
  .status {
42
  display: flex;
43
  align-items: center;
44
  margin-bottom: 5px;
45
  }
46
-
47
  .led {
48
  width: 8px;
49
  height: 8px;
50
  border-radius: 50%;
51
  margin-right: 8px;
52
  }
53
-
54
  .active {
55
  background: #0f0;
56
  box-shadow: 0 0 10px #0f0;
57
  animation: pulse 2s infinite;
58
  }
59
-
60
  @keyframes pulse {
61
  0% { opacity: 1; }
62
  50% { opacity: 0.5; }
63
  100% { opacity: 1; }
64
  }
65
-
66
  .inactive {
67
  background: #f00;
68
  }
69
-
70
- #map-container {
71
- position: relative;
72
- height: 100%;
73
- border-radius: 8px;
74
- overflow: hidden;
75
- }
76
-
77
- canvas#map {
78
- position: absolute;
79
- top: 0;
80
- left: 0;
81
- width: 100%;
82
- height: 100%;
83
- z-index: 1;
84
- }
85
-
86
- #world-map {
87
- position: absolute;
88
- top: 0;
89
- left: 0;
90
- width: 100%;
91
- height: 100%;
92
- z-index: 0;
93
- filter: invert(1) hue-rotate(180deg);
94
- opacity: 0.5;
95
- background: #111;
96
- }
97
-
98
  .signal-strength {
99
  height: 4px;
100
  background: #222;
101
  margin-top: 5px;
102
  border-radius: 2px;
103
  }
104
-
105
  .signal-bar {
106
  height: 100%;
107
- background: linear-gradient(to right, #0f0, #00ff00);
108
- width: 0%;
109
- transition: width 0.3s ease-in-out;
110
- box-shadow: 0 0 5px #0f0;
111
- border-radius: 2px;
112
  }
113
 
 
114
  .detection {
115
- background: rgba(0, 255, 0, 0.1);
116
- border-left: 3px solid #0f0;
117
- padding: 8px;
118
  margin: 5px 0;
119
  font-size: 12px;
120
- border-radius: 0 4px 4px 0;
121
- }
122
-
123
- .signal-line {
124
- position: absolute;
125
- background: linear-gradient(90deg, rgba(0,255,0,0.4) 0%, rgba(0,255,0,0) 100%);
126
- height: 2px;
127
- transform-origin: 0 0;
128
- pointer-events: none;
129
- opacity: 0.7;
130
- animation: signalPulse 2s infinite;
131
  }
132
 
133
- @keyframes signalPulse {
134
- 0% { opacity: 0.7; }
135
- 50% { opacity: 0.3; }
136
- 100% { opacity: 0.7; }
137
  }
138
-
139
  .leaflet-container {
140
  background: #111 !important;
141
  }
142
-
143
  .leaflet-control-attribution {
144
- background: rgba(17, 17, 17, 0.8) !important;
145
  color: #666 !important;
146
  }
147
-
148
- .leaflet-control-attribution a {
149
- color: #888 !important;
150
- }
151
-
152
- .leaflet-popup-content {
153
- color: black;
154
- }
155
-
156
- .leaflet-control-zoom {
157
- border: none !important;
158
- background: rgba(17, 17, 17, 0.8) !important;
159
- }
160
-
161
- .leaflet-control-zoom a {
162
- color: #0f0 !important;
163
  background: #222 !important;
164
- border: 1px solid #0f0 !important;
165
  }
166
 
167
- .leaflet-control-zoom a:hover {
168
- background: #333 !important;
 
 
 
 
169
  }
170
  </style>
171
  </head>
172
-
173
  <body>
174
  <div class="container">
 
175
  <div class="sidebar">
176
  <h3>Active SDR Receivers</h3>
177
  <div id="receivers"></div>
@@ -180,15 +124,14 @@
180
  <div id="detections"></div>
181
  </div>
182
 
183
- <div id="map-container">
184
- <div id="world-map"></div>
185
- <canvas id="map"></canvas>
186
- </div>
187
  </div>
188
 
189
- <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.js"></script>
 
190
  <script>
191
- // Global SDR stations data
192
  const sdrStations = [
193
  // Europe
194
  {
@@ -273,172 +216,332 @@
273
  range: 145,
274
  active: true
275
  },
276
- // ... [이전에 μ •μ˜λœ λ‚˜λ¨Έμ§€ μŠ€ν…Œμ΄μ…˜λ“€λ„ 포함]
277
- ];
278
-
279
- class RadarSystem {
280
- constructor() {
281
- this.canvas = document.getElementById('map');
282
- this.ctx = this.canvas.getContext('2d');
283
- this.targets = new Set();
284
- this.signalLines = new Set();
285
- this.setupWorldMap();
286
- this.setupCanvas();
287
- this.renderReceivers();
288
- this.startTracking();
289
-
290
- window.addEventListener('resize', this.handleResize.bind(this));
291
- }
292
-
293
- setupWorldMap() {
294
- // 지도 μ΄ˆκΈ°ν™” μ„€μ • μˆ˜μ •
295
- this.worldMap = L.map('world-map', {
296
- center: [30, 0],
297
- zoom: 3,
298
- minZoom: 2,
299
- maxZoom: 18,
300
- zoomControl: true,
301
- dragging: true,
302
- scrollWheelZoom: true,
303
- doubleClickZoom: true
304
- });
305
-
306
- // 타일 λ ˆμ΄μ–΄ μŠ€νƒ€μΌ μˆ˜μ •
307
- L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
308
- attribution: 'Β© OpenStreetMap contributors',
309
- noWrap: false,
310
- bounds: [[-90, -180], [90, 180]]
311
- }).addTo(this.worldMap);
312
-
313
- // 쀌 컨트둀 μœ„μΉ˜ μ‘°μ •
314
- L.control.zoom({
315
- position: 'topright'
316
- }).addTo(this.worldMap);
317
-
318
- // SDR μŠ€ν…Œμ΄μ…˜ 마컀 μΆ”κ°€
319
- sdrStations.forEach(station => {
320
- // μŠ€ν…Œμ΄μ…˜ 마컀
321
- const stationMarker = L.circleMarker([station.location[0], station.location[1]], {
322
- radius: 8,
323
- color: '#0f0',
324
- fillColor: '#0f0',
325
- fillOpacity: 0.5,
326
- weight: 2
327
- }).addTo(this.worldMap);
328
-
329
- // λ²”μœ„ 원
330
- L.circle([station.location[0], station.location[1]], {
331
- radius: station.range * 1000, // kmλ₯Ό m둜 λ³€ν™˜
332
- color: '#0f0',
333
- fillColor: '#0f0',
334
- fillOpacity: 0.1,
335
- weight: 1
336
- }).addTo(this.worldMap);
337
-
338
- // νŒμ—… 정보
339
- stationMarker.bindPopup(`
340
- <div style="color: black;">
341
- <strong>${station.name}</strong><br>
342
- πŸ“‘ ${station.url}<br>
343
- πŸ“» ${station.frequency}<br>
344
- Range: ${station.range}km
345
- </div>
346
- `);
347
- });
348
- }
349
-
350
- generateTarget() {
351
- const station = sdrStations[Math.floor(Math.random() * sdrStations.length)];
352
- const range = station.range / 100; // λ²”μœ„ μ‘°μ •
353
- return {
354
- type: Math.random() > 0.7 ? 'aircraft' : 'vehicle',
355
- position: {
356
- lat: station.location[0] + (Math.random() - 0.5) * range,
357
- lon: station.location[1] + (Math.random() - 0.5) * range
358
  },
359
- speed: Math.random() * 100 + 50,
360
- heading: Math.random() * 360,
361
- station: station
362
- };
363
- }
364
-
365
- updateTargets() {
366
- if (Math.random() < 0.1 && this.targets.size < 10) {
367
- const newTarget = this.generateTarget();
368
- this.targets.add(newTarget);
369
-
370
- // νƒ€κ²Ÿ 마컀 μŠ€νƒ€μΌ μˆ˜μ •
371
- const targetMarker = L.circleMarker([newTarget.position.lat, newTarget.position.lon], {
372
- radius: 5,
373
- color: newTarget.type === 'aircraft' ? '#ff0' : '#f00',
374
- fillColor: newTarget.type === 'aircraft' ? '#ff0' : '#f00',
375
- fillOpacity: 0.8,
376
- weight: 2
377
- }).addTo(this.worldMap);
378
-
379
- newTarget.marker = targetMarker;
380
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
381
 
382
- this.targets.forEach(target => {
383
- const rad = target.heading * Math.PI / 180;
384
- target.position.lat += Math.cos(rad) * target.speed * 0.00001;
385
- target.position.lon += Math.sin(rad) * target.speed * 0.00001;
 
 
 
 
 
 
 
 
 
386
 
387
- if (target.marker) {
388
- target.marker.setLatLng([target.position.lat, target.position.lon]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389
  }
390
 
391
- const distance = this.calculateDistance(
392
- target.position.lat,
393
- target.position.lon,
394
- target.station.location[0],
395
- target.station.location[1]
396
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
397
 
398
- if (distance > target.station.range) {
399
- if (target.marker) {
400
- this.worldMap.removeLayer(target.marker);
401
- }
402
- this.targets.delete(target);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
403
  }
404
- });
405
- }
406
 
407
- addSignalLine(target) {
408
- const stationLatLng = L.latLng(target.station.location[0], target.station.location[1]);
409
- const targetLatLng = L.latLng(target.position.lat, target.position.lon);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
410
 
411
- // μ‹ ν˜Έμ„  μŠ€νƒ€μΌ μˆ˜μ •
412
- const line = L.polyline([stationLatLng, targetLatLng], {
413
- color: '#0f0',
414
- weight: 2,
415
- opacity: 0.8,
416
- dashArray: '5, 10',
417
- className: 'signal-line'
418
- }).addTo(this.worldMap);
 
 
 
 
 
 
 
 
 
 
419
 
420
- // μ‹ ν˜Έμ„  μ• λ‹ˆλ©”μ΄μ…˜
421
- setTimeout(() => {
422
- this.worldMap.removeLayer(line);
423
- }, 2000);
424
- }
 
 
 
 
 
 
425
 
 
426
  startTracking() {
427
  setInterval(() => {
 
 
 
 
 
 
 
 
 
 
 
428
  this.updateTargets();
429
- this.targets.forEach(target => {
430
- if (Math.random() < 0.3) {
431
- this.addDetection(target);
432
- }
433
- });
434
- }, 2000);
435
  }
436
  }
437
 
438
- // Initialize the radar system when the page loads
439
  window.addEventListener('load', () => {
440
- new RadarSystem();
441
  });
442
  </script>
443
  </body>
444
- </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;
 
15
  font-family: monospace;
16
  overflow: hidden;
17
  }
 
18
  .container {
19
  display: grid;
20
  grid-template-columns: 300px 1fr;
21
  gap: 20px;
 
22
  }
 
23
  .sidebar {
24
  background: #111;
25
  padding: 15px;
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;
 
42
  border-radius: 4px;
43
  position: relative;
44
  }
 
45
  .status {
46
  display: flex;
47
  align-items: center;
48
  margin-bottom: 5px;
49
  }
 
50
  .led {
51
  width: 8px;
52
  height: 8px;
53
  border-radius: 50%;
54
  margin-right: 8px;
55
  }
 
56
  .active {
57
  background: #0f0;
58
  box-shadow: 0 0 10px #0f0;
59
  animation: pulse 2s infinite;
60
  }
 
61
  @keyframes pulse {
62
  0% { opacity: 1; }
63
  50% { opacity: 0.5; }
64
  100% { opacity: 1; }
65
  }
 
66
  .inactive {
67
  background: #f00;
68
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  .signal-strength {
70
  height: 4px;
71
  background: #222;
72
  margin-top: 5px;
73
  border-radius: 2px;
74
  }
 
75
  .signal-bar {
76
  height: 100%;
77
+ background: #0f0;
78
+ width: 50%;
79
+ transition: width 0.3s;
 
 
80
  }
81
 
82
+ /* 탐지(Detection) λͺ©λ‘ μŠ€νƒ€μΌ */
83
  .detection {
84
+ padding: 5px;
 
 
85
  margin: 5px 0;
86
  font-size: 12px;
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>
 
116
  <body>
117
  <div class="container">
118
+ <!-- μ‚¬μ΄λ“œλ°” -->
119
  <div class="sidebar">
120
  <h3>Active SDR Receivers</h3>
121
  <div id="receivers"></div>
 
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
  {
 
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>
401
+ </div>
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>
547
+ </html>