kolaslab commited on
Commit
55962fe
·
verified ·
1 Parent(s): 1e56734

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +258 -223
index.html CHANGED
@@ -3,8 +3,10 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <title>Hyperscan: Global SDR Radar(Simul)</title>
 
 
6
  <style>
7
- /* 전체 배경 기본 스타일 */
8
  body {
9
  margin: 0;
10
  padding: 20px;
@@ -25,13 +27,30 @@
25
  height: calc(100vh - 40px);
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
- /* 탐지(Detections) 목록 */
80
  .detection {
81
  padding: 5px;
82
  margin: 5px 0;
@@ -84,7 +103,7 @@
84
  border-left: 2px solid #0f0;
85
  }
86
 
87
- /* 이벤트 로그 출력 */
88
  .alert {
89
  background: #911;
90
  padding: 5px;
@@ -92,11 +111,27 @@
92
  border-left: 2px solid #f00;
93
  color: #f66;
94
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  </style>
96
  </head>
97
  <body>
98
  <div class="container">
99
- <!-- 사이드바 -->
100
  <div class="sidebar">
101
  <h2>Hyperscan: Global SDR Radar(Simul)</h2>
102
 
@@ -110,12 +145,14 @@
110
  <div id="events"></div>
111
  </div>
112
 
113
- <!-- 캔버스 지도 영역 -->
114
- <canvas id="map"></canvas>
115
  </div>
116
 
 
 
117
  <script>
118
- // 예시 SDR 스테이션: 원하는 만큼 추가/수정 가능
119
  const sdrStations = [
120
  {
121
  name: "Twente WebSDR",
@@ -143,42 +180,71 @@
143
  }
144
  ];
145
 
 
146
  class RadarSystem {
147
  constructor() {
148
- // Canvas 준비
149
- this.canvas = document.getElementById('map');
150
- this.ctx = this.canvas.getContext('2d');
151
-
152
- // 타겟 정보 저장 (Set으로 관리)
153
- this.targets = new Set();
154
-
155
- // 타겟의 이동 궤적(trail)을 저장 (key: 타겟ID, value: {x,y} 배열)
156
- this.trails = new Map();
 
 
 
 
157
 
158
  // 이벤트 로그
159
  this.eventsLog = [];
160
 
161
- // 폭풍/교란 이벤트 상태
162
- this.stormActive = false; // 폭풍 토글
163
- this.stormCenter = { lat: 50.5, lon: 5.0 }; // 폭풍 중심
164
- this.stormRadius = 200; // 폭풍 반경 (km)
165
-
166
- this.setupCanvas();
167
  this.renderReceivers();
168
  this.startTracking();
169
  }
170
 
171
- // 캔버스 크기 ��추기
172
- setupCanvas() {
173
- this.canvas.width = this.canvas.offsetWidth;
174
- this.canvas.height = this.canvas.offsetHeight;
175
- window.addEventListener('resize', () => {
176
- this.canvas.width = this.canvas.offsetWidth;
177
- this.canvas.height = this.canvas.offsetHeight;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  });
179
  }
180
 
181
- // 사이드바에 수신기 리스트 렌더링
182
  renderReceivers() {
183
  const container = document.getElementById('receivers');
184
  container.innerHTML = sdrStations.map(st => `
@@ -198,68 +264,79 @@
198
  `).join('');
199
  }
200
 
201
- // 임의로 타겟 하나 생성
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
202
  generateTarget() {
203
- const lat = 51.5 + (Math.random()-0.5)*4; // 임의 범위
204
- const lon = 5.0 + (Math.random()-0.5)*8;
205
  return {
206
  id: Math.random().toString(36).substr(2, 6).toUpperCase(),
207
- type: Math.random() > 0.7 ? 'aircraft' : 'vehicle', // 30%확률 aircraft
208
  lat,
209
  lon,
210
- speed: (Math.random()*200 + 100).toFixed(0),
211
- altitude: (Math.random()*30000 + 1000).toFixed(0),
212
  heading: Math.random()*360,
213
  signalStrength: Math.random()
214
  };
215
  }
216
 
217
- // 타겟 이동(heading, speed 기반)
218
- moveTarget(target) {
219
- const speedKnots = parseFloat(target.speed);
220
- // 1 knot 약 0.0005 deg/sec 가정 (단순화)
221
  const speedFactor = 0.00005;
222
- const rad = (target.heading * Math.PI) / 180;
223
- // 북(위도+), (위도-), 동(경도+), 서(경도-)
224
- target.lat += Math.cos(rad) * speedKnots * speedFactor;
225
- target.lon += Math.sin(rad) * speedKnots * speedFactor;
226
- }
227
-
228
- // 폭풍 On/Off
229
- toggleStorm() {
230
- this.stormActive = !this.stormActive;
231
- const msg = this.stormActive
232
- ? "폭풍 발생! 수신 교란 우려"
233
- : "폭풍 소멸. 상태 정상화";
234
- this.addEventLog(msg);
235
- }
236
-
237
- // 로그 추가
238
- addEventLog(msg) {
239
- this.eventsLog.push(msg);
240
- const eventsDiv = document.getElementById('events');
241
- // 추가
242
- eventsDiv.innerHTML += `<div class="alert">${msg}</div>`;
243
- // 너무 많아지면 오래된 기록 제거
244
- if (this.eventsLog.length > 10) {
245
- this.eventsLog.shift();
246
- eventsDiv.removeChild(eventsDiv.firstChild);
247
  }
248
  }
249
 
250
- // 위도경도를 캔버스 좌표로 변환 (단순도)
251
- latLongToXY(lat, lon) {
252
- const centerLat = 51.5;
253
- const centerLon = 5.0;
254
- const scale = 100;
255
- const x = (lon - centerLon) * scale + this.canvas.width / 2;
256
- const y = (centerLat - lat) * scale + this.canvas.height / 2;
257
- return { x, y };
258
- }
259
-
260
- // 두 점(위도경도) 사이 거리(km)
261
- distanceKm(lat1, lon1, lat2, lon2) {
262
- const R = 6371;
263
  const dLat = (lat2 - lat1) * Math.PI/180;
264
  const dLon = (lon2 - lon1) * Math.PI/180;
265
  const a = Math.sin(dLat/2)*Math.sin(dLat/2)
@@ -268,149 +345,82 @@
268
  return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
269
  }
270
 
271
- // 배경+그리드 그리기
272
- drawBackground() {
273
- this.ctx.fillStyle = '#111';
274
- this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
275
-
276
- this.ctx.strokeStyle = '#1a1a1a';
277
- this.ctx.lineWidth = 1;
278
- // 세로 그리드
279
- for (let i=0; i<this.canvas.width; i+=50) {
280
- this.ctx.beginPath();
281
- this.ctx.moveTo(i, 0);
282
- this.ctx.lineTo(i, this.canvas.height);
283
- this.ctx.stroke();
284
- }
285
- // 가로 그리드
286
- for (let i=0; i<this.canvas.height; i+=50) {
287
- this.ctx.beginPath();
288
- this.ctx.moveTo(0, i);
289
- this.ctx.lineTo(this.canvas.width, i);
290
- this.ctx.stroke();
291
- }
292
- }
293
-
294
- // 스테이션 + 폭풍 표시
295
- drawStations() {
296
- // 폭풍 범위 시각화
297
- if (this.stormActive) {
298
- const sc = this.latLongToXY(this.stormCenter.lat, this.stormCenter.lon);
299
- this.ctx.beginPath();
300
- this.ctx.arc(sc.x, sc.y, this.stormRadius, 0, Math.PI*2);
301
- this.ctx.fillStyle = 'rgba(255,0,0,0.1)';
302
- this.ctx.fill();
303
- this.ctx.strokeStyle = 'rgba(255,0,0,0.5)';
304
- this.ctx.stroke();
305
- }
306
-
307
- // 수신기 스테이션
308
- sdrStations.forEach(st => {
309
- const pos = this.latLongToXY(st.location[0], st.location[1]);
310
- // 범위 원
311
- this.ctx.beginPath();
312
- this.ctx.arc(pos.x, pos.y, st.range, 0, Math.PI*2);
313
- this.ctx.strokeStyle = st.active
314
- ? 'rgba(0,255,0,0.2)'
315
- : 'rgba(255,0,0,0.2)';
316
- this.ctx.stroke();
317
-
318
- // 중심 점
319
- this.ctx.beginPath();
320
- this.ctx.arc(pos.x, pos.y, 4, 0, Math.PI*2);
321
- this.ctx.fillStyle = st.active ? '#0f0' : '#f00';
322
- this.ctx.fill();
323
-
324
- // 라벨
325
- this.ctx.fillStyle = '#0f0';
326
- this.ctx.font = '10px monospace';
327
- this.ctx.fillText(st.name, pos.x+8, pos.y+4);
328
  });
329
- }
330
-
331
- // 타겟 궤적 그리기
332
- drawTargets() {
333
- this.targets.forEach(target => {
334
- // 타겟 이동
335
- this.moveTarget(target);
336
-
337
- // 폭풍 영향: 범위 내에 있으면 신호강도 감소
338
- if (this.stormActive) {
339
- const distStorm = this.distanceKm(
340
- target.lat, target.lon,
341
- this.stormCenter.lat, this.stormCenter.lon
342
- );
343
- if (distStorm <= this.stormRadius) {
344
- target.signalStrength = Math.max(0, target.signalStrength - 0.01);
345
- }
346
- }
347
-
348
- // 타겟 위치
349
- const pos = this.latLongToXY(target.lat, target.lon);
350
-
351
- // 궤적 저장
352
- if (!this.trails.has(target.id)) {
353
- this.trails.set(target.id, []);
354
- }
355
- const trail = this.trails.get(target.id);
356
- trail.push({x: pos.x, y: pos.y});
357
- if (trail.length > 100) {
358
- trail.shift(); // 오래된 좌표 제거
359
  }
360
 
361
- // 스테이션 연결선
362
  sdrStations.forEach(st => {
363
  if (st.active) {
364
- const dist = this.distanceKm(
365
- target.lat, target.lon,
366
- st.location[0], st.location[1]
367
- );
368
  if (dist <= st.range) {
369
- const sp = this.latLongToXY(st.location[0], st.location[1]);
370
- this.ctx.beginPath();
371
- this.ctx.moveTo(sp.x, sp.y);
372
- this.ctx.lineTo(pos.x, pos.y);
373
- this.ctx.strokeStyle = `rgba(0,255,0,${target.signalStrength*0.3})`;
374
- this.ctx.stroke();
 
 
 
375
  }
376
  }
377
  });
378
-
379
- // 타겟 궤적
380
- this.ctx.beginPath();
381
- this.ctx.strokeStyle = (target.type==='aircraft')
382
- ? 'rgba(255,255,0,0.3)'
383
- : 'rgba(0,255,255,0.3)';
384
- for (let i=0; i<trail.length-1; i++) {
385
- this.ctx.moveTo(trail[i].x, trail[i].y);
386
- this.ctx.lineTo(trail[i+1].x, trail[i+1].y);
387
- }
388
- this.ctx.stroke();
389
-
390
- // 현재 타겟 점
391
- this.ctx.beginPath();
392
- this.ctx.arc(pos.x, pos.y, 3, 0, Math.PI*2);
393
- this.ctx.fillStyle = (target.type==='aircraft') ? '#ff0' : '#0ff';
394
- this.ctx.fill();
395
-
396
- // 식별 정보
397
- this.ctx.fillStyle = '#666';
398
- this.ctx.font = '10px monospace';
399
- this.ctx.fillText(`${target.id} (${target.type})`, pos.x+8, pos.y+4);
400
  });
401
  }
402
 
403
- // 사이드바 'Real-time Detections' 업데이트
 
 
 
 
 
 
 
 
 
 
 
404
  updateDetections() {
405
  const detections = document.getElementById('detections');
406
  let html = '';
407
  this.targets.forEach(t => {
408
  html += `
409
  <div class="detection">
410
- ${t.type==='aircraft' ? '✈️' : '🚗'}
411
  ${t.id}
 
412
  Speed: ${t.speed}kts
413
- ${t.type==='aircraft' ? 'Alt: '+t.altitude+'ft' : ''}
414
  Sig: ${(t.signalStrength*100).toFixed(0)}%
415
  </div>
416
  `;
@@ -418,56 +428,81 @@
418
  detections.innerHTML = html;
419
  }
420
 
421
- // 수신기 신호강도무작위 업데이트
422
  updateSignalStrengths() {
423
  sdrStations.forEach(st => {
424
  const bar = document.querySelector(`#rx-${st.url.split(':')[0]} .signal-bar`);
425
  if (bar) {
426
- const strength = 40 + Math.random()*60;
427
  bar.style.width = `${strength}%`;
428
  }
429
  });
430
  }
431
 
432
- // 메인 루프
433
  startTracking() {
434
- // 폭풍 토글(약 20% 확률로 10초마다 한 번 발생)
435
  setInterval(() => {
436
  if (Math.random() < 0.2) {
437
  this.toggleStorm();
438
  }
439
  }, 10000);
440
 
441
- // 100ms마다 갱신
442
  setInterval(() => {
443
- // 10% 확률로 타겟 추가
444
  if (Math.random() < 0.1 && this.targets.size < 15) {
445
  const newT = this.generateTarget();
446
- this.targets.add(newT);
447
- this.addEventLog(`새 타겟 출현: ${newT.id}`);
448
  }
449
- // 10% 확률로 타겟 하나 제거
450
  if (Math.random() < 0.1 && this.targets.size > 0) {
451
- const first = Array.from(this.targets)[0];
452
- this.targets.delete(first);
453
- this.addEventLog(`타겟 소멸: ${first.id}`);
454
  }
455
 
456
- // 프레임 화면 그리기
457
- this.drawBackground();
458
- this.drawStations();
459
- this.drawTargets();
460
 
461
- // 사이드바 갱신
 
 
462
  this.updateDetections();
 
463
  this.updateSignalStrengths();
464
  }, 100);
465
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
466
  }
467
 
468
- // 페이지 로드 시작
469
  window.addEventListener('load', () => {
470
- new RadarSystem();
471
  });
472
  </script>
473
  </body>
 
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;
 
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
+ }
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
+ /* =============== 사이드바 수신기 목록 =============== */
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: #911;
109
  padding: 5px;
 
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
+
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
  <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",
 
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();
 
 
 
 
 
204
  this.renderReceivers();
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',
228
+ fillColor: '#0f0',
229
+ fillOpacity: 1
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);
237
+
238
+ // 툴팁
239
+ stationMarker.bindTooltip(`
240
+ <b>${st.name}</b><br/>
241
+ Frequency: ${st.frequency}<br/>
242
+ Range: ${st.range} km
243
+ `);
244
  });
245
  }
246
 
247
+ // ===== 사이드바 Receivers 표시 =====
248
  renderReceivers() {
249
  const container = document.getElementById('receivers');
250
  container.innerHTML = sdrStations.map(st => `
 
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
283
+ ? "폭풍 발생! (교란 가능)"
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,
296
+ className: 'storm-range'
297
+ }).addTo(this.map);
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
  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]);
 
 
 
386
  if (dist <= st.range) {
387
+ const line = L.polyline([
388
+ [t.lat, t.lon],
389
+ st.location
390
+ ], {
391
+ color: '#0f0',
392
+ opacity: t.signalStrength * 0.3,
393
+ weight: 1
394
+ }).addTo(this.map);
395
+ this.targetSignalLines.set(`${id}-${st.name}`, line);
396
  }
397
  }
398
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
  `;
 
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>