aiqtech commited on
Commit
bd0429c
·
verified ·
1 Parent(s): 55df423

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +456 -88
index.html CHANGED
@@ -26,7 +26,7 @@
26
  border-radius: 20px;
27
  padding: 30px;
28
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
29
- max-width: 1200px;
30
  width: 100%;
31
  }
32
 
@@ -46,6 +46,7 @@
46
  border-radius: 10px;
47
  overflow: hidden;
48
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
 
49
  }
50
 
51
  canvas {
@@ -61,6 +62,7 @@
61
  gap: 15px;
62
  margin: 20px 0;
63
  flex-wrap: wrap;
 
64
  }
65
 
66
  button {
@@ -85,6 +87,35 @@
85
  transform: translateY(0);
86
  }
87
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  .info-panel {
89
  background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
90
  padding: 15px;
@@ -123,17 +154,19 @@
123
  display: flex;
124
  align-items: center;
125
  justify-content: center;
126
- gap: 20px;
127
  margin-top: 20px;
128
  padding: 15px;
129
  background: #f8f9fa;
130
  border-radius: 10px;
 
131
  }
132
 
133
  .legend-item {
134
  display: flex;
135
  align-items: center;
136
  gap: 8px;
 
137
  }
138
 
139
  .legend-color {
@@ -143,6 +176,11 @@
143
  border: 1px solid #ddd;
144
  }
145
 
 
 
 
 
 
146
  .tooltip {
147
  position: absolute;
148
  background: rgba(0, 0, 0, 0.9);
@@ -174,6 +212,13 @@
174
  outline: none;
175
  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
176
  }
 
 
 
 
 
 
 
177
  </style>
178
  </head>
179
  <body>
@@ -184,13 +229,20 @@
184
  <button onclick="generateNewTerrain()">🏔️ 새로운 지형 생성</button>
185
  <button onclick="toggleContours()">📊 등고선 표시/숨기기</button>
186
  <button onclick="toggleHeatmap()">🌡️ 히트맵 전환</button>
187
- <button onclick="toggle3D()">🎮 3D 뷰 전환</button>
 
188
  <select id="terrainType" onchange="changeTerrainType()">
189
  <option value="mountain">⛰️ 산악 지형</option>
190
  <option value="valley">🏞️ 계곡 지형</option>
191
  <option value="plateau">🏔️ 고원 지형</option>
192
- <option value="island">🏝️ 지형</option>
 
193
  </select>
 
 
 
 
 
194
  <button onclick="exportMap()">💾 지도 저장</button>
195
  </div>
196
 
@@ -199,6 +251,10 @@
199
  <div class="tooltip" id="tooltip"></div>
200
  </div>
201
 
 
 
 
 
202
  <div class="info-panel">
203
  <div class="info-item">
204
  <div class="info-label">좌표</div>
@@ -213,27 +269,43 @@
213
  <div class="info-value" id="slope">-</div>
214
  </div>
215
  <div class="info-item">
216
- <div class="info-label">등고선 간격</div>
217
- <div class="info-value">10m</div>
218
  </div>
219
  </div>
220
 
221
  <div class="legend">
222
  <div class="legend-item">
223
  <div class="legend-color" style="background: #0d4f8b;"></div>
224
- <span>낮은 고도 (0-200m)</span>
225
  </div>
226
  <div class="legend-item">
227
  <div class="legend-color" style="background: #61a861;"></div>
228
- <span>중간 고도 (200-500m)</span>
229
  </div>
230
  <div class="legend-item">
231
  <div class="legend-color" style="background: #f5deb3;"></div>
232
- <span>높은 고도 (500-800m)</span>
233
  </div>
234
  <div class="legend-item">
235
  <div class="legend-color" style="background: #8b4513;"></div>
236
- <span>최고 고도 (800m+)</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  </div>
238
  </div>
239
  </div>
@@ -249,22 +321,32 @@
249
  canvas.width = WIDTH;
250
  canvas.height = HEIGHT;
251
 
252
- // 지형 데이터를 저장할 배열
 
 
 
 
 
253
  let heightMap = [];
 
 
 
254
  let showContours = true;
255
  let showHeatmap = false;
256
- let is3D = false;
 
257
  let terrainType = 'mountain';
 
258
 
259
- // 노이즈 함수 (Perlin noise 간단 구현)
260
- function noise(x, y, scale, octaves) {
261
  let value = 0;
262
  let amplitude = 1;
263
  let frequency = scale;
264
  let maxValue = 0;
265
 
266
  for (let i = 0; i < octaves; i++) {
267
- value += amplitude * simpleNoise(x * frequency, y * frequency);
268
  maxValue += amplitude;
269
  amplitude *= 0.5;
270
  frequency *= 2;
@@ -280,9 +362,15 @@
280
 
281
  // 지형 생성 함수
282
  function generateTerrain() {
 
283
  heightMap = [];
 
 
 
 
284
  const gridSize = 100;
285
 
 
286
  for (let y = 0; y < gridSize; y++) {
287
  heightMap[y] = [];
288
  for (let x = 0; x < gridSize; x++) {
@@ -290,36 +378,36 @@
290
 
291
  switch(terrainType) {
292
  case 'mountain':
293
- // 산악 지형
294
- height = noise(x, y, 0.02, 6) * 0.6;
295
- height += noise(x, y, 0.05, 4) * 0.3;
296
- height += noise(x, y, 0.1, 2) * 0.1;
297
  height = Math.pow(Math.abs(height), 1.2) * Math.sign(height);
298
  break;
299
 
300
  case 'valley':
301
- // 계곡 지형
302
  const distX = (x - gridSize/2) / gridSize;
303
  const distY = (y - gridSize/2) / gridSize;
304
  const dist = Math.sqrt(distX*distX + distY*distY);
305
- height = noise(x, y, 0.03, 4) * 0.5;
306
  height *= (1 - dist * 0.5);
307
  height -= dist * 0.3;
308
  break;
309
 
310
  case 'plateau':
311
- // 고원 지형
312
- height = noise(x, y, 0.02, 3) * 0.3;
313
- if (height > 0.1) height = 0.3 + noise(x, y, 0.05, 2) * 0.1;
 
 
 
 
 
 
314
  break;
315
 
316
- case 'island':
317
- // 지형
318
- const centerX = (x - gridSize/2) / gridSize;
319
- const centerY = (y - gridSize/2) / gridSize;
320
- const radius = Math.sqrt(centerX*centerX + centerY*centerY);
321
- height = noise(x, y, 0.03, 5) * 0.6;
322
- height *= Math.max(0, 1 - radius * 2);
323
  break;
324
  }
325
 
@@ -328,18 +416,179 @@
328
  heightMap[y][x] = height;
329
  }
330
  }
 
 
 
 
 
 
 
 
 
331
  }
332
 
333
- // 등고선 계산
334
- function calculateContours(interval) {
335
- const contours = [];
336
- const levels = [];
337
 
338
- for (let level = 0; level <= 1; level += interval) {
339
- levels.push(level);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
  }
 
 
 
 
 
341
 
342
- return { contours, levels };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
343
  }
344
 
345
  // 지도 그리기
@@ -350,40 +599,73 @@
350
  const cellWidth = WIDTH / gridSize;
351
  const cellHeight = HEIGHT / gridSize;
352
 
353
- // 배경 그리기 (높이맵 또는 히트맵)
354
  for (let y = 0; y < gridSize; y++) {
355
  for (let x = 0; x < gridSize; x++) {
356
  const height = heightMap[y][x];
357
 
358
  if (showHeatmap) {
359
- // 히트맵 색상
360
  const hue = (1 - height) * 240;
361
  ctx.fillStyle = `hsl(${hue}, 70%, 50%)`;
362
  } else {
363
- // 지형 색상
364
  ctx.fillStyle = getTerrainColor(height);
365
  }
366
 
367
- if (is3D) {
368
- // 3D 효과
369
- const offset = height * 10;
370
- ctx.fillRect(
371
- x * cellWidth + offset,
372
- y * cellHeight - offset,
373
- cellWidth + 1,
374
- cellHeight + 1
375
- );
376
- } else {
377
- ctx.fillRect(
378
- x * cellWidth,
379
- y * cellHeight,
380
- cellWidth + 1,
381
- cellHeight + 1
382
- );
383
- }
384
  }
385
  }
386
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
  // 등고선 그리기
388
  if (showContours) {
389
  ctx.strokeStyle = 'rgba(0, 0, 0, 0.3)';
@@ -414,7 +696,6 @@
414
  }
415
  }
416
 
417
- // 주 등고선은 굵게
418
  if (level % 0.2 === 0) {
419
  ctx.lineWidth = 2;
420
  ctx.strokeStyle = 'rgba(0, 0, 0, 0.5)';
@@ -427,23 +708,44 @@
427
  }
428
  }
429
 
430
- // 그리드 표시
431
- ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
432
- ctx.lineWidth = 0.5;
433
- for (let i = 0; i <= gridSize; i += 10) {
434
- ctx.beginPath();
435
- ctx.moveTo(i * cellWidth, 0);
436
- ctx.lineTo(i * cellWidth, HEIGHT);
437
- ctx.stroke();
438
 
439
- ctx.beginPath();
440
- ctx.moveTo(0, i * cellHeight);
441
- ctx.lineTo(WIDTH, i * cellHeight);
442
- ctx.stroke();
443
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
444
  }
445
 
446
- // Marching squares 알고리즘으로 등고선 그리기
447
  function drawContourCell(x, y, width, height, corners, level) {
448
  let state = 0;
449
  if (corners[0] > level) state |= 1;
@@ -500,20 +802,42 @@
500
  ctx.lineTo(x2, y2);
501
  }
502
 
503
- // 지형 색상 결정
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
504
  function getTerrainColor(height) {
505
- if (height < 0.2) return '#0d4f8b'; // 깊은
506
- if (height < 0.3) return '#1e7eb8'; // 얕은 물
507
- if (height < 0.4) return '#61a861'; // 낮은 지대 (초록)
508
- if (height < 0.5) return '#8fc68f'; // 평지
509
- if (height < 0.6) return '#c4d4aa'; // 언덕
510
- if (height < 0.7) return '#f5deb3'; // 높은 언덕
511
- if (height < 0.8) return '#d2b48c'; // 낮은 산
512
- if (height < 0.9) return '#8b4513'; // 산
513
- return '#fff'; // 눈 덮인 산꼭대기
514
  }
515
 
516
- // 마우스 이벤트 처리
517
  canvas.addEventListener('mousemove', (e) => {
518
  const rect = canvas.getBoundingClientRect();
519
  const x = (e.clientX - rect.left) * (WIDTH / rect.width);
@@ -535,11 +859,15 @@
535
  slope = Math.round(Math.sqrt(dx * dx + dy * dy) * 100);
536
  }
537
 
 
 
 
 
538
  document.getElementById('coordinates').textContent = `${gridX}, ${gridY}`;
539
  document.getElementById('elevation').textContent = `${elevation}m`;
540
  document.getElementById('slope').textContent = `${slope}°`;
 
541
 
542
- // 툴팁 표시
543
  tooltip.style.display = 'block';
544
  tooltip.style.left = e.clientX + 10 + 'px';
545
  tooltip.style.top = e.clientY - 30 + 'px';
@@ -552,6 +880,7 @@
552
  document.getElementById('coordinates').textContent = '-';
553
  document.getElementById('elevation').textContent = '-';
554
  document.getElementById('slope').textContent = '-';
 
555
  });
556
 
557
  // 컨트롤 함수들
@@ -570,8 +899,13 @@
570
  drawMap();
571
  }
572
 
573
- function toggle3D() {
574
- is3D = !is3D;
 
 
 
 
 
575
  drawMap();
576
  }
577
 
@@ -581,9 +915,42 @@
581
  drawMap();
582
  }
583
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
584
  function exportMap() {
585
  const link = document.createElement('a');
586
- link.download = 'topographic-map.png';
587
  link.href = canvas.toDataURL();
588
  link.click();
589
  }
@@ -591,6 +958,7 @@
591
  // 초기화
592
  generateTerrain();
593
  drawMap();
 
594
 
595
  // 화면 크기 변경 대응
596
  window.addEventListener('resize', () => {
 
26
  border-radius: 20px;
27
  padding: 30px;
28
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
29
+ max-width: 1400px;
30
  width: 100%;
31
  }
32
 
 
46
  border-radius: 10px;
47
  overflow: hidden;
48
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
49
+ background: #f0f0f0;
50
  }
51
 
52
  canvas {
 
62
  gap: 15px;
63
  margin: 20px 0;
64
  flex-wrap: wrap;
65
+ align-items: center;
66
  }
67
 
68
  button {
 
87
  transform: translateY(0);
88
  }
89
 
90
+ .zoom-controls {
91
+ display: flex;
92
+ align-items: center;
93
+ gap: 10px;
94
+ background: white;
95
+ padding: 8px 15px;
96
+ border-radius: 25px;
97
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
98
+ }
99
+
100
+ .zoom-btn {
101
+ width: 35px;
102
+ height: 35px;
103
+ padding: 0;
104
+ border-radius: 50%;
105
+ font-size: 20px;
106
+ display: flex;
107
+ align-items: center;
108
+ justify-content: center;
109
+ }
110
+
111
+ .scale-info {
112
+ font-size: 14px;
113
+ color: #666;
114
+ font-weight: 600;
115
+ min-width: 100px;
116
+ text-align: center;
117
+ }
118
+
119
  .info-panel {
120
  background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
121
  padding: 15px;
 
154
  display: flex;
155
  align-items: center;
156
  justify-content: center;
157
+ gap: 15px;
158
  margin-top: 20px;
159
  padding: 15px;
160
  background: #f8f9fa;
161
  border-radius: 10px;
162
+ flex-wrap: wrap;
163
  }
164
 
165
  .legend-item {
166
  display: flex;
167
  align-items: center;
168
  gap: 8px;
169
+ font-size: 14px;
170
  }
171
 
172
  .legend-color {
 
176
  border: 1px solid #ddd;
177
  }
178
 
179
+ .legend-symbol {
180
+ font-size: 20px;
181
+ margin-right: 5px;
182
+ }
183
+
184
  .tooltip {
185
  position: absolute;
186
  background: rgba(0, 0, 0, 0.9);
 
212
  outline: none;
213
  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
214
  }
215
+
216
+ .map-size-info {
217
+ text-align: center;
218
+ color: #666;
219
+ font-size: 14px;
220
+ margin-top: 10px;
221
+ }
222
  </style>
223
  </head>
224
  <body>
 
229
  <button onclick="generateNewTerrain()">🏔️ 새로운 지형 생성</button>
230
  <button onclick="toggleContours()">📊 등고선 표시/숨기기</button>
231
  <button onclick="toggleHeatmap()">🌡️ 히트맵 전환</button>
232
+ <button onclick="toggleRoads()">🛣️ 도로 표시/숨기기</button>
233
+ <button onclick="toggleRivers()">💧 하천 표시/숨기기</button>
234
  <select id="terrainType" onchange="changeTerrainType()">
235
  <option value="mountain">⛰️ 산악 지형</option>
236
  <option value="valley">🏞️ 계곡 지형</option>
237
  <option value="plateau">🏔️ 고원 지형</option>
238
+ <option value="rural">🏘️ 전원 지형</option>
239
+ <option value="urban">🏙️ 도시 평원</option>
240
  </select>
241
+ <div class="zoom-controls">
242
+ <button class="zoom-btn" onclick="zoomOut()">−</button>
243
+ <span class="scale-info" id="scaleInfo">1:5000</span>
244
+ <button class="zoom-btn" onclick="zoomIn()">+</button>
245
+ </div>
246
  <button onclick="exportMap()">💾 지도 저장</button>
247
  </div>
248
 
 
251
  <div class="tooltip" id="tooltip"></div>
252
  </div>
253
 
254
+ <div class="map-size-info" id="mapSizeInfo">
255
+ 지도 범위: 4km × 3km
256
+ </div>
257
+
258
  <div class="info-panel">
259
  <div class="info-item">
260
  <div class="info-label">좌표</div>
 
269
  <div class="info-value" id="slope">-</div>
270
  </div>
271
  <div class="info-item">
272
+ <div class="info-label">실제 거리</div>
273
+ <div class="info-value" id="realDistance">-</div>
274
  </div>
275
  </div>
276
 
277
  <div class="legend">
278
  <div class="legend-item">
279
  <div class="legend-color" style="background: #0d4f8b;"></div>
280
+ <span>수역 (0-50m)</span>
281
  </div>
282
  <div class="legend-item">
283
  <div class="legend-color" style="background: #61a861;"></div>
284
+ <span>평지 (50-200m)</span>
285
  </div>
286
  <div class="legend-item">
287
  <div class="legend-color" style="background: #f5deb3;"></div>
288
+ <span>구릉 (200-500m)</span>
289
  </div>
290
  <div class="legend-item">
291
  <div class="legend-color" style="background: #8b4513;"></div>
292
+ <span>산지 (500m+)</span>
293
+ </div>
294
+ <div class="legend-item">
295
+ <span class="legend-symbol">━</span>
296
+ <span>도로</span>
297
+ </div>
298
+ <div class="legend-item">
299
+ <span class="legend-symbol" style="color: #4682B4;">〰️</span>
300
+ <span>하천</span>
301
+ </div>
302
+ <div class="legend-item">
303
+ <span class="legend-symbol">🏘️</span>
304
+ <span>마을</span>
305
+ </div>
306
+ <div class="legend-item">
307
+ <span class="legend-symbol">🏢</span>
308
+ <span>도시</span>
309
  </div>
310
  </div>
311
  </div>
 
321
  canvas.width = WIDTH;
322
  canvas.height = HEIGHT;
323
 
324
+ // 축척 관련 변수
325
+ const scales = [2500, 5000, 10000, 25000, 50000];
326
+ let currentScaleIndex = 1; // 기본 1:5000
327
+ let currentScale = scales[currentScaleIndex];
328
+
329
+ // 지형 데이터를 저장할 변수들
330
  let heightMap = [];
331
+ let roads = [];
332
+ let rivers = [];
333
+ let settlements = [];
334
  let showContours = true;
335
  let showHeatmap = false;
336
+ let showRoads = true;
337
+ let showRivers = true;
338
  let terrainType = 'mountain';
339
+ let seed = Math.random() * 10000;
340
 
341
+ // 개선된 노이즈 함수
342
+ function noise(x, y, scale, octaves, seed) {
343
  let value = 0;
344
  let amplitude = 1;
345
  let frequency = scale;
346
  let maxValue = 0;
347
 
348
  for (let i = 0; i < octaves; i++) {
349
+ value += amplitude * simpleNoise(x * frequency + seed, y * frequency + seed);
350
  maxValue += amplitude;
351
  amplitude *= 0.5;
352
  frequency *= 2;
 
362
 
363
  // 지형 생성 함수
364
  function generateTerrain() {
365
+ seed = Math.random() * 10000; // 새로운 시드 생성
366
  heightMap = [];
367
+ roads = [];
368
+ rivers = [];
369
+ settlements = [];
370
+
371
  const gridSize = 100;
372
 
373
+ // 높이맵 생성
374
  for (let y = 0; y < gridSize; y++) {
375
  heightMap[y] = [];
376
  for (let x = 0; x < gridSize; x++) {
 
378
 
379
  switch(terrainType) {
380
  case 'mountain':
381
+ height = noise(x, y, 0.02, 6, seed) * 0.6;
382
+ height += noise(x, y, 0.05, 4, seed + 100) * 0.3;
383
+ height += noise(x, y, 0.1, 2, seed + 200) * 0.1;
 
384
  height = Math.pow(Math.abs(height), 1.2) * Math.sign(height);
385
  break;
386
 
387
  case 'valley':
 
388
  const distX = (x - gridSize/2) / gridSize;
389
  const distY = (y - gridSize/2) / gridSize;
390
  const dist = Math.sqrt(distX*distX + distY*distY);
391
+ height = noise(x, y, 0.03, 4, seed) * 0.5;
392
  height *= (1 - dist * 0.5);
393
  height -= dist * 0.3;
394
  break;
395
 
396
  case 'plateau':
397
+ height = noise(x, y, 0.02, 3, seed) * 0.3;
398
+ if (height > 0.1) height = 0.3 + noise(x, y, 0.05, 2, seed + 100) * 0.1;
399
+ break;
400
+
401
+ case 'rural':
402
+ // 전원 지형 - 완만한 구릉과 평지
403
+ height = noise(x, y, 0.015, 4, seed) * 0.2;
404
+ height += noise(x, y, 0.03, 2, seed + 100) * 0.1;
405
+ height = Math.max(0.1, height); // 최소 높이 보장
406
  break;
407
 
408
+ case 'urban':
409
+ // 도시 평원 - 매우 평탄한 지형
410
+ height = noise(x, y, 0.01, 2, seed) * 0.1 + 0.15;
 
 
 
 
411
  break;
412
  }
413
 
 
416
  heightMap[y][x] = height;
417
  }
418
  }
419
+
420
+ // 도로 생성
421
+ generateRoads(gridSize);
422
+
423
+ // 하천 생성
424
+ generateRivers(gridSize);
425
+
426
+ // 정착지 생성
427
+ generateSettlements(gridSize);
428
  }
429
 
430
+ // 도로 생성
431
+ function generateRoads(gridSize) {
432
+ roads = [];
433
+ const numRoads = terrainType === 'urban' ? 8 : (terrainType === 'rural' ? 5 : 3);
434
 
435
+ for (let i = 0; i < numRoads; i++) {
436
+ const road = [];
437
+ let x = Math.random() * gridSize;
438
+ let y = Math.random() * gridSize;
439
+ const targetX = Math.random() * gridSize;
440
+ const targetY = Math.random() * gridSize;
441
+
442
+ // A* 또는 간단한 경로 찾기
443
+ for (let step = 0; step < 50; step++) {
444
+ road.push({ x: Math.floor(x), y: Math.floor(y) });
445
+
446
+ // 낮은 고도 선호
447
+ const dx = targetX - x;
448
+ const dy = targetY - y;
449
+ const angle = Math.atan2(dy, dx);
450
+
451
+ // 경사를 고려한 이동
452
+ let bestAngle = angle;
453
+ let minSlope = Infinity;
454
+
455
+ for (let a = -Math.PI/4; a <= Math.PI/4; a += Math.PI/8) {
456
+ const testAngle = angle + a;
457
+ const nx = x + Math.cos(testAngle) * 2;
458
+ const ny = y + Math.sin(testAngle) * 2;
459
+
460
+ if (nx >= 0 && nx < gridSize && ny >= 0 && ny < gridSize) {
461
+ const slope = Math.abs(heightMap[Math.floor(ny)][Math.floor(nx)] -
462
+ heightMap[Math.floor(y)][Math.floor(x)]);
463
+ if (slope < minSlope) {
464
+ minSlope = slope;
465
+ bestAngle = testAngle;
466
+ }
467
+ }
468
+ }
469
+
470
+ x += Math.cos(bestAngle) * 2;
471
+ y += Math.sin(bestAngle) * 2;
472
+
473
+ if (Math.abs(x - targetX) < 2 && Math.abs(y - targetY) < 2) break;
474
+ }
475
+
476
+ roads.push(road);
477
+ }
478
+ }
479
+
480
+ // 하천 생성
481
+ function generateRivers(gridSize) {
482
+ rivers = [];
483
+ const numRivers = terrainType === 'valley' ? 3 : 2;
484
+
485
+ for (let i = 0; i < numRivers; i++) {
486
+ const river = [];
487
+
488
+ // 높은 곳에서 시작
489
+ let x = Math.random() * gridSize;
490
+ let y = Math.random() * gridSize;
491
+ let highestPoint = heightMap[Math.floor(y)][Math.floor(x)];
492
+
493
+ // 더 높은 지점 찾기
494
+ for (let j = 0; j < 10; j++) {
495
+ const tx = Math.random() * gridSize;
496
+ const ty = Math.random() * gridSize;
497
+ if (heightMap[Math.floor(ty)][Math.floor(tx)] > highestPoint) {
498
+ x = tx;
499
+ y = ty;
500
+ highestPoint = heightMap[Math.floor(ty)][Math.floor(tx)];
501
+ }
502
+ }
503
+
504
+ // 낮은 곳���로 흐르기
505
+ for (let step = 0; step < 100; step++) {
506
+ river.push({ x: Math.floor(x), y: Math.floor(y) });
507
+
508
+ // 가장 낮은 이웃 찾기
509
+ let lowestHeight = heightMap[Math.floor(y)][Math.floor(x)];
510
+ let nextX = x, nextY = y;
511
+
512
+ for (let dx = -1; dx <= 1; dx++) {
513
+ for (let dy = -1; dy <= 1; dy++) {
514
+ if (dx === 0 && dy === 0) continue;
515
+
516
+ const nx = Math.floor(x + dx);
517
+ const ny = Math.floor(y + dy);
518
+
519
+ if (nx >= 0 && nx < gridSize && ny >= 0 && ny < gridSize) {
520
+ if (heightMap[ny][nx] < lowestHeight) {
521
+ lowestHeight = heightMap[ny][nx];
522
+ nextX = x + dx + (Math.random() - 0.5) * 0.5;
523
+ nextY = y + dy + (Math.random() - 0.5) * 0.5;
524
+ }
525
+ }
526
+ }
527
+ }
528
+
529
+ if (nextX === x && nextY === y) break; // 더 이상 낮은 곳이 없음
530
+
531
+ x = nextX;
532
+ y = nextY;
533
+
534
+ if (lowestHeight < 0.2) break; // 물에 도달
535
+ }
536
+
537
+ rivers.push(river);
538
  }
539
+ }
540
+
541
+ // 정착지 생성
542
+ function generateSettlements(gridSize) {
543
+ settlements = [];
544
 
545
+ if (terrainType === 'rural') {
546
+ // 마을 생성
547
+ const numVillages = 5 + Math.floor(Math.random() * 5);
548
+ for (let i = 0; i < numVillages; i++) {
549
+ let x, y;
550
+ do {
551
+ x = Math.random() * gridSize;
552
+ y = Math.random() * gridSize;
553
+ } while (heightMap[Math.floor(y)][Math.floor(x)] > 0.5 ||
554
+ heightMap[Math.floor(y)][Math.floor(x)] < 0.15);
555
+
556
+ settlements.push({
557
+ x: Math.floor(x),
558
+ y: Math.floor(y),
559
+ type: 'village',
560
+ size: 3 + Math.random() * 3
561
+ });
562
+ }
563
+ } else if (terrainType === 'urban') {
564
+ // 도시 생성
565
+ const numCities = 2 + Math.floor(Math.random() * 2);
566
+ for (let i = 0; i < numCities; i++) {
567
+ let x = (i + 1) * gridSize / (numCities + 1) + (Math.random() - 0.5) * 20;
568
+ let y = gridSize / 2 + (Math.random() - 0.5) * 30;
569
+
570
+ settlements.push({
571
+ x: Math.floor(x),
572
+ y: Math.floor(y),
573
+ type: 'city',
574
+ size: 10 + Math.random() * 10
575
+ });
576
+ }
577
+
578
+ // 도시 주변 작은 마을들
579
+ const numTowns = 5 + Math.floor(Math.random() * 5);
580
+ for (let i = 0; i < numTowns; i++) {
581
+ let x = Math.random() * gridSize;
582
+ let y = Math.random() * gridSize;
583
+
584
+ settlements.push({
585
+ x: Math.floor(x),
586
+ y: Math.floor(y),
587
+ type: 'town',
588
+ size: 5 + Math.random() * 5
589
+ });
590
+ }
591
+ }
592
  }
593
 
594
  // 지도 그리기
 
599
  const cellWidth = WIDTH / gridSize;
600
  const cellHeight = HEIGHT / gridSize;
601
 
602
+ // 배경 그리기
603
  for (let y = 0; y < gridSize; y++) {
604
  for (let x = 0; x < gridSize; x++) {
605
  const height = heightMap[y][x];
606
 
607
  if (showHeatmap) {
 
608
  const hue = (1 - height) * 240;
609
  ctx.fillStyle = `hsl(${hue}, 70%, 50%)`;
610
  } else {
 
611
  ctx.fillStyle = getTerrainColor(height);
612
  }
613
 
614
+ ctx.fillRect(
615
+ x * cellWidth,
616
+ y * cellHeight,
617
+ cellWidth + 1,
618
+ cellHeight + 1
619
+ );
 
 
 
 
 
 
 
 
 
 
 
620
  }
621
  }
622
 
623
+ // 하천 그리기
624
+ if (showRivers) {
625
+ rivers.forEach(river => {
626
+ ctx.strokeStyle = '#4682B4';
627
+ ctx.lineWidth = 2;
628
+ ctx.beginPath();
629
+
630
+ river.forEach((point, index) => {
631
+ const x = point.x * cellWidth;
632
+ const y = point.y * cellHeight;
633
+
634
+ if (index === 0) {
635
+ ctx.moveTo(x, y);
636
+ } else {
637
+ ctx.lineTo(x, y);
638
+ }
639
+ });
640
+
641
+ ctx.stroke();
642
+ });
643
+ }
644
+
645
+ // 도로 그리기
646
+ if (showRoads) {
647
+ roads.forEach(road => {
648
+ ctx.strokeStyle = '#555';
649
+ ctx.lineWidth = 2;
650
+ ctx.setLineDash([5, 3]);
651
+ ctx.beginPath();
652
+
653
+ road.forEach((point, index) => {
654
+ const x = point.x * cellWidth;
655
+ const y = point.y * cellHeight;
656
+
657
+ if (index === 0) {
658
+ ctx.moveTo(x, y);
659
+ } else {
660
+ ctx.lineTo(x, y);
661
+ }
662
+ });
663
+
664
+ ctx.stroke();
665
+ ctx.setLineDash([]);
666
+ });
667
+ }
668
+
669
  // 등고선 그리기
670
  if (showContours) {
671
  ctx.strokeStyle = 'rgba(0, 0, 0, 0.3)';
 
696
  }
697
  }
698
 
 
699
  if (level % 0.2 === 0) {
700
  ctx.lineWidth = 2;
701
  ctx.strokeStyle = 'rgba(0, 0, 0, 0.5)';
 
708
  }
709
  }
710
 
711
+ // 정착지 그리기
712
+ settlements.forEach(settlement => {
713
+ const x = settlement.x * cellWidth;
714
+ const y = settlement.y * cellHeight;
 
 
 
 
715
 
716
+ if (settlement.type === 'city') {
717
+ // 도시
718
+ ctx.fillStyle = '#666';
719
+ ctx.fillRect(x - settlement.size/2, y - settlement.size/2, settlement.size, settlement.size);
720
+ ctx.strokeStyle = '#333';
721
+ ctx.strokeRect(x - settlement.size/2, y - settlement.size/2, settlement.size, settlement.size);
722
+
723
+ ctx.font = '20px Arial';
724
+ ctx.fillText('🏢', x - 10, y + 5);
725
+ } else if (settlement.type === 'village') {
726
+ // 마을
727
+ ctx.fillStyle = '#8B7355';
728
+ ctx.beginPath();
729
+ ctx.arc(x, y, settlement.size, 0, Math.PI * 2);
730
+ ctx.fill();
731
+
732
+ ctx.font = '16px Arial';
733
+ ctx.fillText('🏘️', x - 8, y + 5);
734
+ } else {
735
+ // 타운
736
+ ctx.fillStyle = '#999';
737
+ ctx.fillRect(x - settlement.size/2, y - settlement.size/2, settlement.size, settlement.size);
738
+
739
+ ctx.font = '14px Arial';
740
+ ctx.fillText('🏪', x - 7, y + 5);
741
+ }
742
+ });
743
+
744
+ // 축척 막대 그리기
745
+ drawScaleBar();
746
  }
747
 
748
+ // Marching squares 알고리즘
749
  function drawContourCell(x, y, width, height, corners, level) {
750
  let state = 0;
751
  if (corners[0] > level) state |= 1;
 
802
  ctx.lineTo(x2, y2);
803
  }
804
 
805
+ // 축척 막대 그리기
806
+ function drawScaleBar() {
807
+ const barWidth = 100;
808
+ const barHeight = 10;
809
+ const x = WIDTH - barWidth - 20;
810
+ const y = HEIGHT - 30;
811
+
812
+ // 실제 거리 계산 (미터)
813
+ const realDistance = (barWidth * currentScale) / 1000; // km로 변환
814
+
815
+ ctx.fillStyle = '#000';
816
+ ctx.fillRect(x, y, barWidth, barHeight);
817
+
818
+ ctx.fillStyle = '#fff';
819
+ ctx.fillRect(x, y, barWidth/2, barHeight);
820
+
821
+ ctx.fillStyle = '#000';
822
+ ctx.font = '12px Arial';
823
+ ctx.fillText(`0`, x - 5, y - 5);
824
+ ctx.fillText(`${realDistance.toFixed(1)}km`, x + barWidth - 20, y - 5);
825
+ }
826
+
827
+ // 지형 색상
828
  function getTerrainColor(height) {
829
+ if (height < 0.15) return '#0d4f8b'; // 물
830
+ if (height < 0.25) return '#1e7eb8'; // 얕은 물
831
+ if (height < 0.35) return '#61a861'; // 평지
832
+ if (height < 0.45) return '#8fc68f'; // 낮은 평지
833
+ if (height < 0.55) return '#c4d4aa'; // 언덕
834
+ if (height < 0.65) return '#f5deb3'; // 높은 언덕
835
+ if (height < 0.75) return '#d2b48c'; // 낮은 산
836
+ if (height < 0.85) return '#8b4513'; // 산
837
+ return '#fff'; // 눈 덮인
838
  }
839
 
840
+ // 마우스 이벤트
841
  canvas.addEventListener('mousemove', (e) => {
842
  const rect = canvas.getBoundingClientRect();
843
  const x = (e.clientX - rect.left) * (WIDTH / rect.width);
 
859
  slope = Math.round(Math.sqrt(dx * dx + dy * dy) * 100);
860
  }
861
 
862
+ // 실제 거리 계산
863
+ const realX = (gridX * currentScale / 1000).toFixed(1);
864
+ const realY = (gridY * currentScale / 1000).toFixed(1);
865
+
866
  document.getElementById('coordinates').textContent = `${gridX}, ${gridY}`;
867
  document.getElementById('elevation').textContent = `${elevation}m`;
868
  document.getElementById('slope').textContent = `${slope}°`;
869
+ document.getElementById('realDistance').textContent = `${realX}, ${realY}km`;
870
 
 
871
  tooltip.style.display = 'block';
872
  tooltip.style.left = e.clientX + 10 + 'px';
873
  tooltip.style.top = e.clientY - 30 + 'px';
 
880
  document.getElementById('coordinates').textContent = '-';
881
  document.getElementById('elevation').textContent = '-';
882
  document.getElementById('slope').textContent = '-';
883
+ document.getElementById('realDistance').textContent = '-';
884
  });
885
 
886
  // 컨트롤 함수들
 
899
  drawMap();
900
  }
901
 
902
+ function toggleRoads() {
903
+ showRoads = !showRoads;
904
+ drawMap();
905
+ }
906
+
907
+ function toggleRivers() {
908
+ showRivers = !showRivers;
909
  drawMap();
910
  }
911
 
 
915
  drawMap();
916
  }
917
 
918
+ function zoomIn() {
919
+ if (currentScaleIndex > 0) {
920
+ currentScaleIndex--;
921
+ currentScale = scales[currentScaleIndex];
922
+ updateScaleInfo();
923
+ drawMap();
924
+ }
925
+ }
926
+
927
+ function zoomOut() {
928
+ if (currentScaleIndex < scales.length - 1) {
929
+ currentScaleIndex++;
930
+ currentScale = scales[currentScaleIndex];
931
+ updateScaleInfo();
932
+ drawMap();
933
+ }
934
+ }
935
+
936
+ function updateScaleInfo() {
937
+ document.getElementById('scaleInfo').textContent = `1:${currentScale}`;
938
+
939
+ // 지도 전체 크기 계산
940
+ const widthKm = (WIDTH * currentScale / 1000000).toFixed(1);
941
+ const heightKm = (HEIGHT * currentScale / 1000000).toFixed(1);
942
+
943
+ // 최대 20km x 15km 제한
944
+ const actualWidthKm = Math.min(20, widthKm);
945
+ const actualHeightKm = Math.min(15, heightKm);
946
+
947
+ document.getElementById('mapSizeInfo').textContent =
948
+ `지도 범위: ${actualWidthKm}km × ${actualHeightKm}km`;
949
+ }
950
+
951
  function exportMap() {
952
  const link = document.createElement('a');
953
+ link.download = `topographic-map-${terrainType}-${Date.now()}.png`;
954
  link.href = canvas.toDataURL();
955
  link.click();
956
  }
 
958
  // 초기화
959
  generateTerrain();
960
  drawMap();
961
+ updateScaleInfo();
962
 
963
  // 화면 크기 변경 대응
964
  window.addEventListener('resize', () => {