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

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +345 -279
index.html CHANGED
@@ -2,9 +2,11 @@
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,12 +28,29 @@
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,7 +95,7 @@
76
  transition: width 0.3s;
77
  }
78
 
79
- /* ์‹ค์‹œ๊ฐ„ ํƒ์ง€ ๋ชฉ๋ก */
80
  .detection {
81
  padding: 5px;
82
  margin: 5px 0;
@@ -84,26 +103,33 @@
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,107 +137,147 @@
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",
134
- location: [52.2389, 6.8343],
135
- frequency: "0-29.160 MHz",
136
- range: 200,
137
- active: true
138
- },
139
- {
140
- name: "TU Delft WebSDR",
141
- url: "websdr.tudelft.nl:8901",
142
- location: [51.9981, 4.3731],
143
- frequency: "0-29.160 MHz",
144
- range: 180,
145
- active: true
146
- },
147
- {
148
- name: "SUWS WebSDR UK",
149
- url: "websdr.suws.org.uk",
150
- location: [51.2785, -0.7642],
151
- frequency: "0-30 MHz",
152
- range: 150,
153
- active: true
154
- },
155
- {
156
- name: "KiwiSDR Switzerland",
157
- url: "hb9ryz.no-ip.org:8073",
158
- location: [47.3769, 8.5417],
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,231 +285,227 @@
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) {
@@ -451,35 +513,39 @@
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>
 
2
  <html>
3
  <head>
4
  <meta charset="UTF-8">
5
+ <title>Hyperscan: Global SDR Radar(Simul) - Extended Events</title>
6
+ <!-- Leaflet CSS -->
7
+ <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
8
  <style>
9
+ /* ===== ๊ณตํ†ต ์Šคํƒ€์ผ ===== */
10
  body {
11
  margin: 0;
12
  padding: 20px;
 
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
  transition: width 0.3s;
96
  }
97
 
98
+ /* ===== ์‹ค์‹œ๊ฐ„ ํƒ์ง€ ๋ชฉ๋ก ===== */
99
  .detection {
100
  padding: 5px;
101
  margin: 5px 0;
 
103
  border-left: 2px solid #0f0;
104
  }
105
 
106
+ /* ===== ์ด๋ฒคํŠธ ๋กœ๊ทธ ===== */
 
 
 
107
  .alert {
108
+ background: #611;
109
  border-left: 2px solid #f00;
110
  margin: 5px 0;
111
  padding: 5px;
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
+ .jam-range {
129
+ stroke: #ff0;
130
+ stroke-width: 1;
131
+ fill: #ff0;
132
+ fill-opacity: 0.1;
133
  }
134
  </style>
135
  </head>
 
137
  <div class="container">
138
  <!-- ์‚ฌ์ด๋“œ๋ฐ” -->
139
  <div class="sidebar">
140
+ <h2>Hyperscan: Global SDR Radar(Simul)</h2>
141
+
142
+ <h3>SDR Receivers</h3>
143
  <div id="receivers"></div>
144
 
145
  <h3>Real-time Detections</h3>
146
  <div id="detections"></div>
147
 
148
  <h3>Events</h3>
149
+ <div id="events"></div>
150
  </div>
151
 
152
+ <!-- Leaflet ์ง€๋„ ์˜์—ญ -->
153
+ <div id="map"></div>
154
  </div>
155
 
156
+ <!-- Leaflet JS -->
157
+ <script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
158
  <script>
159
+ // 23๊ฐœ ์ „ ์„ธ๊ณ„ SDR ์Šคํ…Œ์ด์…˜
160
  const sdrStations = [
161
+ // Europe
162
+ { name: "Twente WebSDR", url: "websdr.ewi.utwente.nl:8901", location: [52.2389, 6.8343], frequency: "0-29.160 MHz", range: 200, active: true },
163
+ { name: "TU Delft WebSDR", url: "websdr.tudelft.nl:8901", location: [51.9981, 4.3731], frequency: "0-29.160 MHz", range: 180, active: true },
164
+ { name: "SUWS WebSDR UK", url: "websdr.suws.org.uk", location: [51.2785, -0.7642], frequency: "0-30 MHz", range: 150, active: true },
165
+ { name: "KiwiSDR Switzerland", url: "hb9ryz.no-ip.org:8073", location: [47.3769, 8.5417], frequency: "0-30 MHz", range: 160, active: true },
166
+ // United States
167
+ { name: "W6DRZ WebSDR", url: "w6drz.sdr.us:8901", location: [34.2847, -118.4429], frequency: "0-30 MHz", range: 170, active: true },
168
+ { name: "K3FEF WebSDR", url: "k3fef.sdr.us:8901", location: [40.5697, -75.9363], frequency: "0-30 MHz", range: 160, active: true },
169
+ { name: "WA2ZKD KiwiSDR", url: "wa2zkd.sdr.us:8073", location: [40.7128, -74.0060], frequency: "0-30 MHz", range: 150, active: true },
170
+ { name: "W4AX WebSDR", url: "w4ax.sdr.us:8901", location: [33.7756, -84.3963], frequency: "0-30 MHz", range: 165, active: true },
171
+ // Japan
172
+ { name: "JH7VHZ WebSDR", url: "jh7vhz.sdr.jp:8901", location: [38.2682, 140.8694], frequency: "0-30 MHz", range: 155, active: true },
173
+ { name: "JA1GJB KiwiSDR", url: "ja1gjb.sdr.jp:8073", location: [35.6762, 139.6503], frequency: "0-30 MHz", range: 145, active: true },
174
+ { name: "JA3ZOH WebSDR", url: "ja3zoh.sdr.jp:8901", location: [34.6937, 135.5023], frequency: "0-30 MHz", range: 150, active: true },
175
+ // Australia
176
+ { name: "VK4YA KiwiSDR", url: "vk4ya.sdr.au:8073", location: [-27.4698, 153.0251], frequency: "0-30 MHz", range: 170, active: true },
177
+ { name: "VK2RG WebSDR", url: "vk2rg.sdr.au:8901", location: [-33.8688, 151.2093], frequency: "0-30 MHz", range: 165, active: true },
178
+ // Russia
179
+ { name: "RZ3DJR WebSDR", url: "rz3djr.sdr.ru:8901", location: [55.7558, 37.6173], frequency: "0-30 MHz", range: 180, active: true },
180
+ { name: "UA9UDX WebSDR", url: "ua9udx.sdr.ru:8901", location: [55.0084, 82.9357], frequency: "0-30 MHz", range: 175, active: true },
181
+ // China
182
+ { name: "BY1PK WebSDR", url: "by1pk.sdr.cn:8901", location: [39.9042, 116.4074], frequency: "0-30 MHz", range: 160, active: true },
183
+ { name: "BG3MDO KiwiSDR", url: "bg3mdo.sdr.cn:8073", location: [23.1291, 113.2644], frequency: "0-30 MHz", range: 155, active: true },
184
+ // South Korea
185
+ { name: "HL2WA KiwiSDR", url: "hl2wa.sdr.kr:8073", location: [37.5665, 126.9780], frequency: "0-30 MHz", range: 150, active: true },
186
+ { name: "DS1URB WebSDR", url: "ds1urb.sdr.kr:8901", location: [35.1796, 129.0756], frequency: "0-30 MHz", range: 145, active: true },
187
+ // Canada
188
+ { name: "VE3HOA WebSDR", url: "ve3hoa.sdr.ca:8901", location: [43.6532, -79.3832], frequency: "0-30 MHz", range: 165, active: true },
189
+ { name: "VA3ROM KiwiSDR", url: "va3rom.sdr.ca:8073", location: [45.4215, -75.6972], frequency: "0-30 MHz", range: 160, active: true },
190
+ // Brazil
191
+ { name: "PY2RDZ WebSDR", url: "py2rdz.sdr.br:8901", location: [-23.5505, -46.6333], frequency: "0-30 MHz", range: 170, active: true },
192
+ { name: "PY1ZV KiwiSDR", url: "py1zv.sdr.br:8073", location: [-22.9068, -43.1729], frequency: "0-30 MHz", range: 165, active: true }
193
  ];
194
 
195
+ // ํƒ€๊ฒŸ(ํ‘œ์ ) ๊ตฌ์กฐ:
196
+ // { id, type:'aircraft'|'vehicle', lat, lon, heading, speed, altitude, signalStrength }
197
+
198
  class RadarSystem {
199
  constructor() {
200
+ // ํญํ’/์žฌ๋ฐ ์ƒํƒœ
 
 
 
201
  this.stormActive = false;
202
+ this.jamActive = false;
203
+
204
+ // ํญํ’/์žฌ๋ฐ ์ค‘์‹ฌ (์œ ๋Ÿฝ ์ชฝ)
205
+ this.stormCenter = [50.5, 5.0];
206
+ this.stormRadius = 500; // km
207
+ this.jamCenter = [52.0, 5.0];
208
+ this.jamRadius = 300; // km
209
 
210
+ // ์ด๋ฒคํŠธ ๋กœ๊ทธ (์ตœ๋Œ€ 30๊ฐœ)
211
+ this.eventsLog = [];
 
 
 
212
 
213
+ // ํƒ€๊ฒŸ ๋ชฉ๋ก
214
+ this.targets = new Map(); // key: targetId, value: object
215
 
216
+ // Leaflet ์š”์†Œ
217
+ this.map = null;
218
+ this.targetMarkers = new Map();
219
+ this.targetSignalLines = new Map();
220
 
221
+ // ํญํ’/์žฌ๋ฐ Circle
222
+ this.stormCircle = null;
223
+ this.jamCircle = null;
224
+
225
+ this.initMap();
226
  this.renderReceivers();
227
  this.startTracking();
228
  }
229
 
230
+ // ์ง€๋„ ์ดˆ๊ธฐํ™”
231
+ initMap() {
232
+ this.map = L.map('map', {
233
+ center: [20, 0], // ์ „ ์„ธ๊ณ„๊ฐ€ ๋ณด์ด๋„๋ก
234
+ zoom: 3,
235
+ worldCopyJump: true
236
+ });
237
 
238
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
239
+ maxZoom: 19,
240
+ attribution: 'ยฉ OpenStreetMap contributors'
241
+ }).addTo(this.map);
242
+
243
+ // ์Šคํ…Œ์ด์…˜ ํ‘œ์‹œ
244
+ sdrStations.forEach(st => {
245
+ // ๋ฒ”์œ„ ์› (leaflet circle)
246
+ L.circle(st.location, {
247
+ radius: st.range * 1000,
248
+ className: 'station-range'
249
+ }).addTo(this.map);
250
+
251
+ // ๋งˆ์ปค
252
+ const marker = L.circleMarker(st.location, {
253
+ radius: 5,
254
+ color: '#0f0',
255
+ fillColor: '#0f0',
256
+ fillOpacity: 1
257
+ }).addTo(this.map);
258
+
259
+ // ํˆดํŒ
260
+ marker.bindTooltip(`
261
+ <b>${st.name}</b><br/>
262
+ Frequency: ${st.frequency}<br/>
263
+ Range: ${st.range} km
264
+ `);
265
  });
266
  }
267
 
268
+ // ์‚ฌ์ด๋“œ๋ฐ” Receivers ํ‘œ์‹œ
269
  renderReceivers() {
270
  const container = document.getElementById('receivers');
271
+ container.innerHTML = sdrStations.map(st => `
272
+ <div class="receiver" id="rx-${st.url.split(':')[0]}">
273
  <div class="status">
274
+ <div class="led ${st.active ? 'active' : 'inactive'}"></div>
275
+ <strong>${st.name}</strong>
276
  </div>
277
+ <div>๐Ÿ“ก ${st.url}</div>
278
+ <div>๐Ÿ“ป ${st.frequency}</div>
279
+ <div>๐Ÿ“ ${st.location.join(', ')}</div>
280
+ <div>Range: ${st.range}km</div>
281
  <div class="signal-strength">
282
  <div class="signal-bar"></div>
283
  </div>
 
285
  `).join('');
286
  }
287
 
288
+ // ์ด๋ฒคํŠธ ๋กœ๊ทธ ์ถ”๊ฐ€ (์ตœ๋Œ€ 30๊ฐœ)
 
 
 
 
 
 
 
 
 
 
 
289
  addEventLog(msg) {
290
+ this.eventsLog.push(msg);
291
  const evDiv = document.getElementById('events');
292
  evDiv.innerHTML += `<div class="alert">${msg}</div>`;
293
 
294
+ if (this.eventsLog.length > 30) {
295
+ this.eventsLog.shift();
 
296
  evDiv.removeChild(evDiv.firstChild);
297
  }
298
  }
299
 
300
+ // ํญํ’ ํ† ๊ธ€
301
+ toggleStorm() {
302
+ this.stormActive = !this.stormActive;
303
+ const msg = this.stormActive ? "ํญํ’ ๋ฐœ์ƒ! (๊ต๋ž€ ๊ฐ€๋Šฅ)" : "ํญํ’์ด ์†Œ๋ฉธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.";
304
+ this.addEventLog(msg);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
305
 
306
+ // ๊ธฐ์กด ์Šคํ†ฐ ์„œํด ์ œ๊ฑฐ
307
+ if (this.stormCircle) {
308
+ this.map.removeLayer(this.stormCircle);
309
+ this.stormCircle = null;
 
 
 
 
 
 
 
310
  }
311
+ if (this.stormActive) {
312
+ this.stormCircle = L.circle(this.stormCenter, {
313
+ radius: this.stormRadius * 1000,
314
+ className: 'storm-range'
315
+ }).addTo(this.map);
 
 
 
 
 
316
  }
317
  }
318
 
319
+ // ์žฌ๋ฐ(Jamming) ํ† ๊ธ€
320
+ toggleJamming() {
321
+ this.jamActive = !this.jamActive;
322
+ const msg = this.jamActive ? "์žฌ๋ฐ(Jamming) ์‹œ์ž‘!" : "์žฌ๋ฐ์ด ์ข…๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.";
323
+ this.addEventLog(msg);
324
+
325
+ // ๊ธฐ์กด ์žฌ๋ฐ ์„œํด ์ œ๊ฑฐ
326
+ if (this.jamCircle) {
327
+ this.map.removeLayer(this.jamCircle);
328
+ this.jamCircle = null;
 
 
 
 
329
  }
330
+ if (this.jamActive) {
331
+ this.jamCircle = L.circle(this.jamCenter, {
332
+ radius: this.jamRadius * 1000,
333
+ className: 'jam-range'
334
+ }).addTo(this.map);
335
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
  }
337
 
338
+ // ๋‘ ์ขŒํ‘œ๊ฐ„ ๊ฑฐ๋ฆฌ (km) haversine
339
+ getDistanceKm(lat1, lon1, lat2, lon2) {
340
+ const R = 6371;
341
+ const dLat = (lat2 - lat1)*Math.PI/180;
342
+ const dLon = (lon2 - lon1)*Math.PI/180;
343
+ const a = Math.sin(dLat/2)*Math.sin(dLat/2)
344
+ + Math.cos(lat1*Math.PI/180)*Math.cos(lat2*Math.PI/180)
345
+ * Math.sin(dLon/2)*Math.sin(dLon/2);
346
+ return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
347
+ }
348
 
349
+ // ํƒ€๊ฒŸ ์ด๋™
350
+ moveTarget(t) {
351
+ // heading+speed => lat/lon ๋ณ€ํ™” (๋‹จ์ˆœ ํ™˜์‚ฐ)
352
+ const speedFactor = 0.00005;
353
+ const rad = t.heading * Math.PI/180;
354
+ t.lat += Math.cos(rad)*t.speed*speedFactor;
355
+ t.lon += Math.sin(rad)*t.speed*speedFactor;
356
 
357
+ // ํญํ’ ๋ฒ”์œ„ ์•ˆ์ด๋ฉด ์‹ ํ˜ธ๊ฐ•๋„ ๊ฐ์†Œ
358
+ if (this.stormActive) {
359
+ const distStorm = this.getDistanceKm(t.lat, t.lon, this.stormCenter[0], this.stormCenter[1]);
360
+ if (distStorm <= this.stormRadius) {
361
+ t.signalStrength = Math.max(0, t.signalStrength - 0.01);
 
 
362
  }
363
+ }
364
+ // ์žฌ๋ฐ ๋ฒ”์œ„ ์•ˆ์ด๋ฉด ์‹ ํ˜ธ๊ฐ•๋„ ๊ฐ์†Œ
365
+ if (this.jamActive) {
366
+ const distJam = this.getDistanceKm(t.lat, t.lon, this.jamCenter[0], this.jamCenter[1]);
367
+ if (distJam <= this.jamRadius) {
368
+ t.signalStrength = Math.max(0, t.signalStrength - 0.01);
369
+ }
370
+ }
371
+ }
372
 
373
+ // ํƒ€๊ฒŸ ๋งˆ์ปค/์—ฐ๊ฒฐ์„  ์ง€๋„ ๊ฐฑ์‹ 
374
+ updateTargetsOnMap() {
375
+ // ๊ธฐ์กด ์—ฐ๊ฒฐ์„  ์ œ๊ฑฐ
376
+ this.targetSignalLines.forEach(line => this.map.removeLayer(line));
377
+ this.targetSignalLines.clear();
378
+
379
+ // ํƒ€๊ฒŸ๋ณ„ ๋งˆ์ปค ์ƒ์„ฑ/์—…๋ฐ์ดํŠธ
380
+ this.targets.forEach((t, id) => {
381
+ // ๋งˆ์ปค
382
+ let marker = this.targetMarkers.get(id);
383
+ if (!marker) {
384
+ // ์ƒˆ ๋งˆ์ปค ์ƒ์„ฑ
385
+ marker = L.circleMarker([t.lat, t.lon], {
386
+ radius: 4,
387
+ color: (t.type==='aircraft') ? '#ff0' : '#0ff',
388
+ fillColor: (t.type==='aircraft') ? '#ff0' : '#0ff',
389
+ fillOpacity: 1
390
+ }).addTo(this.map);
391
+ this.targetMarkers.set(id, marker);
392
+ } else {
393
+ // ์ด๋ฏธ ์žˆ์œผ๋ฉด ์ขŒํ‘œ/์ƒ‰์ƒ ๊ฐฑ์‹ 
394
+ marker.setLatLng([t.lat, t.lon]);
395
+ marker.setStyle({
396
+ color: (t.type==='aircraft') ? '#ff0' : '#0ff',
397
+ fillColor: (t.type==='aircraft') ? '#ff0' : '#0ff'
398
+ });
399
  }
400
 
401
+ // ํˆดํŒ
402
+ marker.bindTooltip(this.makeTooltipHTML(t), { sticky: true });
403
+
404
+ // ์Šคํ…Œ์ด์…˜ ๋ฒ”์œ„ ๋‚ด๋ฉด ์—ฐ๊ฒฐ์„ 
405
  sdrStations.forEach(st => {
406
  if (st.active) {
407
+ const dist = this.getDistanceKm(t.lat, t.lon, st.location[0], st.location[1]);
408
+ if (dist <= st.range) {
409
+ const line = L.polyline([
410
+ [t.lat, t.lon],
411
+ st.location
412
+ ], {
413
+ color: '#0f0',
414
+ opacity: t.signalStrength*0.3,
415
+ weight: 1
416
+ }).addTo(this.map);
417
+
418
+ this.targetSignalLines.set(`${id}-${st.name}`, line);
419
  }
420
  }
421
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
422
  });
423
  }
424
 
425
+ // ํƒ€๊ฒŸ ํˆดํŒ
426
+ makeTooltipHTML(t) {
427
+ return `
428
+ <b>${t.id}</b><br/>
429
+ Type: ${t.type}<br/>
430
+ Speed: ${t.speed.toFixed(0)} kts<br/>
431
+ ${
432
+ t.type==='aircraft' ? `Alt: ${t.altitude.toFixed(0)} ft<br/>` : ''
433
+ }
434
+ Sig: ${(t.signalStrength*100).toFixed(0)}%
435
+ `;
436
+ }
437
+
438
+ // ์‹ค์‹œ๊ฐ„ Detections ๋ชฉ๋ก
439
  updateDetections() {
440
+ const div = document.getElementById('detections');
441
+ let html = '';
442
+ this.targets.forEach(t => {
443
+ html += `<div class="detection">
444
+ ${t.type==='aircraft'?'โœˆ๏ธ':'๐Ÿš—'}
445
+ ${t.id}
446
+ ${t.type==='aircraft'? `Alt: ${t.altitude.toFixed(0)}ft ` : ''}
447
+ Speed: ${t.speed.toFixed(0)}kts
448
+ Sig: ${(t.signalStrength*100).toFixed(0)}%
449
+ </div>`;
450
+ });
451
+ div.innerHTML = html;
452
  }
453
 
454
+ // Receivers ์‹ ํ˜ธ๊ฐ•๋„ ๋ฐ”(๋ฌด์ž‘์œ„)
455
  updateSignalStrengths() {
456
  sdrStations.forEach(st => {
457
  const bar = document.querySelector(`#rx-${st.url.split(':')[0]} .signal-bar`);
458
  if (bar) {
459
+ const strength = 40 + Math.random()*60;
460
  bar.style.width = `${strength}%`;
461
  }
462
  });
463
  }
464
 
465
+ // ๋ฌด์ž‘์œ„ ํƒ€๊ฒŸ ์ƒ์„ฑ
466
+ generateTarget() {
467
+ const lat = 20 + (Math.random()-0.5)*40; // ยฑ20๋„
468
+ const lon = 0 + (Math.random()-0.5)*80; // ยฑ40๋„
469
+ return {
470
+ id: Math.random().toString(36).substr(2, 6).toUpperCase(),
471
+ type: (Math.random()>0.7) ? 'aircraft' : 'vehicle',
472
+ lat,
473
+ lon,
474
+ heading: Math.random()*360,
475
+ speed: Math.random()*200+100,
476
+ altitude: Math.random()*30000+5000,
477
+ signalStrength: Math.random()
478
+ };
479
  }
480
 
481
+ // ํƒ€๊ฒŸ ์ œ๊ฑฐ
482
+ removeTarget(id) {
483
+ const t = this.targets.get(id);
484
+ if (!t) return;
485
+ this.targets.delete(id);
486
+ this.addEventLog(`ํƒ€๊ฒŸ ์†Œ๋ฉธ: ${t.id}`);
487
+
488
+ // ๋งˆ์ปค ์ œ๊ฑฐ
489
+ const marker = this.targetMarkers.get(id);
490
+ if (marker) {
491
+ this.map.removeLayer(marker);
492
+ this.targetMarkers.delete(id);
493
+ }
494
+
495
+ // ์—ฐ๊ฒฐ์„  ์ œ๊ฑฐ
496
+ [...this.targetSignalLines.keys()].forEach(k => {
497
+ if (k.includes(id)) {
498
+ this.map.removeLayer(this.targetSignalLines.get(k));
499
+ this.targetSignalLines.delete(k);
500
+ }
501
+ });
502
  }
503
 
504
+ // ๋ฉ”์ธ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ๋ฃจํ”„
505
  startTracking() {
506
+ // 10์ดˆ ๊ฐ„๊ฒฉ์œผ๋กœ ํญํ’/์žฌ๋ฐ 20% ํ™•๋ฅ ๋กœ ํ† ๊ธ€
507
  setInterval(() => {
508
+ if (Math.random() < 0.2) {
509
  this.toggleStorm();
510
  }
511
  if (Math.random() < 0.2) {
 
513
  }
514
  }, 10000);
515
 
516
+ // 100ms ๊ฐ„๊ฒฉ
517
  setInterval(() => {
518
+ // (10% ํ™•๋ฅ ) ์ƒˆ ํƒ€๊ฒŸ ์ถ”๊ฐ€, ์ตœ๋Œ€ 20๊ฐœ
519
+ if (Math.random()<0.1 && this.targets.size<20) {
520
  const newT = this.generateTarget();
521
+ this.targets.set(newT.id, newT);
522
  this.addEventLog(`์ƒˆ ํƒ€๊ฒŸ ๋“ฑ์žฅ: ${newT.id}`);
523
  }
524
+ // (10% ํ™•๋ฅ ) ํƒ€๊ฒŸ ์ œ๊ฑฐ
525
+ if (Math.random()<0.1 && this.targets.size>0) {
526
+ const firstKey = Array.from(this.targets.keys())[0];
527
+ this.removeTarget(firstKey);
 
528
  }
529
 
530
+ // ํƒ€๊ฒŸ ์ด๋™
531
+ this.targets.forEach((t, id) => {
532
+ this.moveTarget(t);
533
+ });
534
 
535
+ // ์ง€๋„ ์—…๋ฐ์ดํŠธ
536
+ this.updateTargetsOnMap();
537
+ // Detections ์—…๋ฐ์ดํŠธ
538
  this.updateDetections();
539
+ // Receivers ์‹ ํ˜ธ ๋ฐ”
540
  this.updateSignalStrengths();
541
  }, 100);
542
  }
543
  }
544
 
545
+ // ํŽ˜์ด์ง€ ๋กœ๋“œ ํ›„ ์‹คํ–‰
546
+ window.addEventListener('load', () => {
547
+ new RadarSystem();
548
+ });
549
  </script>
550
  </body>
551
  </html>