Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>World Map Editor</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/chroma.min.js"></script> | |
<script src="https://unpkg.com/[email protected]/countries-110m.json"></script> | |
<script src="https://d3js.org/d3.v7.min.js"></script> | |
<script src="https://d3js.org/topojson.v3.min.js"></script> | |
<style> | |
.country { | |
stroke: #333; | |
stroke-width: 0.5px; | |
transition: all 0.3s ease; | |
} | |
.country:hover { | |
stroke-width: 1.5px; | |
stroke: #000; | |
} | |
.color-picker { | |
-webkit-appearance: none; | |
-moz-appearance: none; | |
appearance: none; | |
width: 40px; | |
height: 40px; | |
background-color: transparent; | |
border: none; | |
cursor: pointer; | |
} | |
.color-picker::-webkit-color-swatch { | |
border-radius: 8px; | |
border: 2px solid #ccc; | |
} | |
.color-picker::-moz-color-swatch { | |
border-radius: 8px; | |
border: 2px solid #ccc; | |
} | |
.tooltip { | |
position: absolute; | |
padding: 8px; | |
background: rgba(0, 0, 0, 0.8); | |
color: white; | |
border-radius: 4px; | |
pointer-events: none; | |
font-size: 12px; | |
z-index: 10; | |
} | |
.zoom-controls { | |
position: absolute; | |
top: 20px; | |
right: 20px; | |
z-index: 100; | |
display: flex; | |
flex-direction: column; | |
gap: 8px; | |
} | |
.zoom-btn { | |
width: 36px; | |
height: 36px; | |
border-radius: 50%; | |
background: white; | |
border: 1px solid #ddd; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
cursor: pointer; | |
box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
font-size: 18px; | |
font-weight: bold; | |
} | |
.zoom-btn:hover { | |
background: #f0f0f0; | |
} | |
</style> | |
</head> | |
<body class="bg-gray-100 min-h-screen"> | |
<div class="container mx-auto px-4 py-8"> | |
<header class="mb-8"> | |
<h1 class="text-4xl font-bold text-center text-blue-800 mb-2">World Map Editor</h1> | |
<p class="text-center text-gray-600">Customize your world map - color countries, adjust borders, and create your perfect visualization</p> | |
</header> | |
<div class="flex flex-col lg:flex-row gap-8"> | |
<!-- Controls Panel --> | |
<div class="w-full lg:w-1/4 bg-white p-6 rounded-lg shadow-md"> | |
<h2 class="text-xl font-semibold mb-4 text-gray-800">Controls</h2> | |
<div class="mb-6"> | |
<h3 class="font-medium mb-2 text-gray-700">Borders</h3> | |
<div class="flex flex-wrap gap-2"> | |
<button id="showAllBorders" class="px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600 transition">Show All</button> | |
<button id="hideAllBorders" class="px-3 py-1 bg-red-500 text-white rounded hover:bg-red-600 transition">Hide All</button> | |
<button id="toggleBorders" class="px-3 py-1 bg-gray-500 text-white rounded hover:bg-gray-600 transition">Toggle</button> | |
</div> | |
<div class="mt-3"> | |
<label class="flex items-center"> | |
<input type="range" id="borderWidth" min="0" max="5" step="0.5" value="0.5" class="w-full"> | |
<span class="ml-2 text-sm text-gray-600">Border width: <span id="borderWidthValue">0.5</span>px</span> | |
</label> | |
</div> | |
</div> | |
<div class="mb-6"> | |
<h3 class="font-medium mb-2 text-gray-700">Country Color</h3> | |
<div class="flex items-center gap-3 mb-3"> | |
<input type="color" id="countryColor" value="#4CAF50" class="color-picker"> | |
<button id="applyColor" class="px-3 py-1 bg-green-500 text-white rounded hover:bg-green-600 transition">Apply</button> | |
<button id="resetColors" class="px-3 py-1 bg-gray-500 text-white rounded hover:bg-gray-600 transition">Reset</button> | |
</div> | |
<div class="flex items-center gap-2"> | |
<input type="checkbox" id="randomColors" class="h-4 w-4"> | |
<label for="randomColors" class="text-sm text-gray-600">Random colors</label> | |
</div> | |
</div> | |
<div class="mb-6"> | |
<h3 class="font-medium mb-2 text-gray-700">Background</h3> | |
<input type="color" id="bgColor" value="#F3F4F6" class="color-picker"> | |
<span class="ml-2 text-sm text-gray-600">Map background</span> | |
</div> | |
<div class="mb-6"> | |
<h3 class="font-medium mb-2 text-gray-700">Selected Country</h3> | |
<div id="selectedCountryInfo" class="p-3 bg-gray-100 rounded text-sm text-gray-700"> | |
Click on a country to select it | |
</div> | |
</div> | |
<div> | |
<h3 class="font-medium mb-2 text-gray-700">Save/Load</h3> | |
<div class="flex flex-wrap gap-2"> | |
<button id="saveMap" class="px-3 py-1 bg-purple-500 text-white rounded hover:bg-purple-600 transition">Save Map</button> | |
<button id="loadMap" class="px-3 py-1 bg-yellow-500 text-white rounded hover:bg-yellow-600 transition">Load Map</button> | |
<button id="exportPNG" class="px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600 transition">Export PNG</button> | |
</div> | |
</div> | |
</div> | |
<!-- Map Container --> | |
<div class="w-full lg:w-3/4 relative"> | |
<div class="bg-white p-4 rounded-lg shadow-md"> | |
<div id="mapContainer" class="relative overflow-hidden border border-gray-300 rounded"> | |
<svg id="map" width="100%" height="600"></svg> | |
<div id="tooltip" class="tooltip"></div> | |
<div class="zoom-controls"> | |
<button id="zoomIn" class="zoom-btn">+</button> | |
<button id="zoomOut" class="zoom-btn">−</button> | |
<button id="resetZoom" class="zoom-btn">↻</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<script> | |
// Initialize variables | |
let selectedCountry = null; | |
let countryColors = {}; | |
let showBorders = true; | |
let borderWidth = 0.5; | |
let transform = d3.zoomIdentity; | |
let svg, g, projection, path; | |
// Wait for DOM to load | |
document.addEventListener('DOMContentLoaded', function() { | |
// Initialize map | |
initMap(); | |
// Set up event listeners | |
setupEventListeners(); | |
}); | |
function initMap() { | |
const width = document.getElementById('mapContainer').clientWidth; | |
const height = 600; | |
// Create SVG | |
svg = d3.select("#map") | |
.attr("width", width) | |
.attr("height", height) | |
.call(d3.zoom() | |
.scaleExtent([1, 8]) | |
.on("zoom", zoomed)) | |
.on("click", stopped, true); | |
// Create group for all map elements | |
g = svg.append("g"); | |
// Set up projection | |
projection = d3.geoMercator() | |
.scale(width / 2 / Math.PI) | |
.translate([width / 2, height / 2]); | |
// Create path generator | |
path = d3.geoPath().projection(projection); | |
// Load and draw world map | |
d3.json("https://unpkg.com/[email protected]/countries-110m.json").then(function(world) { | |
const countries = topojson.feature(world, world.objects.countries).features; | |
// Draw countries | |
g.selectAll(".country") | |
.data(countries) | |
.enter() | |
.append("path") | |
.attr("class", "country") | |
.attr("d", path) | |
.attr("id", d => "country-" + d.id) | |
.attr("fill", d => getRandomColor(d.id)) | |
.attr("stroke", "#333") | |
.attr("stroke-width", borderWidth) | |
.on("mouseover", function(event, d) { | |
d3.select(this).attr("stroke-width", borderWidth * 2); | |
showTooltip(event, d); | |
}) | |
.on("mouseout", function() { | |
if (selectedCountry !== this) { | |
d3.select(this).attr("stroke-width", borderWidth); | |
} | |
hideTooltip(); | |
}) | |
.on("click", function(event, d) { | |
// Reset previously selected country | |
if (selectedCountry) { | |
d3.select(selectedCountry).attr("stroke-width", borderWidth); | |
} | |
// Set new selected country | |
selectedCountry = this; | |
d3.select(this).attr("stroke-width", borderWidth * 3); | |
// Update selected country info | |
updateSelectedCountryInfo(d); | |
}); | |
// Store country data for later use | |
window.countriesData = countries; | |
}); | |
} | |
function setupEventListeners() { | |
// Border controls | |
document.getElementById('showAllBorders').addEventListener('click', function() { | |
showBorders = true; | |
updateBorders(); | |
}); | |
document.getElementById('hideAllBorders').addEventListener('click', function() { | |
showBorders = false; | |
updateBorders(); | |
}); | |
document.getElementById('toggleBorders').addEventListener('click', function() { | |
showBorders = !showBorders; | |
updateBorders(); | |
}); | |
document.getElementById('borderWidth').addEventListener('input', function() { | |
borderWidth = parseFloat(this.value); | |
document.getElementById('borderWidthValue').textContent = borderWidth; | |
updateBorders(); | |
}); | |
// Color controls | |
document.getElementById('applyColor').addEventListener('click', function() { | |
if (!selectedCountry) { | |
alert("Please select a country first"); | |
return; | |
} | |
const color = document.getElementById('countryColor').value; | |
const countryId = selectedCountry.id.replace('country-', ''); | |
// Store color for this country | |
countryColors[countryId] = color; | |
// Apply color to selected country | |
d3.select(selectedCountry).attr("fill", color); | |
// Update info panel | |
updateSelectedCountryInfo(); | |
}); | |
document.getElementById('resetColors').addEventListener('click', function() { | |
countryColors = {}; | |
d3.selectAll(".country").attr("fill", d => getRandomColor(d.id)); | |
updateSelectedCountryInfo(); | |
}); | |
document.getElementById('randomColors').addEventListener('change', function() { | |
if (this.checked) { | |
d3.selectAll(".country").attr("fill", d => getRandomColor(d.id)); | |
} | |
}); | |
// Background color | |
document.getElementById('bgColor').addEventListener('input', function() { | |
document.getElementById('mapContainer').style.backgroundColor = this.value; | |
}); | |
// Zoom controls | |
document.getElementById('zoomIn').addEventListener('click', function() { | |
svg.transition().call(d3.zoom().scaleBy, 2); | |
}); | |
document.getElementById('zoomOut').addEventListener('click', function() { | |
svg.transition().call(d3.zoom().scaleBy, 0.5); | |
}); | |
document.getElementById('resetZoom').addEventListener('click', function() { | |
svg.transition().call(d3.zoom().transform, d3.zoomIdentity); | |
}); | |
// Save/Load | |
document.getElementById('saveMap').addEventListener('click', saveMap); | |
document.getElementById('loadMap').addEventListener('click', loadMap); | |
document.getElementById('exportPNG').addEventListener('click', exportPNG); | |
} | |
function updateBorders() { | |
const strokeValue = showBorders ? "#333" : "none"; | |
d3.selectAll(".country") | |
.attr("stroke", strokeValue) | |
.attr("stroke-width", showBorders ? borderWidth : 0); | |
if (selectedCountry) { | |
d3.select(selectedCountry).attr("stroke-width", showBorders ? borderWidth * 3 : 0); | |
} | |
} | |
function updateSelectedCountryInfo(d) { | |
const infoPanel = document.getElementById('selectedCountryInfo'); | |
if (!selectedCountry) { | |
infoPanel.innerHTML = "Click on a country to select it"; | |
return; | |
} | |
const countryId = selectedCountry.id.replace('country-', ''); | |
const countryData = window.countriesData.find(c => c.id == countryId); | |
const countryName = countryData?.properties?.name || "Unknown Country"; | |
const currentColor = countryColors[countryId] || d3.select(selectedCountry).attr("fill"); | |
infoPanel.innerHTML = ` | |
<strong>${countryName}</strong><br> | |
<div class="mt-1 flex items-center"> | |
<span>Color: </span> | |
<div class="w-4 h-4 ml-2 border border-gray-300" style="background-color: ${currentColor}"></div> | |
<span class="ml-1">${currentColor}</span> | |
</div> | |
<div class="mt-1">ID: ${countryId}</div> | |
`; | |
} | |
function showTooltip(event, d) { | |
const tooltip = document.getElementById('tooltip'); | |
const countryName = d.properties?.name || "Unknown Country"; | |
tooltip.style.display = 'block'; | |
tooltip.style.left = (event.pageX + 10) + 'px'; | |
tooltip.style.top = (event.pageY + 10) + 'px'; | |
tooltip.innerHTML = countryName; | |
} | |
function hideTooltip() { | |
document.getElementById('tooltip').style.display = 'none'; | |
} | |
function getRandomColor(id) { | |
// If we have a stored color for this country, use it | |
if (countryColors[id]) { | |
return countryColors[id]; | |
} | |
// Otherwise generate a random color or use default | |
if (document.getElementById('randomColors')?.checked) { | |
return chroma.random().hex(); | |
} | |
return "#DDD"; | |
} | |
function zoomed(event) { | |
transform = event.transform; | |
g.attr("transform", transform); | |
g.attr("stroke-width", borderWidth / transform.k); | |
} | |
function stopped() { | |
if (d3.event.defaultPrevented) return; | |
if (!d3.event.target.closest('.country')) { | |
// Clicked on empty space - deselect country | |
if (selectedCountry) { | |
d3.select(selectedCountry).attr("stroke-width", borderWidth); | |
selectedCountry = null; | |
updateSelectedCountryInfo(); | |
} | |
} | |
} | |
function saveMap() { | |
const mapData = { | |
countryColors: countryColors, | |
showBorders: showBorders, | |
borderWidth: borderWidth, | |
bgColor: document.getElementById('bgColor').value, | |
transform: transform | |
}; | |
const dataStr = JSON.stringify(mapData); | |
localStorage.setItem('worldMapEditorData', dataStr); | |
alert("Map saved successfully!"); | |
} | |
function loadMap() { | |
const savedData = localStorage.getItem('worldMapEditorData'); | |
if (!savedData) { | |
alert("No saved map found"); | |
return; | |
} | |
try { | |
const mapData = JSON.parse(savedData); | |
// Apply saved settings | |
countryColors = mapData.countryColors || {}; | |
showBorders = mapData.showBorders !== false; | |
borderWidth = mapData.borderWidth || 0.5; | |
document.getElementById('borderWidth').value = borderWidth; | |
document.getElementById('borderWidthValue').textContent = borderWidth; | |
document.getElementById('bgColor').value = mapData.bgColor || "#F3F4F6"; | |
document.getElementById('mapContainer').style.backgroundColor = mapData.bgColor || "#F3F4F6"; | |
// Apply colors to countries | |
d3.selectAll(".country").each(function() { | |
const countryId = this.id.replace('country-', ''); | |
if (countryColors[countryId]) { | |
d3.select(this).attr("fill", countryColors[countryId]); | |
} | |
}); | |
// Apply borders | |
updateBorders(); | |
// Apply transform if available | |
if (mapData.transform) { | |
transform = mapData.transform; | |
g.attr("transform", transform); | |
g.attr("stroke-width", borderWidth / transform.k); | |
} | |
alert("Map loaded successfully!"); | |
} catch (e) { | |
console.error("Error loading map:", e); | |
alert("Error loading saved map"); | |
} | |
} | |
function exportPNG() { | |
const svgElement = document.getElementById('map'); | |
const svgData = new XMLSerializer().serializeToString(svgElement); | |
const canvas = document.createElement('canvas'); | |
const ctx = canvas.getContext('2d'); | |
const img = new Image(); | |
img.onload = function() { | |
canvas.width = img.width; | |
canvas.height = img.height; | |
ctx.drawImage(img, 0, 0); | |
const pngFile = canvas.toDataURL('image/png'); | |
const downloadLink = document.createElement('a'); | |
downloadLink.download = 'world-map.png'; | |
downloadLink.href = pngFile; | |
downloadLink.click(); | |
}; | |
img.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgData))); | |
} | |
</script> | |
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Kingrane/draw-your-world" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
</html> |