terrain / index.html
aiqtech's picture
Update index.html
78ba202 verified
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>디지털 등고선 지형 지도</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.container {
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
padding: 30px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
max-width: 1400px;
width: 100%;
}
h1 {
text-align: center;
color: #333;
margin-bottom: 10px;
font-size: 2em;
background: linear-gradient(135deg, #667eea, #764ba2);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.map-container {
position: relative;
margin: 20px auto;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
background: #f0f0f0;
}
canvas {
display: block;
cursor: crosshair;
width: 100%;
height: auto;
}
.controls {
display: flex;
justify-content: center;
gap: 15px;
margin: 20px 0;
flex-wrap: wrap;
align-items: center;
}
button {
padding: 12px 24px;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border: none;
border-radius: 25px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
}
button:active {
transform: translateY(0);
}
.zoom-controls {
display: flex;
align-items: center;
gap: 10px;
background: white;
padding: 8px 15px;
border-radius: 25px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.zoom-btn {
width: 35px;
height: 35px;
padding: 0;
border-radius: 50%;
font-size: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.scale-info {
font-size: 14px;
color: #666;
font-weight: 600;
min-width: 100px;
text-align: center;
}
.info-panel {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
padding: 15px;
border-radius: 10px;
margin-top: 20px;
display: flex;
justify-content: space-around;
flex-wrap: wrap;
gap: 15px;
}
.info-item {
text-align: center;
padding: 10px;
background: white;
border-radius: 8px;
min-width: 120px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.info-label {
font-size: 12px;
color: #666;
margin-bottom: 5px;
text-transform: uppercase;
letter-spacing: 1px;
}
.info-value {
font-size: 18px;
font-weight: bold;
color: #333;
}
.legend {
display: flex;
align-items: center;
justify-content: center;
gap: 15px;
margin-top: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 10px;
flex-wrap: wrap;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
}
.legend-color {
width: 30px;
height: 20px;
border-radius: 4px;
border: 1px solid #ddd;
}
.legend-symbol {
font-size: 20px;
margin-right: 5px;
}
.tooltip {
position: absolute;
background: rgba(0, 0, 0, 0.9);
color: white;
padding: 8px 12px;
border-radius: 6px;
font-size: 14px;
pointer-events: none;
z-index: 1000;
display: none;
transition: all 0.2s ease;
}
select {
padding: 10px 15px;
border-radius: 8px;
border: 2px solid #667eea;
background: white;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
}
select:hover {
border-color: #764ba2;
}
select:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
}
.map-size-info {
text-align: center;
color: #666;
font-size: 14px;
margin-top: 10px;
}
</style>
</head>
<body>
<div class="container">
<h1>🗺️ 디지털 등고선 지형 지도</h1>
<div class="controls">
<button onclick="generateNewTerrain()">🏔️ 새로운 지형 생성</button>
<button onclick="toggleContours()">📊 등고선 표시/숨기기</button>
<button onclick="toggleHeatmap()">🌡️ 히트맵 전환</button>
<button onclick="toggleRoads()">🛣️ 도로 표시/숨기기</button>
<button onclick="toggleRivers()">💧 하천 표시/숨기기</button>
<select id="terrainType" onchange="changeTerrainType()">
<option value="mountain">⛰️ 산악 지형</option>
<option value="valley">🏞️ 계곡 지형</option>
<option value="plateau">🏔️ 고원 지형</option>
<option value="rural">🏘️ 전원 지형</option>
<option value="urban">🏙️ 도시 평원</option>
</select>
<div class="zoom-controls">
<button class="zoom-btn" onclick="zoomOut()"></button>
<span class="scale-info" id="scaleInfo">1:5000</span>
<button class="zoom-btn" onclick="zoomIn()">+</button>
</div>
<button onclick="exportMap()">💾 지도 저장</button>
</div>
<div class="map-container">
<canvas id="mapCanvas"></canvas>
<div class="tooltip" id="tooltip"></div>
</div>
<div class="map-size-info" id="mapSizeInfo">
지도 범위: 4km × 3km
</div>
<div class="info-panel">
<div class="info-item">
<div class="info-label">좌표</div>
<div class="info-value" id="coordinates">-</div>
</div>
<div class="info-item">
<div class="info-label">고도</div>
<div class="info-value" id="elevation">-</div>
</div>
<div class="info-item">
<div class="info-label">경사도</div>
<div class="info-value" id="slope">-</div>
</div>
<div class="info-item">
<div class="info-label">실제 거리</div>
<div class="info-value" id="realDistance">-</div>
</div>
</div>
<div class="legend">
<div class="legend-item">
<div class="legend-color" style="background: #0d4f8b;"></div>
<span>수역 (0-50m)</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #61a861;"></div>
<span>평지 (50-200m)</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #f5deb3;"></div>
<span>구릉 (200-500m)</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #8b4513;"></div>
<span>산지 (500m+)</span>
</div>
<div class="legend-item">
<span class="legend-symbol"></span>
<span>도로</span>
</div>
<div class="legend-item">
<span class="legend-symbol" style="color: #4682B4;">〰️</span>
<span>하천</span>
</div>
<div class="legend-item">
<span class="legend-symbol">🏘️</span>
<span>마을</span>
</div>
<div class="legend-item">
<span class="legend-symbol">🏢</span>
<span>도시</span>
</div>
</div>
</div>
<script>
const canvas = document.getElementById('mapCanvas');
const ctx = canvas.getContext('2d');
const tooltip = document.getElementById('tooltip');
// 캔버스 크기 설정
const WIDTH = 800;
const HEIGHT = 600;
canvas.width = WIDTH;
canvas.height = HEIGHT;
// 축척 관련 변수
const scales = [2500, 5000, 10000, 25000, 50000];
let currentScaleIndex = 1; // 기본 1:5000
let currentScale = scales[currentScaleIndex];
let zoomLevel = 1; // 줌 레벨 (0.5 ~ 4)
let offsetX = 0; // 뷰포트 오프셋
let offsetY = 0;
// 지형 데이터를 저장할 변수들
let heightMap = [];
let roads = [];
let rivers = [];
let settlements = [];
let showContours = true;
let showHeatmap = false;
let showRoads = true;
let showRivers = true;
let terrainType = 'mountain';
let seed = Math.random() * 10000;
let isDragging = false;
let dragStartX = 0;
let dragStartY = 0;
// 개선된 노이즈 함수
function noise(x, y, scale, octaves, seed) {
let value = 0;
let amplitude = 1;
let frequency = scale;
let maxValue = 0;
for (let i = 0; i < octaves; i++) {
value += amplitude * simpleNoise(x * frequency + seed, y * frequency + seed);
maxValue += amplitude;
amplitude *= 0.5;
frequency *= 2;
}
return value / maxValue;
}
function simpleNoise(x, y) {
const n = Math.sin(x * 12.9898 + y * 78.233) * 43758.5453;
return (n - Math.floor(n)) * 2 - 1;
}
// 지형 생성 함수
function generateTerrain() {
seed = Math.random() * 10000; // 새로운 시드 생성
heightMap = [];
roads = [];
rivers = [];
settlements = [];
const gridSize = 100;
// 높이맵 생성
for (let y = 0; y < gridSize; y++) {
heightMap[y] = [];
for (let x = 0; x < gridSize; x++) {
let height = 0;
switch(terrainType) {
case 'mountain':
height = noise(x, y, 0.02, 6, seed) * 0.6;
height += noise(x, y, 0.05, 4, seed + 100) * 0.3;
height += noise(x, y, 0.1, 2, seed + 200) * 0.1;
height = Math.pow(Math.abs(height), 1.2) * Math.sign(height);
break;
case 'valley':
const distX = (x - gridSize/2) / gridSize;
const distY = (y - gridSize/2) / gridSize;
const dist = Math.sqrt(distX*distX + distY*distY);
height = noise(x, y, 0.03, 4, seed) * 0.5;
height *= (1 - dist * 0.5);
height -= dist * 0.3;
break;
case 'plateau':
height = noise(x, y, 0.02, 3, seed) * 0.3;
if (height > 0.1) height = 0.3 + noise(x, y, 0.05, 2, seed + 100) * 0.1;
break;
case 'rural':
// 전원 지형 - 완만한 구릉과 평지
height = noise(x, y, 0.015, 4, seed) * 0.2;
height += noise(x, y, 0.03, 2, seed + 100) * 0.1;
height = Math.max(0.1, height); // 최소 높이 보장
break;
case 'urban':
// 도시 평원 - 매우 평탄한 지형
height = noise(x, y, 0.01, 2, seed) * 0.1 + 0.15;
break;
}
// 정규화 (0-1 범위)
height = Math.max(0, Math.min(1, (height + 1) / 2));
heightMap[y][x] = height;
}
}
// 도로 생성
generateRoads(gridSize);
// 하천 생성
generateRivers(gridSize);
// 정착지 생성
generateSettlements(gridSize);
}
// 도로 생성
function generateRoads(gridSize) {
roads = [];
const numRoads = terrainType === 'urban' ? 8 : (terrainType === 'rural' ? 5 : 3);
for (let i = 0; i < numRoads; i++) {
const road = [];
let x = Math.random() * gridSize;
let y = Math.random() * gridSize;
const targetX = Math.random() * gridSize;
const targetY = Math.random() * gridSize;
// A* 또는 간단한 경로 찾기
for (let step = 0; step < 50; step++) {
road.push({ x: Math.floor(x), y: Math.floor(y) });
// 낮은 고도 선호
const dx = targetX - x;
const dy = targetY - y;
const angle = Math.atan2(dy, dx);
// 경사를 고려한 이동
let bestAngle = angle;
let minSlope = Infinity;
for (let a = -Math.PI/4; a <= Math.PI/4; a += Math.PI/8) {
const testAngle = angle + a;
const nx = x + Math.cos(testAngle) * 2;
const ny = y + Math.sin(testAngle) * 2;
if (nx >= 0 && nx < gridSize && ny >= 0 && ny < gridSize) {
const slope = Math.abs(heightMap[Math.floor(ny)][Math.floor(nx)] -
heightMap[Math.floor(y)][Math.floor(x)]);
if (slope < minSlope) {
minSlope = slope;
bestAngle = testAngle;
}
}
}
x += Math.cos(bestAngle) * 2;
y += Math.sin(bestAngle) * 2;
if (Math.abs(x - targetX) < 2 && Math.abs(y - targetY) < 2) break;
}
roads.push(road);
}
}
// 하천 생성
function generateRivers(gridSize) {
rivers = [];
const numRivers = terrainType === 'valley' ? 3 : 2;
for (let i = 0; i < numRivers; i++) {
const river = [];
// 높은 곳에서 시작
let x = Math.random() * gridSize;
let y = Math.random() * gridSize;
let highestPoint = heightMap[Math.floor(y)][Math.floor(x)];
// 더 높은 지점 찾기
for (let j = 0; j < 10; j++) {
const tx = Math.random() * gridSize;
const ty = Math.random() * gridSize;
if (heightMap[Math.floor(ty)][Math.floor(tx)] > highestPoint) {
x = tx;
y = ty;
highestPoint = heightMap[Math.floor(ty)][Math.floor(tx)];
}
}
// 낮은 곳으로 흐르기
for (let step = 0; step < 100; step++) {
river.push({ x: Math.floor(x), y: Math.floor(y) });
// 가장 낮은 이웃 찾기
let lowestHeight = heightMap[Math.floor(y)][Math.floor(x)];
let nextX = x, nextY = y;
for (let dx = -1; dx <= 1; dx++) {
for (let dy = -1; dy <= 1; dy++) {
if (dx === 0 && dy === 0) continue;
const nx = Math.floor(x + dx);
const ny = Math.floor(y + dy);
if (nx >= 0 && nx < gridSize && ny >= 0 && ny < gridSize) {
if (heightMap[ny][nx] < lowestHeight) {
lowestHeight = heightMap[ny][nx];
nextX = x + dx + (Math.random() - 0.5) * 0.5;
nextY = y + dy + (Math.random() - 0.5) * 0.5;
}
}
}
}
if (nextX === x && nextY === y) break; // 더 이상 낮은 곳이 없음
x = nextX;
y = nextY;
if (lowestHeight < 0.2) break; // 물에 도달
}
rivers.push(river);
}
}
// 정착지 생성
function generateSettlements(gridSize) {
settlements = [];
if (terrainType === 'rural') {
// 마을 생성
const numVillages = 5 + Math.floor(Math.random() * 5);
for (let i = 0; i < numVillages; i++) {
let x, y;
do {
x = Math.random() * gridSize;
y = Math.random() * gridSize;
} while (heightMap[Math.floor(y)][Math.floor(x)] > 0.5 ||
heightMap[Math.floor(y)][Math.floor(x)] < 0.15);
settlements.push({
x: Math.floor(x),
y: Math.floor(y),
type: 'village',
size: 3 + Math.random() * 3
});
}
} else if (terrainType === 'urban') {
// 도시 생성
const numCities = 2 + Math.floor(Math.random() * 2);
for (let i = 0; i < numCities; i++) {
let x = (i + 1) * gridSize / (numCities + 1) + (Math.random() - 0.5) * 20;
let y = gridSize / 2 + (Math.random() - 0.5) * 30;
settlements.push({
x: Math.floor(x),
y: Math.floor(y),
type: 'city',
size: 10 + Math.random() * 10
});
}
// 도시 주변 작은 마을들
const numTowns = 5 + Math.floor(Math.random() * 5);
for (let i = 0; i < numTowns; i++) {
let x = Math.random() * gridSize;
let y = Math.random() * gridSize;
settlements.push({
x: Math.floor(x),
y: Math.floor(y),
type: 'town',
size: 5 + Math.random() * 5
});
}
}
}
// 지도 그리기
function drawMap() {
ctx.clearRect(0, 0, WIDTH, HEIGHT);
// 줌과 오프셋 적용
ctx.save();
ctx.translate(WIDTH/2, HEIGHT/2);
ctx.scale(zoomLevel, zoomLevel);
ctx.translate(-WIDTH/2 + offsetX, -HEIGHT/2 + offsetY);
const gridSize = heightMap.length;
const cellWidth = WIDTH / gridSize;
const cellHeight = HEIGHT / gridSize;
// 배경 그리기
for (let y = 0; y < gridSize; y++) {
for (let x = 0; x < gridSize; x++) {
const height = heightMap[y][x];
if (showHeatmap) {
const hue = (1 - height) * 240;
ctx.fillStyle = `hsl(${hue}, 70%, 50%)`;
} else {
ctx.fillStyle = getTerrainColor(height);
}
ctx.fillRect(
x * cellWidth,
y * cellHeight,
cellWidth + 1,
cellHeight + 1
);
}
}
// 하천 그리기
if (showRivers) {
rivers.forEach(river => {
ctx.strokeStyle = '#4682B4';
ctx.lineWidth = 2;
ctx.beginPath();
river.forEach((point, index) => {
const x = point.x * cellWidth;
const y = point.y * cellHeight;
if (index === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
});
ctx.stroke();
});
}
// 도로 그리기
if (showRoads) {
roads.forEach(road => {
ctx.strokeStyle = '#555';
ctx.lineWidth = 2;
ctx.setLineDash([5, 3]);
ctx.beginPath();
road.forEach((point, index) => {
const x = point.x * cellWidth;
const y = point.y * cellHeight;
if (index === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
});
ctx.stroke();
ctx.setLineDash([]);
});
}
// 등고선 그리기
if (showContours) {
ctx.strokeStyle = 'rgba(0, 0, 0, 0.3)';
ctx.lineWidth = 1;
const contourLevels = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9];
for (let level of contourLevels) {
ctx.beginPath();
for (let y = 0; y < gridSize - 1; y++) {
for (let x = 0; x < gridSize - 1; x++) {
const corners = [
heightMap[y][x],
heightMap[y][x + 1],
heightMap[y + 1][x + 1],
heightMap[y + 1][x]
];
drawContourCell(
x * cellWidth,
y * cellHeight,
cellWidth,
cellHeight,
corners,
level
);
}
}
if (level % 0.2 === 0) {
ctx.lineWidth = 2;
ctx.strokeStyle = 'rgba(0, 0, 0, 0.5)';
} else {
ctx.lineWidth = 1;
ctx.strokeStyle = 'rgba(0, 0, 0, 0.3)';
}
ctx.stroke();
}
}
// 정착지 그리기
settlements.forEach(settlement => {
const x = settlement.x * cellWidth;
const y = settlement.y * cellHeight;
if (settlement.type === 'city') {
// 도시
ctx.fillStyle = '#666';
ctx.fillRect(x - settlement.size/2, y - settlement.size/2, settlement.size, settlement.size);
ctx.strokeStyle = '#333';
ctx.strokeRect(x - settlement.size/2, y - settlement.size/2, settlement.size, settlement.size);
ctx.font = '20px Arial';
ctx.fillText('🏢', x - 10, y + 5);
} else if (settlement.type === 'village') {
// 마을
ctx.fillStyle = '#8B7355';
ctx.beginPath();
ctx.arc(x, y, settlement.size, 0, Math.PI * 2);
ctx.fill();
ctx.font = '16px Arial';
ctx.fillText('🏘️', x - 8, y + 5);
} else {
// 타운
ctx.fillStyle = '#999';
ctx.fillRect(x - settlement.size/2, y - settlement.size/2, settlement.size, settlement.size);
ctx.font = '14px Arial';
ctx.fillText('🏪', x - 7, y + 5);
}
});
// 축척 막대 그리기
ctx.restore(); // 줌 효과 제거 후 축척 막대 그리기
drawScaleBar();
}
// Marching squares 알고리즘
function drawContourCell(x, y, width, height, corners, level) {
let state = 0;
if (corners[0] > level) state |= 1;
if (corners[1] > level) state |= 2;
if (corners[2] > level) state |= 4;
if (corners[3] > level) state |= 8;
switch(state) {
case 1: case 14:
drawLine(x, y + height * interpolate(corners[0], corners[3], level),
x + width * interpolate(corners[0], corners[1], level), y);
break;
case 2: case 13:
drawLine(x + width * interpolate(corners[0], corners[1], level), y,
x + width, y + height * interpolate(corners[1], corners[2], level));
break;
case 3: case 12:
drawLine(x, y + height * interpolate(corners[0], corners[3], level),
x + width, y + height * interpolate(corners[1], corners[2], level));
break;
case 4: case 11:
drawLine(x + width, y + height * interpolate(corners[1], corners[2], level),
x + width * interpolate(corners[3], corners[2], level), y + height);
break;
case 5:
drawLine(x, y + height * interpolate(corners[0], corners[3], level),
x + width * interpolate(corners[0], corners[1], level), y);
drawLine(x + width, y + height * interpolate(corners[1], corners[2], level),
x + width * interpolate(corners[3], corners[2], level), y + height);
break;
case 6: case 9:
drawLine(x + width * interpolate(corners[0], corners[1], level), y,
x + width * interpolate(corners[3], corners[2], level), y + height);
break;
case 7: case 8:
drawLine(x, y + height * interpolate(corners[0], corners[3], level),
x + width * interpolate(corners[3], corners[2], level), y + height);
break;
case 10:
drawLine(x + width * interpolate(corners[0], corners[1], level), y,
x + width, y + height * interpolate(corners[1], corners[2], level));
drawLine(x, y + height * interpolate(corners[0], corners[3], level),
x + width * interpolate(corners[3], corners[2], level), y + height);
break;
}
}
function interpolate(v1, v2, level) {
return (level - v1) / (v2 - v1);
}
function drawLine(x1, y1, x2, y2) {
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
}
// 축척 막대 그리기
function drawScaleBar() {
const barWidth = 100;
const barHeight = 10;
const x = WIDTH - barWidth - 20;
const y = HEIGHT - 30;
// 실제 거리 계산 (미터)
const realDistance = (barWidth * currentScale) / 1000; // km로 변환
ctx.fillStyle = '#000';
ctx.fillRect(x, y, barWidth, barHeight);
ctx.fillStyle = '#fff';
ctx.fillRect(x, y, barWidth/2, barHeight);
ctx.fillStyle = '#000';
ctx.font = '12px Arial';
ctx.fillText(`0`, x - 5, y - 5);
ctx.fillText(`${realDistance.toFixed(1)}km`, x + barWidth - 20, y - 5);
}
// 지형 색상
function getTerrainColor(height) {
if (height < 0.15) return '#0d4f8b'; // 물
if (height < 0.25) return '#1e7eb8'; // 얕은 물
if (height < 0.35) return '#61a861'; // 평지
if (height < 0.45) return '#8fc68f'; // 낮은 평지
if (height < 0.55) return '#c4d4aa'; // 언덕
if (height < 0.65) return '#f5deb3'; // 높은 언덕
if (height < 0.75) return '#d2b48c'; // 낮은 산
if (height < 0.85) return '#8b4513'; // 산
return '#fff'; // 눈 덮인 산
}
// 마우스 이벤트
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
if (isDragging) {
const dx = e.clientX - dragStartX;
const dy = e.clientY - dragStartY;
offsetX = dx / zoomLevel;
offsetY = dy / zoomLevel;
drawMap();
return;
}
// 줌을 고려한 마우스 좌표 계산
const x = ((e.clientX - rect.left) * (WIDTH / rect.width) - WIDTH/2) / zoomLevel + WIDTH/2 - offsetX;
const y = ((e.clientY - rect.top) * (HEIGHT / rect.height) - HEIGHT/2) / zoomLevel + HEIGHT/2 - offsetY;
const gridSize = heightMap.length;
const gridX = Math.floor(x / (WIDTH / gridSize));
const gridY = Math.floor(y / (HEIGHT / gridSize));
if (gridX >= 0 && gridX < gridSize && gridY >= 0 && gridY < gridSize) {
const height = heightMap[gridY][gridX];
const elevation = Math.round(height * 1000);
// 경사도 계산
let slope = 0;
if (gridX > 0 && gridX < gridSize - 1 && gridY > 0 && gridY < gridSize - 1) {
const dx = heightMap[gridY][gridX + 1] - heightMap[gridY][gridX - 1];
const dy = heightMap[gridY + 1][gridX] - heightMap[gridY - 1][gridX];
slope = Math.round(Math.sqrt(dx * dx + dy * dy) * 100);
}
// 실제 거리 계산 (줌 레벨 고려)
const mapWidthKm = (WIDTH * currentScale / zoomLevel) / 1000000;
const mapHeightKm = (HEIGHT * currentScale / zoomLevel) / 1000000;
const realX = (gridX * mapWidthKm / gridSize).toFixed(2);
const realY = (gridY * mapHeightKm / gridSize).toFixed(2);
document.getElementById('coordinates').textContent = `${gridX}, ${gridY}`;
document.getElementById('elevation').textContent = `${elevation}m`;
document.getElementById('slope').textContent = `${slope}°`;
document.getElementById('realDistance').textContent = `${realX}, ${realY}km`;
tooltip.style.display = 'block';
tooltip.style.left = e.clientX + 10 + 'px';
tooltip.style.top = e.clientY - 30 + 'px';
tooltip.textContent = `고도: ${elevation}m`;
}
});
// 드래그 이벤트
canvas.addEventListener('mousedown', (e) => {
if (e.button === 0) { // 왼쪽 버튼
isDragging = true;
dragStartX = e.clientX - offsetX * zoomLevel;
dragStartY = e.clientY - offsetY * zoomLevel;
canvas.style.cursor = 'grabbing';
}
});
canvas.addEventListener('mouseup', () => {
isDragging = false;
canvas.style.cursor = 'crosshair';
});
canvas.addEventListener('mouseleave', () => {
isDragging = false;
canvas.style.cursor = 'crosshair';
tooltip.style.display = 'none';
document.getElementById('coordinates').textContent = '-';
document.getElementById('elevation').textContent = '-';
document.getElementById('slope').textContent = '-';
document.getElementById('realDistance').textContent = '-';
});
// 마우스 휠 줌
canvas.addEventListener('wheel', (e) => {
e.preventDefault();
const delta = e.deltaY > 0 ? 0.9 : 1.1;
const newZoom = zoomLevel * delta;
if (newZoom >= 0.5 && newZoom <= 4) {
zoomLevel = newZoom;
updateScaleInfo();
drawMap();
}
});
// 컨트롤 함수들
function generateNewTerrain() {
generateTerrain();
drawMap();
}
function toggleContours() {
showContours = !showContours;
drawMap();
}
function toggleHeatmap() {
showHeatmap = !showHeatmap;
drawMap();
}
function toggleRoads() {
showRoads = !showRoads;
drawMap();
}
function toggleRivers() {
showRivers = !showRivers;
drawMap();
}
function changeTerrainType() {
terrainType = document.getElementById('terrainType').value;
generateTerrain();
drawMap();
}
function zoomIn() {
if (currentScaleIndex > 0) {
currentScaleIndex--;
currentScale = scales[currentScaleIndex];
zoomLevel = Math.min(4, zoomLevel * 1.5);
offsetX = 0;
offsetY = 0;
updateScaleInfo();
drawMap();
}
}
function zoomOut() {
if (currentScaleIndex < scales.length - 1) {
currentScaleIndex++;
currentScale = scales[currentScaleIndex];
zoomLevel = Math.max(0.5, zoomLevel / 1.5);
offsetX = 0;
offsetY = 0;
updateScaleInfo();
drawMap();
}
}
function updateScaleInfo() {
const effectiveScale = currentScale / zoomLevel;
document.getElementById('scaleInfo').textContent = `1:${Math.round(effectiveScale)}`;
// 지도 전체 크기 계산 (줌 고려)
const widthKm = (WIDTH * effectiveScale / 1000000).toFixed(1);
const heightKm = (HEIGHT * effectiveScale / 1000000).toFixed(1);
// 최대 20km x 15km 제한
const actualWidthKm = Math.min(20, widthKm);
const actualHeightKm = Math.min(15, heightKm);
document.getElementById('mapSizeInfo').textContent =
`지도 범위: ${actualWidthKm}km × ${actualHeightKm}km (줌: ${(zoomLevel * 100).toFixed(0)}%)`;
}
function exportMap() {
const link = document.createElement('a');
link.download = `topographic-map-${terrainType}-${Date.now()}.png`;
link.href = canvas.toDataURL();
link.click();
}
// 초기화
generateTerrain();
drawMap();
updateScaleInfo();
// 화면 크기 변경 대응
window.addEventListener('resize', () => {
drawMap();
});
</script>
</body>
</html>