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>