kolaslab commited on
Commit
da78557
ยท
verified ยท
1 Parent(s): 55962fe

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +294 -134
index.html CHANGED
@@ -6,7 +6,7 @@
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;
@@ -27,13 +27,13 @@
27
  height: calc(100vh - 40px);
28
  overflow-y: auto;
29
  }
30
- /* ์ง€๋„ ์˜์—ญ */
31
  #map {
32
  height: calc(100vh - 40px);
33
  border-radius: 8px;
34
  background: #111;
35
  }
36
- /* Leaflet ๋‹คํฌ ํ…Œ๋งˆ */
 
37
  .leaflet-tile-pane {
38
  filter: invert(1) hue-rotate(180deg);
39
  }
@@ -50,7 +50,7 @@
50
  color: #0f0 !important;
51
  }
52
 
53
- /* =============== ์‚ฌ์ด๋“œ๋ฐ” ์ˆ˜์‹ ๊ธฐ ๋ชฉ๋ก =============== */
54
  .receiver {
55
  margin: 10px 0;
56
  padding: 10px;
@@ -95,7 +95,7 @@
95
  transition: width 0.3s;
96
  }
97
 
98
- /* =============== ์‹ค์‹œ๊ฐ„ ํƒ์ง€ ๋ชฉ๋ก =============== */
99
  .detection {
100
  padding: 5px;
101
  margin: 5px 0;
@@ -103,7 +103,7 @@
103
  border-left: 2px solid #0f0;
104
  }
105
 
106
- /* =============== ์ด๋ฒคํŠธ ๋กœ๊ทธ =============== */
107
  .alert {
108
  background: #911;
109
  padding: 5px;
@@ -112,7 +112,7 @@
112
  color: #f66;
113
  }
114
 
115
- /* =============== ํญํ’ / ์Šคํ…Œ์ด์…˜ ๋ฒ”์œ„ ํ‘œ์‹œ =============== */
116
  .station-range {
117
  stroke: #0f0;
118
  stroke-width: 1;
@@ -125,13 +125,11 @@
125
  fill: #f00;
126
  fill-opacity: 0.1;
127
  }
128
-
129
- /* ํƒ€๊ฒŸ ๋งˆ์ปค ์Šคํƒ€์ผ (marker ์ž์ฒด๋Š” circleMarker์˜ ์˜ต์…˜์œผ๋กœ ์ฒ˜๋ฆฌ) */
130
  </style>
131
  </head>
132
  <body>
133
  <div class="container">
134
- <!-- ===== ์‚ฌ์ด๋“œ๋ฐ” ===== -->
135
  <div class="sidebar">
136
  <h2>Hyperscan: Global SDR Radar(Simul)</h2>
137
 
@@ -145,15 +143,16 @@
145
  <div id="events"></div>
146
  </div>
147
 
148
- <!-- ===== Leaflet ์ง€๋„ ===== -->
149
  <div id="map"></div>
150
  </div>
151
 
152
  <!-- Leaflet JS -->
153
  <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
154
  <script>
155
- // ์ „ ์„ธ๊ณ„ SDR ์Šคํ…Œ์ด์…˜ ์˜ˆ์‹œ
156
  const sdrStations = [
 
157
  {
158
  name: "Twente WebSDR",
159
  url: "websdr.ewi.utwente.nl:8901",
@@ -162,6 +161,22 @@
162
  range: 200,
163
  active: true
164
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  {
166
  name: "KiwiSDR Switzerland",
167
  url: "hb9ryz.no-ip.org:8073",
@@ -170,34 +185,186 @@
170
  range: 160,
171
  active: true
172
  },
 
173
  {
174
- name: "SUWS WebSDR UK",
175
- url: "websdr.suws.org.uk",
176
- location: [51.2785, -0.7642],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
  frequency: "0-30 MHz",
178
  range: 150,
179
  active: true
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  }
181
  ];
182
 
183
- // ============== RadarSystem ํด๋ž˜์Šค (Leaflet ๊ธฐ๋ฐ˜) ==============
184
  class RadarSystem {
185
  constructor() {
186
  // ํญํ’ ์ƒํƒœ
187
  this.stormActive = false;
188
- // ํญํ’ ์ค‘์‹ฌ(๋Œ€์ถฉ ์œ ๋Ÿฝ ๊ทผ๋ฐฉ)
189
  this.stormCenter = [50.5, 5.0];
190
  // ํญํ’ ๋ฐ˜๊ฒฝ(km)
191
  this.stormRadius = 200;
192
 
193
- // ํƒ€๊ฒŸ ๋ชฉ๋ก
194
- this.targets = new Map(); // key: targetId, value: { lat, lon, ... }
195
- // ํƒ€๊ฒŸ๋ณ„ marker Layer
196
  this.targetMarkers = new Map();
197
- // ํƒ€๊ฒŸ๋ณ„ signal lines (ํƒ€๊ฒŸ-์Šคํ…Œ์ด์…˜ ์—ฐ๊ฒฐ์„ )
198
  this.targetSignalLines = new Map();
199
 
200
- // ์ด๋ฒคํŠธ ๋กœ๊ทธ
201
  this.eventsLog = [];
202
 
203
  this.initializeMap();
@@ -205,23 +372,23 @@
205
  this.startTracking();
206
  }
207
 
208
- // ===== Leaflet ์ง€๋„ ์ดˆ๊ธฐํ™” =====
209
  initializeMap() {
210
  this.map = L.map('map', {
211
- center: [51.5, 5.0],
212
- zoom: 5,
213
  worldCopyJump: true
214
  });
215
 
216
- // OSM Tile Layer
217
  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
218
  maxZoom: 19,
219
  attribution: 'ยฉ OpenStreetMap contributors'
220
  }).addTo(this.map);
221
 
222
- // ๊ฐ ์Šคํ…Œ์ด์…˜ ํ‘œ์‹œ (๋งˆ์ปค + ๋ฒ”์œ„ ์›)
223
  sdrStations.forEach(st => {
224
- // ๋งˆ์ปค
225
  const stationMarker = L.circleMarker(st.location, {
226
  radius: 5,
227
  color: '#0f0',
@@ -230,7 +397,7 @@
230
  }).addTo(this.map);
231
 
232
  // ๋ฒ”์œ„ ์›
233
- const coverage = L.circle(st.location, {
234
  radius: st.range * 1000,
235
  className: 'station-range'
236
  }).addTo(this.map);
@@ -244,7 +411,7 @@
244
  });
245
  }
246
 
247
- // ===== ์‚ฌ์ด๋“œ๋ฐ” Receivers ํ‘œ์‹œ =====
248
  renderReceivers() {
249
  const container = document.getElementById('receivers');
250
  container.innerHTML = sdrStations.map(st => `
@@ -264,19 +431,19 @@
264
  `).join('');
265
  }
266
 
267
- // ===== ์ด๋ฒคํŠธ ๋กœ๊ทธ ์ถœ๋ ฅ =====
268
  addEventLog(msg) {
269
  this.eventsLog.push(msg);
270
  const eventsDiv = document.getElementById('events');
271
  eventsDiv.innerHTML += `<div class="alert">${msg}</div>`;
272
- // ์˜ค๋ž˜๋œ ๋กœ๊ทธ ์ œ๊ฑฐ
273
- if (this.eventsLog.length > 15) {
274
- this.eventsLog.shift();
275
  eventsDiv.removeChild(eventsDiv.firstChild);
276
  }
277
  }
278
 
279
- // ===== ํญํ’ ํ† ๊ธ€ =====
280
  toggleStorm() {
281
  this.stormActive = !this.stormActive;
282
  const msg = this.stormActive
@@ -284,12 +451,10 @@
284
  : "ํญํ’์ด ์†Œ๋ฉธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.";
285
  this.addEventLog(msg);
286
 
287
- // ํญํ’ ์‹œ๊ฐํ™”
288
- // ๊ธฐ์กด ํญํ’ ๋ ˆ์ด์–ด ์žˆ์œผ๋ฉด ์ œ๊ฑฐ
289
  if (this.stormCircle) {
290
  this.map.removeLayer(this.stormCircle);
291
  }
292
- // ํญํ’ ํ™œ์„ฑํ™” ์‹œ ์ƒˆ๋กœ ํ‘œ์‹œ
293
  if (this.stormActive) {
294
  this.stormCircle = L.circle(this.stormCenter, {
295
  radius: this.stormRadius * 1000,
@@ -298,45 +463,25 @@
298
  }
299
  }
300
 
301
- // ===== ๋ฌด์ž‘์œ„ ํƒ€๊ฒŸ ์ƒ์„ฑ =====
302
  generateTarget() {
303
- const lat = 51.5 + (Math.random()-0.5)*6; // ยฑ3๋„
304
- const lon = 5.0 + (Math.random()-0.5)*10; // ยฑ5๋„
305
  return {
306
  id: Math.random().toString(36).substr(2, 6).toUpperCase(),
307
- type: Math.random() > 0.7 ? 'aircraft' : 'vehicle',
308
  lat,
309
  lon,
310
- speed: Math.floor(Math.random()*200 + 100), // kts
311
  altitude: Math.floor(Math.random()*30000 + 1000),
312
  heading: Math.random()*360,
313
  signalStrength: Math.random()
314
  };
315
  }
316
 
317
- // ===== ํƒ€๊ฒŸ ์ด๋™ =====
318
- moveTarget(t) {
319
- // heading + speed โ†’ ๋Œ€๋žต์ ์ธ ์œ„๋„/๊ฒฝ๋„ ๋ณ€ํ™”
320
- const speedFactor = 0.00005;
321
- const rad = t.heading * Math.PI / 180;
322
- t.lat += Math.cos(rad) * t.speed * speedFactor;
323
- t.lon += Math.sin(rad) * t.speed * speedFactor;
324
-
325
- // ํญํ’ ์•ˆ์ด๋ฉด signalStrength ๊ฐ์†Œ
326
- if (this.stormActive) {
327
- const distStorm = this.getDistance(
328
- t.lat, t.lon,
329
- this.stormCenter[0], this.stormCenter[1]
330
- );
331
- if (distStorm <= this.stormRadius) {
332
- t.signalStrength = Math.max(0, t.signalStrength - 0.01);
333
- }
334
- }
335
- }
336
-
337
- // ===== ๋‘ ์ขŒํ‘œ ๊ฐ„ ๊ฑฐ๋ฆฌ(km) (Haversine) =====
338
  getDistance(lat1, lon1, lat2, lon2) {
339
- const R = 6371;
340
  const dLat = (lat2 - lat1) * Math.PI/180;
341
  const dLon = (lon2 - lon1) * Math.PI/180;
342
  const a = Math.sin(dLat/2)*Math.sin(dLat/2)
@@ -345,41 +490,55 @@
345
  return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
346
  }
347
 
348
- // ===== ํ‘œ์ ยท์—ฐ๊ฒฐ์„  ์ง€๋„์—์„œ ๊ฐฑ์‹  =====
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
349
  updateTargetsOnMap() {
350
- // 1) ๊ธฐ์กด์— ์žˆ๋˜ ๋ชจ๋“  ์—ฐ๊ฒฐ์„  ์ œ๊ฑฐ
351
  this.targetSignalLines.forEach(line => {
352
  this.map.removeLayer(line);
353
  });
354
  this.targetSignalLines.clear();
355
 
356
- // 2) ๊ฐ ํƒ€๊ฒŸ ๋งˆ์ปค ์œ„์น˜ ๊ฐฑ์‹ 
357
  this.targets.forEach((t, id) => {
358
- // ๋งˆ์ปค๊ฐ€ ์ด๋ฏธ ์žˆ์œผ๋ฉด ์—…๋ฐ์ดํŠธ, ์—†์œผ๋ฉด ์ƒ์„ฑ
359
  let marker = this.targetMarkers.get(id);
360
  if (!marker) {
361
  marker = L.circleMarker([t.lat, t.lon], {
362
  radius: 4,
363
- color: (t.type === 'aircraft') ? '#ff0' : '#0ff',
364
- fillColor: (t.type === 'aircraft') ? '#ff0' : '#0ff',
365
  fillOpacity: 1
366
  }).addTo(this.map);
367
 
368
- // ํˆดํŒ
369
- marker.bindTooltip(this.makeTargetTooltip(t), { sticky: true });
370
  this.targetMarkers.set(id, marker);
371
  } else {
372
- // ์ขŒํ‘œ, ํˆดํŒ ์—…๋ฐ์ดํŠธ
373
  marker.setLatLng([t.lat, t.lon]);
374
- marker.setTooltipContent(this.makeTargetTooltip(t));
375
- // ์ƒ‰๊น” ์—…๋ฐ์ดํŠธ(ํƒ€๊ฒŸ ์ƒํƒœ๊ฐ€ ๋ณ€ํ–ˆ์„ ์ˆ˜๋„)
376
  marker.setStyle({
377
- color: (t.type === 'aircraft') ? '#ff0' : '#0ff',
378
- fillColor: (t.type === 'aircraft') ? '#ff0' : '#0ff'
379
  });
 
380
  }
381
 
382
- // 3) ์Šคํ…Œ์ด์…˜ ๋ฒ”์œ„ ๋‚ด๋ฉด ์—ฐ๊ฒฐ์„  ํ‘œ์‹œ
383
  sdrStations.forEach(st => {
384
  if (st.active) {
385
  const dist = this.getDistance(t.lat, t.lon, st.location[0], st.location[1]);
@@ -392,6 +551,7 @@
392
  opacity: t.signalStrength * 0.3,
393
  weight: 1
394
  }).addTo(this.map);
 
395
  this.targetSignalLines.set(`${id}-${st.name}`, line);
396
  }
397
  }
@@ -399,110 +559,110 @@
399
  });
400
  }
401
 
402
- // ===== ํƒ€๊ฒŸ ํˆดํŒ ๋ฌธ์ž์—ด =====
403
- makeTargetTooltip(t) {
404
  return `
405
  <b>${t.id}</b><br/>
406
  Type: ${t.type}<br/>
407
  Speed: ${t.speed} kts<br/>
408
- ${t.type === 'aircraft' ? `Alt: ${t.altitude} ft<br/>` : ''}
 
 
 
 
409
  Sig: ${(t.signalStrength*100).toFixed(0)}%
410
  `;
411
  }
412
 
413
- // ===== ์‹ค์‹œ๊ฐ„ Detections ์‚ฌ์ด๋“œ๋ฐ” ํ‘œ์‹œ =====
414
  updateDetections() {
415
- const detections = document.getElementById('detections');
416
  let html = '';
417
  this.targets.forEach(t => {
418
- html += `
419
- <div class="detection">
420
- ${t.type === 'aircraft' ? 'โœˆ๏ธ' : '๐Ÿš—'}
421
- ${t.id}
422
- ${t.type === 'aircraft' ? `Alt: ${t.altitude}ft` : ''}
423
- Speed: ${t.speed}kts
424
- Sig: ${(t.signalStrength*100).toFixed(0)}%
425
- </div>
426
- `;
427
  });
428
- detections.innerHTML = html;
429
  }
430
 
431
- // ===== ์ˆ˜์‹ ๊ธฐ ์‹ ํ˜ธ ๋ฐ” ์—…๋ฐ์ดํŠธ =====
432
  updateSignalStrengths() {
433
  sdrStations.forEach(st => {
434
  const bar = document.querySelector(`#rx-${st.url.split(':')[0]} .signal-bar`);
435
  if (bar) {
436
- const strength = 40 + Math.random() * 60; // 40~100
437
  bar.style.width = `${strength}%`;
438
  }
439
  });
440
  }
441
 
442
- // ===== ๋ฉ”์ธ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ๋ฃจํ”„ =====
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
443
  startTracking() {
444
- // ํญํ’์„ 10์ดˆ ๊ฐ„๊ฒฉ์œผ๋กœ 20% ํ™•๋ฅ ๋กœ ํ† ๊ธ€
445
  setInterval(() => {
446
  if (Math.random() < 0.2) {
447
  this.toggleStorm();
448
  }
449
  }, 10000);
450
 
451
- // 100ms ๊ฐ„๊ฒฉ์œผ๋กœ ๋ฐ˜๋ณต
452
  setInterval(() => {
453
- // (10% ํ™•๋ฅ ) ์ƒˆ ํƒ€๊ฒŸ ์ถ”๊ฐ€, ์ตœ๋Œ€ 15๊ฐœ
454
- if (Math.random() < 0.1 && this.targets.size < 15) {
455
  const newT = this.generateTarget();
456
  this.targets.set(newT.id, newT);
457
- this.addEventLog(`ํƒ€๊ฒŸ ์ถœํ˜„: ${newT.id}`);
458
  }
459
- // (10% ํ™•๋ฅ ) ํƒ€๊ฒŸ ํ•˜๋‚˜ ์ œ๊ฑฐ
460
  if (Math.random() < 0.1 && this.targets.size > 0) {
461
- // ๋งจ ์ฒ˜์Œ ํƒ€๊ฒŸ ํ•˜๋‚˜ ๊บผ๋‚ด๊ธฐ
462
- const firstKey = Array.from(this.targets.keys())[0];
463
- this.removeTarget(firstKey);
464
  }
465
 
466
  // ๋ชจ๋“  ํƒ€๊ฒŸ ์ด๋™
467
- this.targets.forEach((t, id) => {
468
  this.moveTarget(t);
469
  });
470
 
471
- // ์ง€๋„/์—ฐ๊ฒฐ์„  ๊ฐฑ์‹ 
472
  this.updateTargetsOnMap();
473
- // ์‚ฌ์ด๋“œ๋ฐ” ํƒ์ง€ ๋ชฉ๋ก ๊ฐฑ์‹ 
474
  this.updateDetections();
475
- // ์ˆ˜์‹ ๊ธฐ ์‹ ํ˜ธ ๋ฐ” ๊ฐฑ์‹ 
476
  this.updateSignalStrengths();
477
  }, 100);
478
  }
479
-
480
- // ํƒ€๊ฒŸ ์ œ๊ฑฐ ์‹œ ๋งˆ์ปค ๋ฐ ์—ฐ๊ฒฐ์„  ์ •๋ฆฌ
481
- removeTarget(id) {
482
- const removed = this.targets.get(id);
483
- if (!removed) return;
484
- this.targets.delete(id);
485
- this.addEventLog(`ํƒ€๊ฒŸ ์†Œ๋ฉธ: ${removed.id}`);
486
-
487
- // ๋งˆ์ปค ์ œ๊ฑฐ
488
- const marker = this.targetMarkers.get(id);
489
- if (marker) {
490
- this.map.removeLayer(marker);
491
- this.targetMarkers.delete(id);
492
- }
493
- // ์—ฐ๊ฒฐ์„  ์ œ๊ฑฐ
494
- [...this.targetSignalLines.keys()].forEach(k => {
495
- if (k.includes(id)) {
496
- this.map.removeLayer(this.targetSignalLines.get(k));
497
- this.targetSignalLines.delete(k);
498
- }
499
- });
500
- }
501
  }
502
 
503
- // ===== ํŽ˜์ด์ง€ ๋กœ๋“œ ํ›„ ์‹œ์ž‘ =====
504
  window.addEventListener('load', () => {
505
- const radar = new RadarSystem();
506
  });
507
  </script>
508
  </body>
 
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;
 
27
  height: calc(100vh - 40px);
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
  }
 
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
+ /* ====================== ํƒ์ง€(Detections) ๋ชฉ๋ก ====================== */
99
  .detection {
100
  padding: 5px;
101
  margin: 5px 0;
 
103
  border-left: 2px solid #0f0;
104
  }
105
 
106
+ /* ====================== ์ด๋ฒคํŠธ ๋กœ๊ทธ ====================== */
107
  .alert {
108
  background: #911;
109
  padding: 5px;
 
112
  color: #f66;
113
  }
114
 
115
+ /* ====================== ์Šคํ…Œ์ด์…˜ ๋ฒ”์œ„ & ํญํ’ ๋ฒ”์œ„ ์Šคํƒ€์ผ ====================== */
116
  .station-range {
117
  stroke: #0f0;
118
  stroke-width: 1;
 
125
  fill: #f00;
126
  fill-opacity: 0.1;
127
  }
 
 
128
  </style>
129
  </head>
130
  <body>
131
  <div class="container">
132
+ <!-- ์‚ฌ์ด๋“œ๋ฐ” -->
133
  <div class="sidebar">
134
  <h2>Hyperscan: Global SDR Radar(Simul)</h2>
135
 
 
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",
 
161
  range: 200,
162
  active: true
163
  },
164
+ {
165
+ name: "TU Delft WebSDR",
166
+ url: "websdr.tudelft.nl:8901",
167
+ location: [51.9981, 4.3731],
168
+ frequency: "0-29.160 MHz",
169
+ range: 180,
170
+ active: true
171
+ },
172
+ {
173
+ name: "SUWS WebSDR UK",
174
+ url: "websdr.suws.org.uk",
175
+ location: [51.2785, -0.7642],
176
+ frequency: "0-30 MHz",
177
+ range: 150,
178
+ active: true
179
+ },
180
  {
181
  name: "KiwiSDR Switzerland",
182
  url: "hb9ryz.no-ip.org:8073",
 
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();
 
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',
 
397
  }).addTo(this.map);
398
 
399
  // ๋ฒ”์œ„ ์›
400
+ L.circle(st.location, {
401
  radius: st.range * 1000,
402
  className: 'station-range'
403
  }).addTo(this.map);
 
411
  });
412
  }
413
 
414
+ // ์‚ฌ์ด๋“œ๋ฐ”์— ์ˆ˜์‹ ๊ธฐ(Receivers) ๋ Œ๋”๋ง
415
  renderReceivers() {
416
  const container = document.getElementById('receivers');
417
  container.innerHTML = sdrStations.map(st => `
 
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
 
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,
 
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)
 
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]);
 
551
  opacity: t.signalStrength * 0.3,
552
  weight: 1
553
  }).addTo(this.map);
554
+
555
  this.targetSignalLines.set(`${id}-${st.name}`, line);
556
  }
557
  }
 
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>