kolaslab commited on
Commit
182a04d
โ€ข
1 Parent(s): da78557

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +257 -441
index.html CHANGED
@@ -2,11 +2,9 @@
2
  <html>
3
  <head>
4
  <meta charset="UTF-8">
5
- <title>Hyperscan: Global SDR Radar(Simul)</title>
6
- <!-- Leaflet CSS -->
7
- <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
8
  <style>
9
- /* ====================== ๊ณตํ†ต ์Šคํƒ€์ผ ====================== */
10
  body {
11
  margin: 0;
12
  padding: 20px;
@@ -28,29 +26,12 @@
28
  overflow-y: auto;
29
  }
30
  #map {
31
- height: calc(100vh - 40px);
32
- border-radius: 8px;
33
  background: #111;
 
 
34
  }
35
 
36
- /* Leaflet ๋‹คํฌํ…Œ๋งˆ ํšจ๊ณผ (ํƒ€์ผ ๋ฐ˜์ „) */
37
- .leaflet-tile-pane {
38
- filter: invert(1) hue-rotate(180deg);
39
- }
40
- .leaflet-container {
41
- background: #111 !important;
42
- }
43
- .leaflet-control-attribution {
44
- background: #222 !important;
45
- color: #666 !important;
46
- }
47
- .leaflet-popup-content-wrapper,
48
- .leaflet-popup-tip {
49
- background: #222 !important;
50
- color: #0f0 !important;
51
- }
52
-
53
- /* ====================== ์ˆ˜์‹ ๊ธฐ(Receivers) ๋ชฉ๋ก ====================== */
54
  .receiver {
55
  margin: 10px 0;
56
  padding: 10px;
@@ -95,7 +76,7 @@
95
  transition: width 0.3s;
96
  }
97
 
98
- /* ====================== ํƒ์ง€(Detections) ๋ชฉ๋ก ====================== */
99
  .detection {
100
  padding: 5px;
101
  margin: 5px 0;
@@ -103,27 +84,26 @@
103
  border-left: 2px solid #0f0;
104
  }
105
 
106
- /* ====================== ์ด๋ฒคํŠธ ๋กœ๊ทธ ====================== */
 
 
 
107
  .alert {
108
- background: #911;
109
- padding: 5px;
110
- margin: 5px 0;
111
  border-left: 2px solid #f00;
 
 
112
  color: #f66;
113
  }
114
 
115
- /* ====================== ์Šคํ…Œ์ด์…˜ ๋ฒ”์œ„ & ํญํ’ ๋ฒ”์œ„ ์Šคํƒ€์ผ ====================== */
116
- .station-range {
117
- stroke: #0f0;
118
- stroke-width: 1;
119
- fill: #0f0;
120
- fill-opacity: 0.1;
121
- }
122
- .storm-range {
123
- stroke: #f00;
124
- stroke-width: 1;
125
- fill: #f00;
126
- fill-opacity: 0.1;
127
  }
128
  </style>
129
  </head>
@@ -131,28 +111,23 @@
131
  <div class="container">
132
  <!-- ์‚ฌ์ด๋“œ๋ฐ” -->
133
  <div class="sidebar">
134
- <h2>Hyperscan: Global SDR Radar(Simul)</h2>
135
-
136
- <h3>SDR Receivers</h3>
137
  <div id="receivers"></div>
138
 
139
  <h3>Real-time Detections</h3>
140
  <div id="detections"></div>
141
-
142
  <h3>Events</h3>
143
- <div id="events"></div>
144
  </div>
145
 
146
- <!-- Leaflet ์ง€๋„ ์˜์—ญ -->
147
- <div id="map"></div>
148
  </div>
149
 
150
- <!-- Leaflet JS -->
151
- <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
152
  <script>
153
- // ์ „ ์„ธ๊ณ„ SDR ์Šคํ…Œ์ด์…˜ 23๊ฐœ (Europe, USA, Japan, Australia, Russia, China, S. Korea, Canada, Brazil)
154
  const sdrStations = [
155
- // Europe
156
  {
157
  name: "Twente WebSDR",
158
  url: "websdr.ewi.utwente.nl:8901",
@@ -184,246 +159,59 @@
184
  frequency: "0-30 MHz",
185
  range: 160,
186
  active: true
187
- },
188
- // United States
189
- {
190
- name: "W6DRZ WebSDR",
191
- url: "w6drz.sdr.us:8901",
192
- location: [34.2847, -118.4429],
193
- frequency: "0-30 MHz",
194
- range: 170,
195
- active: true
196
- },
197
- {
198
- name: "K3FEF WebSDR",
199
- url: "k3fef.sdr.us:8901",
200
- location: [40.5697, -75.9363],
201
- frequency: "0-30 MHz",
202
- range: 160,
203
- active: true
204
- },
205
- {
206
- name: "WA2ZKD KiwiSDR",
207
- url: "wa2zkd.sdr.us:8073",
208
- location: [40.7128, -74.0060],
209
- frequency: "0-30 MHz",
210
- range: 150,
211
- active: true
212
- },
213
- {
214
- name: "W4AX WebSDR",
215
- url: "w4ax.sdr.us:8901",
216
- location: [33.7756, -84.3963],
217
- frequency: "0-30 MHz",
218
- range: 165,
219
- active: true
220
- },
221
- // Japan
222
- {
223
- name: "JH7VHZ WebSDR",
224
- url: "jh7vhz.sdr.jp:8901",
225
- location: [38.2682, 140.8694],
226
- frequency: "0-30 MHz",
227
- range: 155,
228
- active: true
229
- },
230
- {
231
- name: "JA1GJB KiwiSDR",
232
- url: "ja1gjb.sdr.jp:8073",
233
- location: [35.6762, 139.6503],
234
- frequency: "0-30 MHz",
235
- range: 145,
236
- active: true
237
- },
238
- {
239
- name: "JA3ZOH WebSDR",
240
- url: "ja3zoh.sdr.jp:8901",
241
- location: [34.6937, 135.5023],
242
- frequency: "0-30 MHz",
243
- range: 150,
244
- active: true
245
- },
246
- // Australia
247
- {
248
- name: "VK4YA KiwiSDR",
249
- url: "vk4ya.sdr.au:8073",
250
- location: [-27.4698, 153.0251],
251
- frequency: "0-30 MHz",
252
- range: 170,
253
- active: true
254
- },
255
- {
256
- name: "VK2RG WebSDR",
257
- url: "vk2rg.sdr.au:8901",
258
- location: [-33.8688, 151.2093],
259
- frequency: "0-30 MHz",
260
- range: 165,
261
- active: true
262
- },
263
- // Russia
264
- {
265
- name: "RZ3DJR WebSDR",
266
- url: "rz3djr.sdr.ru:8901",
267
- location: [55.7558, 37.6173],
268
- frequency: "0-30 MHz",
269
- range: 180,
270
- active: true
271
- },
272
- {
273
- name: "UA9UDX WebSDR",
274
- url: "ua9udx.sdr.ru:8901",
275
- location: [55.0084, 82.9357],
276
- frequency: "0-30 MHz",
277
- range: 175,
278
- active: true
279
- },
280
- // China
281
- {
282
- name: "BY1PK WebSDR",
283
- url: "by1pk.sdr.cn:8901",
284
- location: [39.9042, 116.4074],
285
- frequency: "0-30 MHz",
286
- range: 160,
287
- active: true
288
- },
289
- {
290
- name: "BG3MDO KiwiSDR",
291
- url: "bg3mdo.sdr.cn:8073",
292
- location: [23.1291, 113.2644],
293
- frequency: "0-30 MHz",
294
- range: 155,
295
- active: true
296
- },
297
- // South Korea
298
- {
299
- name: "HL2WA KiwiSDR",
300
- url: "hl2wa.sdr.kr:8073",
301
- location: [37.5665, 126.9780],
302
- frequency: "0-30 MHz",
303
- range: 150,
304
- active: true
305
- },
306
- {
307
- name: "DS1URB WebSDR",
308
- url: "ds1urb.sdr.kr:8901",
309
- location: [35.1796, 129.0756],
310
- frequency: "0-30 MHz",
311
- range: 145,
312
- active: true
313
- },
314
- // Canada
315
- {
316
- name: "VE3HOA WebSDR",
317
- url: "ve3hoa.sdr.ca:8901",
318
- location: [43.6532, -79.3832],
319
- frequency: "0-30 MHz",
320
- range: 165,
321
- active: true
322
- },
323
- {
324
- name: "VA3ROM KiwiSDR",
325
- url: "va3rom.sdr.ca:8073",
326
- location: [45.4215, -75.6972],
327
- frequency: "0-30 MHz",
328
- range: 160,
329
- active: true
330
- },
331
- // Brazil
332
- {
333
- name: "PY2RDZ WebSDR",
334
- url: "py2rdz.sdr.br:8901",
335
- location: [-23.5505, -46.6333],
336
- frequency: "0-30 MHz",
337
- range: 170,
338
- active: true
339
- },
340
- {
341
- name: "PY1ZV KiwiSDR",
342
- url: "py1zv.sdr.br:8073",
343
- location: [-22.9068, -43.1729],
344
- frequency: "0-30 MHz",
345
- range: 165,
346
- active: true
347
  }
348
  ];
349
 
350
- // Leaflet ๊ธฐ๋ฐ˜ RadarSystem
351
  class RadarSystem {
352
  constructor() {
353
- // ํญํ’ ์ƒํƒœ
 
 
 
354
  this.stormActive = false;
355
- // ํญํ’ ์ค‘์‹ฌ(์œ ๋Ÿฝ ๊ทผ๋ฐฉ)
356
- this.stormCenter = [50.5, 5.0];
357
- // ํญํ’ ๋ฐ˜๊ฒฝ(km)
358
- this.stormRadius = 200;
359
-
360
- // ํƒ€๊ฒŸ ๋ชฉ๋ก (key: targetID, value: ๊ฐ์ฒด)
361
- this.targets = new Map();
362
- // ํƒ€๊ฒŸ ๋งˆ์ปค
363
- this.targetMarkers = new Map();
364
- // ํƒ€๊ฒŸ-์Šคํ…Œ์ด์…˜ ์—ฐ๊ฒฐ์„ 
365
- this.targetSignalLines = new Map();
366
-
367
- // ์ด๋ฒคํŠธ ๋กœ๊ทธ (์ตœ๋Œ€ 30๊ฐœ ์œ ์ง€)
368
- this.eventsLog = [];
369
-
370
- this.initializeMap();
371
  this.renderReceivers();
372
  this.startTracking();
373
  }
374
 
375
- // ์ง€๋„ ์ดˆ๊ธฐํ™”
376
- initializeMap() {
377
- this.map = L.map('map', {
378
- center: [20, 0],
379
- zoom: 3,
380
- worldCopyJump: true
381
- });
382
-
383
- // ํƒ€์ผ ๋ ˆ์ด์–ด (OSM)
384
- L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
385
- maxZoom: 19,
386
- attribution: 'ยฉ OpenStreetMap contributors'
387
- }).addTo(this.map);
388
 
389
- // ์Šคํ…Œ์ด์…˜ ํ‘œ์‹œ
390
- sdrStations.forEach(st => {
391
- // ์Šคํ…Œ์ด์…˜ ๋งˆ์ปค
392
- const stationMarker = L.circleMarker(st.location, {
393
- radius: 5,
394
- color: '#0f0',
395
- fillColor: '#0f0',
396
- fillOpacity: 1
397
- }).addTo(this.map);
398
-
399
- // ๋ฒ”์œ„ ์›
400
- L.circle(st.location, {
401
- radius: st.range * 1000,
402
- className: 'station-range'
403
- }).addTo(this.map);
404
-
405
- // ํˆดํŒ
406
- stationMarker.bindTooltip(`
407
- <b>${st.name}</b><br/>
408
- Frequency: ${st.frequency}<br/>
409
- Range: ${st.range} km
410
- `);
411
  });
412
  }
413
 
414
- // ์‚ฌ์ด๋“œ๋ฐ”์— ์ˆ˜์‹ ๊ธฐ(Receivers) ๋ Œ๋”๋ง
415
  renderReceivers() {
416
  const container = document.getElementById('receivers');
417
- container.innerHTML = sdrStations.map(st => `
418
- <div class="receiver" id="rx-${st.url.split(':')[0]}">
419
  <div class="status">
420
- <div class="led ${st.active ? 'active' : 'inactive'}"></div>
421
- <strong>${st.name}</strong>
422
  </div>
423
- <div>๐Ÿ“ก ${st.url}</div>
424
- <div>๐Ÿ“ป ${st.frequency}</div>
425
- <div>๐Ÿ“ ${st.location.join(', ')}</div>
426
- <div>Range: ${st.range}km</div>
427
  <div class="signal-strength">
428
  <div class="signal-bar"></div>
429
  </div>
@@ -431,239 +219,267 @@
431
  `).join('');
432
  }
433
 
434
- // ์ด๋ฒคํŠธ ๋กœ๊ทธ ์ถ”๊ฐ€ (์ตœ๋Œ€ 30๊ฐœ ์œ ์ง€)
435
- addEventLog(msg) {
436
- this.eventsLog.push(msg);
437
- const eventsDiv = document.getElementById('events');
438
- eventsDiv.innerHTML += `<div class="alert">${msg}</div>`;
439
 
440
- if (this.eventsLog.length > 30) {
441
- this.eventsLog.shift(); // ๊ฐ€์žฅ ์˜ค๋ž˜๋œ ๋กœ๊ทธ ์ œ๊ฑฐ
442
- eventsDiv.removeChild(eventsDiv.firstChild);
443
- }
444
  }
445
 
446
- // ํญํ’ ํ† ๊ธ€
447
- toggleStorm() {
448
- this.stormActive = !this.stormActive;
449
- const msg = this.stormActive
450
- ? "ํญํ’ ๋ฐœ์ƒ! (๊ต๋ž€ ๊ฐ€๋Šฅ)"
451
- : "ํญํ’์ด ์†Œ๋ฉธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.";
452
- this.addEventLog(msg);
453
-
454
- // ํญํ’ ๋ฒ”์œ„ ํ‘œ์‹œ/ํ•ด์ œ
455
- if (this.stormCircle) {
456
- this.map.removeLayer(this.stormCircle);
457
- }
458
- if (this.stormActive) {
459
- this.stormCircle = L.circle(this.stormCenter, {
460
- radius: this.stormRadius * 1000,
461
- className: 'storm-range'
462
- }).addTo(this.map);
463
  }
464
  }
465
 
466
- // ๋ฌด์ž‘์œ„ ํƒ€๊ฒŸ ์ƒ์„ฑ
467
  generateTarget() {
468
- const lat = 20 + (Math.random()-0.5)*40; // ๋ฒ”์œ„ ๋Œ€ํญ ํ™•๋Œ€, ์ „์ง€๊ตฌ์ 
469
- const lon = 0 + (Math.random()-0.5)*80;
 
 
470
  return {
471
- id: Math.random().toString(36).substr(2, 6).toUpperCase(),
472
  type: (Math.random() > 0.7) ? 'aircraft' : 'vehicle',
473
- lat,
474
- lon,
475
- speed: Math.floor(Math.random()*200 + 100),
476
- altitude: Math.floor(Math.random()*30000 + 1000),
 
 
477
  heading: Math.random()*360,
 
478
  signalStrength: Math.random()
479
  };
480
  }
481
 
482
- // ๋‘ ์ขŒํ‘œ ๊ฐ„ ๊ฑฐ๋ฆฌ(km)
483
- getDistance(lat1, lon1, lat2, lon2) {
484
- const R = 6371;
485
- const dLat = (lat2 - lat1) * Math.PI/180;
486
- const dLon = (lon2 - lon1) * Math.PI/180;
487
- const a = Math.sin(dLat/2)*Math.sin(dLat/2)
488
- + Math.cos(lat1*Math.PI/180)*Math.cos(lat2*Math.PI/180)
489
- * Math.sin(dLon/2)*Math.sin(dLon/2);
490
- return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
491
  }
492
 
493
- // ํƒ€๊ฒŸ ์ด๋™(heading, speed)
494
- moveTarget(t) {
495
- const speedFactor = 0.00005;
496
- const rad = (t.heading * Math.PI)/180;
497
- t.lat += Math.cos(rad) * t.speed * speedFactor;
498
- t.lon += Math.sin(rad) * t.speed * speedFactor;
499
-
500
- // ํญํ’ ๋ฒ”์œ„ ๋‚ด๋ผ๋ฉด ์‹ ํ˜ธ๊ฐ•๋„ ํ•˜๋ฝ
501
  if (this.stormActive) {
502
- const dist = this.getDistance(t.lat, t.lon, this.stormCenter[0], this.stormCenter[1]);
503
- if (dist < this.stormRadius) {
504
- t.signalStrength = Math.max(0, t.signalStrength - 0.01);
505
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
506
  }
507
  }
508
 
509
- // ํƒ€๊ฒŸ/์—ฐ๊ฒฐ์„  ์ง€๋„ ์—…๋ฐ์ดํŠธ
510
- updateTargetsOnMap() {
511
- // ๊ธฐ์กด ์—ฐ๊ฒฐ์„  ์ œ๊ฑฐ
512
- this.targetSignalLines.forEach(line => {
513
- this.map.removeLayer(line);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
514
  });
515
- this.targetSignalLines.clear();
516
-
517
- // ๊ฐ ํƒ€๊ฒŸ์— ๋Œ€ํ•ด ๋งˆ์ปค ์œ„์น˜/ํˆดํŒ ๊ฐฑ์‹ , ์—ฐ๊ฒฐ์„  ์ƒ์„ฑ
518
- this.targets.forEach((t, id) => {
519
- // ๋งˆ์ปค
520
- let marker = this.targetMarkers.get(id);
521
- if (!marker) {
522
- marker = L.circleMarker([t.lat, t.lon], {
523
- radius: 4,
524
- color: t.type === 'aircraft' ? '#ff0' : '#0ff',
525
- fillColor: t.type === 'aircraft' ? '#ff0' : '#0ff',
526
- fillOpacity: 1
527
- }).addTo(this.map);
528
-
529
- marker.bindTooltip(this.makeTooltipHTML(t), { sticky: true });
530
- this.targetMarkers.set(id, marker);
531
- } else {
532
- // ์ขŒํ‘œ ๋ฐ ์Šคํƒ€์ผ ์—…๋ฐ์ดํŠธ
533
- marker.setLatLng([t.lat, t.lon]);
534
- marker.setStyle({
535
- color: t.type === 'aircraft' ? '#ff0' : '#0ff',
536
- fillColor: t.type === 'aircraft' ? '#ff0' : '#0ff'
537
- });
538
- marker.setTooltipContent(this.makeTooltipHTML(t));
539
  }
540
 
541
- // ์Šคํ…Œ์ด์…˜ ๋ฒ”์œ„ ๋‚ด๋ฉด ์—ฐ๊ฒฐ์„  ํ‘œ์‹œ
 
 
 
 
 
 
 
 
 
542
  sdrStations.forEach(st => {
543
  if (st.active) {
544
- const dist = this.getDistance(t.lat, t.lon, st.location[0], st.location[1]);
545
- if (dist <= st.range) {
546
- const line = L.polyline([
547
- [t.lat, t.lon],
548
- st.location
549
- ], {
550
- color: '#0f0',
551
- opacity: t.signalStrength * 0.3,
552
- weight: 1
553
- }).addTo(this.map);
554
-
555
- this.targetSignalLines.set(`${id}-${st.name}`, line);
556
  }
557
  }
558
  });
559
- });
560
- }
561
 
562
- // ํƒ€๊ฒŸ ํˆดํŒ
563
- makeTooltipHTML(t) {
564
- return `
565
- <b>${t.id}</b><br/>
566
- Type: ${t.type}<br/>
567
- Speed: ${t.speed} kts<br/>
568
- ${
569
- t.type === 'aircraft'
570
- ? `Alt: ${t.altitude} ft<br/>`
571
- : ''
572
- }
573
- Sig: ${(t.signalStrength*100).toFixed(0)}%
574
- `;
 
575
  }
576
 
577
- // ์‹ค์‹œ๊ฐ„ Detections ๋ชฉ๋ก ์—…๋ฐ์ดํŠธ
578
  updateDetections() {
579
- const detDiv = document.getElementById('detections');
580
- let html = '';
581
- this.targets.forEach(t => {
582
- html += `<div class="detection">
583
- ${t.type === 'aircraft' ? 'โœˆ๏ธ' : '๐Ÿš—'} ${t.id}
584
- ${t.type === 'aircraft' ? `Alt: ${t.altitude}ft ` : ''}
585
- Speed: ${t.speed}kts
586
- Sig: ${(t.signalStrength*100).toFixed(0)}%
587
- </div>`;
588
- });
589
- detDiv.innerHTML = html;
590
  }
591
 
592
- // ์ˆ˜์‹ ๊ธฐ ์‹ ํ˜ธ๊ฐ•๋„ ๋ฐ” (๋žœ๋ค)
593
  updateSignalStrengths() {
594
  sdrStations.forEach(st => {
595
  const bar = document.querySelector(`#rx-${st.url.split(':')[0]} .signal-bar`);
596
  if (bar) {
597
- const strength = 40 + Math.random()*60;
598
  bar.style.width = `${strength}%`;
599
  }
600
  });
601
  }
602
 
603
- // ํƒ€๊ฒŸ ์ œ๊ฑฐ
604
- removeTarget(id) {
605
- const t = this.targets.get(id);
606
- if (!t) return;
607
- this.targets.delete(id);
608
- this.addEventLog(`ํƒ€๊ฒŸ ์†Œ๋ฉธ: ${t.id}`);
609
-
610
- // ๋งˆ์ปค ์‚ญ์ œ
611
- const marker = this.targetMarkers.get(id);
612
- if (marker) {
613
- this.map.removeLayer(marker);
614
- this.targetMarkers.delete(id);
615
- }
616
- // ์—ฐ๊ฒฐ์„ ๋„ ์‚ญ์ œ
617
- [...this.targetSignalLines.keys()].forEach(k => {
618
- if (k.includes(id)) {
619
- this.map.removeLayer(this.targetSignalLines.get(k));
620
- this.targetSignalLines.delete(k);
621
- }
622
- });
623
  }
624
 
625
  // ๋ฉ”์ธ ๋ฃจํ”„
626
  startTracking() {
627
- // ํญํ’: 10์ดˆ ๊ฐ„๊ฒฉ์œผ๋กœ 20% ํ™•๋ฅ  ํ† ๊ธ€
628
  setInterval(() => {
629
- if (Math.random() < 0.2) {
630
  this.toggleStorm();
631
  }
 
 
 
632
  }, 10000);
633
 
634
- // 100ms ๊ฐ„๊ฒฉ ๊ฐฑ์‹ 
635
  setInterval(() => {
636
- // 10% ํ™•๋ฅ ๋กœ ์ƒˆ ํƒ€๊ฒŸ, ์ตœ๋Œ€ 20๊ฐœ
637
- if (Math.random() < 0.1 && this.targets.size < 20) {
638
  const newT = this.generateTarget();
639
- this.targets.set(newT.id, newT);
640
  this.addEventLog(`์ƒˆ ํƒ€๊ฒŸ ๋“ฑ์žฅ: ${newT.id}`);
641
  }
642
- // 10% ํ™•๋ฅ ๋กœ ํƒ€๊ฒŸ ํ•˜๋‚˜ ์ œ๊ฑฐ
643
  if (Math.random() < 0.1 && this.targets.size > 0) {
644
- const firstID = Array.from(this.targets.keys())[0];
645
- this.removeTarget(firstID);
 
646
  }
647
 
648
- // ๋ชจ๋“  ํƒ€๊ฒŸ ์ด๋™
649
- this.targets.forEach(t => {
650
- this.moveTarget(t);
651
- });
652
 
653
- // ์ง€๋„ ๊ฐฑ์‹ 
654
- this.updateTargetsOnMap();
655
- // ์‚ฌ์ด๋“œ๋ฐ” Detections
656
  this.updateDetections();
657
- // ์ˆ˜์‹ ๊ธฐ ์‹ ํ˜ธ๊ฐ•๋„
658
  this.updateSignalStrengths();
659
  }, 100);
660
  }
661
  }
662
 
663
- // ํŽ˜์ด์ง€ ๋กœ๋“œ ํ›„ ๋ ˆ์ด๋” ์‹œ์Šคํ…œ ์‹œ์ž‘
664
- window.addEventListener('load', () => {
665
- new RadarSystem();
666
- });
667
  </script>
668
  </body>
669
  </html>
 
2
  <html>
3
  <head>
4
  <meta charset="UTF-8">
5
+ <title>Real SDR Network Monitor - Extended Events</title>
 
 
6
  <style>
7
+ /* ๊ธฐ๋ณธ ์Šคํƒ€์ผ */
8
  body {
9
  margin: 0;
10
  padding: 20px;
 
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
+ /* ์‹ค์‹œ๊ฐ„ ํƒ์ง€ ๋ชฉ๋ก */
80
  .detection {
81
  padding: 5px;
82
  margin: 5px 0;
 
84
  border-left: 2px solid #0f0;
85
  }
86
 
87
+ /* ์ด๋ฒคํŠธ ๋กœ๊ทธ (ํญํ’, ์žฌ๋ฐ ๋“ฑ) */
88
+ .events-area {
89
+ margin-top: 15px;
90
+ }
91
  .alert {
92
+ background: #511;
 
 
93
  border-left: 2px solid #f00;
94
+ margin: 5px 0;
95
+ padding: 5px;
96
  color: #f66;
97
  }
98
 
99
+ /* ์บ”๋ฒ„์Šค ๋ผ์ธ(์‹ ํ˜ธ์„ ) ๋“ฑ */
100
+ .signal-line {
101
+ position: absolute;
102
+ background: linear-gradient(90deg, rgba(0,255,0,0.2) 0%, rgba(0,255,0,0) 100%);
103
+ height: 1px;
104
+ transform-origin: 0 0;
105
+ pointer-events: none;
106
+ opacity: 0.5;
 
 
 
 
107
  }
108
  </style>
109
  </head>
 
111
  <div class="container">
112
  <!-- ์‚ฌ์ด๋“œ๋ฐ” -->
113
  <div class="sidebar">
114
+ <h3>Active SDR Receivers</h3>
 
 
115
  <div id="receivers"></div>
116
 
117
  <h3>Real-time Detections</h3>
118
  <div id="detections"></div>
119
+
120
  <h3>Events</h3>
121
+ <div class="events-area" id="events"></div>
122
  </div>
123
 
124
+ <!-- Canvas ์ง€๋„ -->
125
+ <canvas id="map"></canvas>
126
  </div>
127
 
 
 
128
  <script>
129
+ // 4๊ฐœ ์˜ˆ์‹œ WebSDR ์Šคํ…Œ์ด์…˜ (์›ํ•˜์‹œ๋ฉด ์ถ”๊ฐ€ ๊ฐ€๋Šฅ)
130
  const sdrStations = [
 
131
  {
132
  name: "Twente WebSDR",
133
  url: "websdr.ewi.utwente.nl:8901",
 
159
  frequency: "0-30 MHz",
160
  range: 160,
161
  active: true
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  }
163
  ];
164
 
 
165
  class RadarSystem {
166
  constructor() {
167
+ this.canvas = document.getElementById('map');
168
+ this.ctx = this.canvas.getContext('2d');
169
+
170
+ // ํญํ’, ์žฌ๋ฐ ๋“ฑ ์ด๋ฒคํŠธ ์ƒํƒœ
171
  this.stormActive = false;
172
+ this.jammingActive = false;
173
+
174
+ // ํญํ’, ์žฌ๋ฐ์˜ ์ค‘์‹ฌ์ขŒํ‘œ (51.5,5 ๊ทผ๋ฐฉ)
175
+ this.stormCenter = { lat: 51.5, lon: 5.0 };
176
+ this.stormRadius = 150; // ํญํ’ ๋ฐ˜๊ฒฝ(px ๋‹จ์œ„๋กœ ๊ฐ€์ •)
177
+ this.jamCenter = { lat: 51.5, lon: 5.0 };
178
+ this.jamRadius = 100; // ์žฌ๋ฐ ๋ฐ˜๊ฒฝ(px)
179
+
180
+ // ์ด๋ฒคํŠธ ๋กœ๊ทธ
181
+ this.events = [];
182
+
183
+ // ์ƒ์„ฑ๋œ ํƒ€๊ฒŸ(ํ‘œ์ )
184
+ this.targets = new Set();
185
+
186
+ this.setupCanvas();
 
187
  this.renderReceivers();
188
  this.startTracking();
189
  }
190
 
191
+ // ์บ”๋ฒ„์Šค ์ดˆ๊ธฐ ์„ค์ •
192
+ setupCanvas() {
193
+ this.canvas.width = this.canvas.offsetWidth;
194
+ this.canvas.height = this.canvas.offsetHeight;
 
 
 
 
 
 
 
 
 
195
 
196
+ window.addEventListener('resize', () => {
197
+ this.canvas.width = this.canvas.offsetWidth;
198
+ this.canvas.height = this.canvas.offsetHeight;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  });
200
  }
201
 
202
+ // ์‚ฌ์ด๋“œ๋ฐ” Receivers ๋ Œ๋”๋ง
203
  renderReceivers() {
204
  const container = document.getElementById('receivers');
205
+ container.innerHTML = sdrStations.map(station => `
206
+ <div class="receiver" id="rx-${station.url.split(':')[0]}">
207
  <div class="status">
208
+ <div class="led ${station.active ? 'active' : 'inactive'}"></div>
209
+ <strong>${station.name}</strong>
210
  </div>
211
+ <div>๐Ÿ“ก ${station.url}</div>
212
+ <div>๐Ÿ“ป ${station.frequency}</div>
213
+ <div>๐Ÿ“ ${station.location.join(', ')}</div>
214
+ <div>Range: ${station.range}km</div>
215
  <div class="signal-strength">
216
  <div class="signal-bar"></div>
217
  </div>
 
219
  `).join('');
220
  }
221
 
222
+ // ์ง€๋„(์บ”๋ฒ„์Šค)์˜ ์ค‘์‹ฌ(51.5, 5.0) ๊ธฐ์ค€์œผ๋กœ lat/lon -> x,y ๋ณ€ํ™˜
223
+ latLongToXY(lat, lon) {
224
+ const centerLat = 51.5;
225
+ const centerLon = 5.0;
226
+ const scale = 100; // 1๋„ ๋‹น 100px ์ •๋„
227
 
228
+ const x = (lon - centerLon) * scale + this.canvas.width / 2;
229
+ const y = (centerLat - lat) * scale + this.canvas.height / 2;
230
+ return { x, y };
 
231
  }
232
 
233
+ // ์ด๋ฒคํŠธ ๋กœ๊ทธ ์ถ”๊ฐ€
234
+ addEventLog(msg) {
235
+ this.events.push(msg);
236
+ const evDiv = document.getElementById('events');
237
+ evDiv.innerHTML += `<div class="alert">${msg}</div>`;
238
+
239
+ // ๋„ˆ๋ฌด ๋งŽ์œผ๋ฉด ์˜ค๋ž˜๋œ ๊ฒƒ ์‚ญ์ œ (์ตœ๋Œ€ 20๊ฐœ ์œ ์ง€)
240
+ if (this.events.length > 20) {
241
+ this.events.shift();
242
+ evDiv.removeChild(evDiv.firstChild);
 
 
 
 
 
 
 
243
  }
244
  }
245
 
246
+ // ๋žœ๋ค ํƒ€๊ฒŸ ์ƒ์„ฑ
247
  generateTarget() {
248
+ // 2๋„ ๋ฒ”์œ„(์•ฝ 200px ์˜์—ญ) ๋‚ด
249
+ const lat = 51.5 + (Math.random() - 0.5)*4;
250
+ const lon = 5.0 + (Math.random() - 0.5)*8;
251
+
252
  return {
 
253
  type: (Math.random() > 0.7) ? 'aircraft' : 'vehicle',
254
+ position: {
255
+ lat,
256
+ lon
257
+ },
258
+ speed: Math.random()*400 + 100, // kts
259
+ altitude: Math.random()*30000 + 1000,
260
  heading: Math.random()*360,
261
+ id: Math.random().toString(36).substr(2, 6).toUpperCase(),
262
  signalStrength: Math.random()
263
  };
264
  }
265
 
266
+ // ํƒ€๊ฒŸ ์ด๋™ (heading, speed ๊ธฐ๋ฐ˜)
267
+ moveTarget(target) {
268
+ // heading(0=๋ถ,90=๋™,180=๋‚จ,270=์„œ) - ๋‹จ์ˆœ ์ฒ˜๋ฆฌ
269
+ const speedFactor = 0.00005;
270
+ const rad = target.heading * Math.PI / 180;
271
+ target.position.lat += Math.cos(rad) * target.speed * speedFactor;
272
+ target.position.lon += Math.sin(rad) * target.speed * speedFactor;
 
 
273
  }
274
 
275
+ // ํญํ’, ์žฌ๋ฐ ๋“ฑ ํ‘œ์‹œ / ํšจ๊ณผ
276
+ drawSpecialEvents() {
277
+ // ํญํ’ ํ‘œ์‹œ
 
 
 
 
 
278
  if (this.stormActive) {
279
+ const sc = this.latLongToXY(this.stormCenter.lat, this.stormCenter.lon);
280
+ this.ctx.beginPath();
281
+ this.ctx.arc(sc.x, sc.y, this.stormRadius, 0, Math.PI*2);
282
+ this.ctx.fillStyle = 'rgba(255,0,0,0.1)';
283
+ this.ctx.fill();
284
+ this.ctx.strokeStyle = 'rgba(255,0,0,0.5)';
285
+ this.ctx.stroke();
286
+ }
287
+
288
+ // ์žฌ๋ฐ ํ‘œ์‹œ
289
+ if (this.jammingActive) {
290
+ const jc = this.latLongToXY(this.jamCenter.lat, this.jamCenter.lon);
291
+ this.ctx.beginPath();
292
+ this.ctx.arc(jc.x, jc.y, this.jamRadius, 0, Math.PI*2);
293
+ this.ctx.fillStyle = 'rgba(255,255,0,0.1)';
294
+ this.ctx.fill();
295
+ this.ctx.strokeStyle = 'rgba(255,255,0,0.5)';
296
+ this.ctx.stroke();
297
  }
298
  }
299
 
300
+ // ๋ฐฐ๊ฒฝ(๊ฒฉ์ž) + ์Šคํ…Œ์ด์…˜ ํ‘œ์‹œ
301
+ drawBackgroundAndStations() {
302
+ // ๋ฐฐ๊ฒฝ
303
+ this.ctx.fillStyle = '#111';
304
+ this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
305
+
306
+ // ๊ทธ๋ฆฌ๋“œ
307
+ this.ctx.strokeStyle = '#1a1a1a';
308
+ this.ctx.lineWidth = 1;
309
+ for (let i=0; i<this.canvas.width; i+=50) {
310
+ this.ctx.beginPath();
311
+ this.ctx.moveTo(i, 0);
312
+ this.ctx.lineTo(i, this.canvas.height);
313
+ this.ctx.stroke();
314
+ }
315
+ for (let i=0; i<this.canvas.height; i+=50) {
316
+ this.ctx.beginPath();
317
+ this.ctx.moveTo(0, i);
318
+ this.ctx.lineTo(this.canvas.width, i);
319
+ this.ctx.stroke();
320
+ }
321
+
322
+ // ์Šคํ…Œ์ด์…˜
323
+ sdrStations.forEach(st => {
324
+ const pos = this.latLongToXY(st.location[0], st.location[1]);
325
+
326
+ // ๋ฒ”์œ„ ์›
327
+ this.ctx.beginPath();
328
+ this.ctx.arc(pos.x, pos.y, st.range, 0, Math.PI*2);
329
+ this.ctx.strokeStyle = st.active ? 'rgba(0,255,0,0.2)' : 'rgba(255,0,0,0.2)';
330
+ this.ctx.stroke();
331
+
332
+ // ์Šคํ…Œ์ด์…˜ ์ 
333
+ this.ctx.beginPath();
334
+ this.ctx.arc(pos.x, pos.y, 4, 0, Math.PI*2);
335
+ this.ctx.fillStyle = st.active ? '#0f0' : '#f00';
336
+ this.ctx.fill();
337
+
338
+ // ์ด๋ฆ„
339
+ this.ctx.fillStyle = '#0f0';
340
+ this.ctx.font = '10px monospace';
341
+ this.ctx.fillText(st.name, pos.x+10, pos.y+4);
342
  });
343
+ }
344
+
345
+ // ํƒ€๊ฒŸ + ์—ฐ๊ฒฐ์„  ๊ทธ๋ฆฌ๊ธฐ
346
+ drawTargets() {
347
+ this.targets.forEach(target => {
348
+ const pos = this.latLongToXY(target.position.lat, target.position.lon);
349
+
350
+ // ์ด๋™
351
+ this.moveTarget(target);
352
+
353
+ // ํญํ’ ๋ฒ”์œ„ ์•ˆ์— ์žˆ์œผ๋ฉด ์‹ ํ˜ธ๊ฐ์†Œ
354
+ if (this.stormActive) {
355
+ const sc = this.latLongToXY(this.stormCenter.lat, this.stormCenter.lon);
356
+ const dist = Math.hypot(pos.x - sc.x, pos.y - sc.y);
357
+ if (dist <= this.stormRadius) {
358
+ target.signalStrength = Math.max(0, target.signalStrength - 0.01);
359
+ }
 
 
 
 
 
 
 
360
  }
361
 
362
+ // ์žฌ๋ฐ ๋ฒ”์œ„ ์•ˆ์— ์žˆ์œผ๋ฉด ์‹ ํ˜ธ๊ฐ์†Œ
363
+ if (this.jammingActive) {
364
+ const jc = this.latLongToXY(this.jamCenter.lat, this.jamCenter.lon);
365
+ const dist = Math.hypot(pos.x - jc.x, pos.y - jc.y);
366
+ if (dist <= this.jamRadius) {
367
+ target.signalStrength = Math.max(0, target.signalStrength - 0.01);
368
+ }
369
+ }
370
+
371
+ // ์Šคํ…Œ์ด์…˜๊ณผ์˜ ์—ฐ๊ฒฐ์„ 
372
  sdrStations.forEach(st => {
373
  if (st.active) {
374
+ // ๊ฑฐ๋ฆฌ(ํ”ฝ์…€)๋กœ ๋‹จ์ˆœ ๋น„๊ต
375
+ const stPos = this.latLongToXY(st.location[0], st.location[1]);
376
+ const dx = pos.x - stPos.x;
377
+ const dy = pos.y - stPos.y;
378
+ const distPx = Math.hypot(dx, dy);
379
+ if (distPx <= st.range) {
380
+ this.ctx.beginPath();
381
+ this.ctx.moveTo(stPos.x, stPos.y);
382
+ this.ctx.lineTo(pos.x, pos.y);
383
+ this.ctx.strokeStyle = `rgba(0,255,0,${target.signalStrength * 0.3})`;
384
+ this.ctx.stroke();
 
385
  }
386
  }
387
  });
 
 
388
 
389
+ // ํƒ€๊ฒŸ ์ 
390
+ this.ctx.beginPath();
391
+ this.ctx.arc(pos.x, pos.y, 3, 0, Math.PI*2);
392
+ this.ctx.fillStyle = (target.type === 'aircraft') ? '#ff0' : '#0ff';
393
+ this.ctx.fill();
394
+
395
+ // ํƒ€๊ฒŸ ์ •๋ณด
396
+ this.ctx.fillStyle = '#666';
397
+ this.ctx.font = '10px monospace';
398
+ const info = (target.type==='aircraft')
399
+ ? `${target.id} โ€ข ${target.speed.toFixed(0)}kts โ€ข ${target.altitude.toFixed(0)}ft`
400
+ : `${target.id} โ€ข ${target.speed.toFixed(0)}kts`;
401
+ this.ctx.fillText(info, pos.x+10, pos.y+4);
402
+ });
403
  }
404
 
405
+ // ์šฐ์ธก 'Real-time Detections' ๋ชฉ๋ก ์—…๋ฐ์ดํŠธ
406
  updateDetections() {
407
+ const detections = document.getElementById('detections');
408
+ detections.innerHTML = Array.from(this.targets)
409
+ .map(t => `
410
+ <div class="detection">
411
+ ${t.type==='aircraft' ? 'โœˆ๏ธ' : '๐Ÿš—'}
412
+ ${t.id}
413
+ ${t.type==='aircraft' ? `${t.altitude.toFixed(0)}ft ` : ''}
414
+ ${t.speed.toFixed(0)}kts
415
+ Sig: ${(t.signalStrength*100).toFixed(0)}%
416
+ </div>
417
+ `).join('');
418
  }
419
 
420
+ // Receivers ์‹ ํ˜ธ ๋ฐ” ์—…๋ฐ์ดํŠธ
421
  updateSignalStrengths() {
422
  sdrStations.forEach(st => {
423
  const bar = document.querySelector(`#rx-${st.url.split(':')[0]} .signal-bar`);
424
  if (bar) {
425
+ const strength = 40 + Math.random() * 60;
426
  bar.style.width = `${strength}%`;
427
  }
428
  });
429
  }
430
 
431
+ // ์ด๋ฒคํŠธ(ํญํ’, ์žฌ๋ฐ) ํ† ๊ธ€
432
+ toggleStorm() {
433
+ this.stormActive = !this.stormActive;
434
+ this.addEventLog(this.stormActive ? "ํญํ’ ๋ฐœ์ƒ! ์‹ ํ˜ธ ๊ต๋ž€ ์šฐ๋ ค" : "ํญํ’ ์†Œ๋ฉธ, ์ •์ƒํ™”");
435
+ }
436
+
437
+ toggleJamming() {
438
+ this.jammingActive = !this.jammingActive;
439
+ this.addEventLog(this.jammingActive ? "์žฌ๋ฐ(Jamming) ์‹œ์ž‘!" : "์žฌ๋ฐ ์ข…๋ฃŒ");
 
 
 
 
 
 
 
 
 
 
 
440
  }
441
 
442
  // ๋ฉ”์ธ ๋ฃจํ”„
443
  startTracking() {
444
+ // ์ผ์ • ๊ฐ„๊ฒฉ(10์ดˆ)์œผ๋กœ ํญํ’ยท์žฌ๋ฐ ํ† ๊ธ€
445
  setInterval(() => {
446
+ if (Math.random() < 0.2) {
447
  this.toggleStorm();
448
  }
449
+ if (Math.random() < 0.2) {
450
+ this.toggleJamming();
451
+ }
452
  }, 10000);
453
 
454
+ // ๋งค 100ms ๋งˆ๋‹ค ํ™”๋ฉด ๊ฐฑ์‹ 
455
  setInterval(() => {
456
+ // 15% ํ™•๋ฅ ๋กœ ์ƒˆ ํƒ€๊ฒŸ ์ถ”๊ฐ€, ์ตœ๋Œ€ 15๊ฐœ
457
+ if (Math.random() < 0.15 && this.targets.size < 15) {
458
  const newT = this.generateTarget();
459
+ this.targets.add(newT);
460
  this.addEventLog(`์ƒˆ ํƒ€๊ฒŸ ๋“ฑ์žฅ: ${newT.id}`);
461
  }
462
+ // 10% ํ™•๋ฅ ๋กœ ํ•˜๋‚˜ ์ œ๊ฑฐ
463
  if (Math.random() < 0.1 && this.targets.size > 0) {
464
+ const first = Array.from(this.targets)[0];
465
+ this.targets.delete(first);
466
+ this.addEventLog(`ํƒ€๊ฒŸ ์†Œ๋ฉธ: ${first.id}`);
467
  }
468
 
469
+ // ๊ทธ๋ฆฌ๊ธฐ ์ˆœ์„œ: ๋ฐฐ๊ฒฝ/๊ทธ๋ฆฌ๋“œ -> ์ด๋ฒคํŠธ์˜์—ญ -> ์Šคํ…Œ์ด์…˜ -> ํƒ€๊ฒŸ
470
+ this.drawBackgroundAndStations();
471
+ this.drawSpecialEvents();
472
+ this.drawTargets();
473
 
474
+ // ์‚ฌ์ด๋“œ๋ฐ” UI ๊ฐฑ์‹ 
 
 
475
  this.updateDetections();
 
476
  this.updateSignalStrengths();
477
  }, 100);
478
  }
479
  }
480
 
481
+ // ์‹คํ–‰
482
+ const radar = new RadarSystem();
 
 
483
  </script>
484
  </body>
485
  </html>