|
<!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; |
|
let currentScale = scales[currentScaleIndex]; |
|
let zoomLevel = 1; |
|
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; |
|
} |
|
|
|
|
|
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; |
|
|
|
|
|
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(); |
|
} |
|
|
|
|
|
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; |
|
|
|
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); |
|
|
|
|
|
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> |