web-map/地图互转工具.html

1673 lines
50 KiB
HTML
Raw Normal View History

2025-06-24 09:44:14 +08:00
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>地图坐标转换工具</title>
<link
rel="icon"
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🗺️</text></svg>"
/>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--secondary-gradient: linear-gradient(135deg, #28a745 0%, #20c997 100%);
--danger-gradient: linear-gradient(135deg, #dc3545 0%, #e83e8c 100%);
--shadow-light: 0 5px 15px rgba(0, 0, 0, 0.1);
--shadow-medium: 0 10px 25px rgba(0, 0, 0, 0.15);
--shadow-heavy: 0 20px 40px rgba(0, 0, 0, 0.1);
--border-radius: 15px;
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: var(--primary-gradient);
min-height: 100vh;
padding: 20px;
animation: backgroundShift 10s ease-in-out infinite alternate;
}
@keyframes backgroundShift {
0% {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
100% {
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
}
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 20px;
box-shadow: var(--shadow-heavy);
overflow: hidden;
animation: slideInUp 0.6s ease-out;
}
@keyframes slideInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.header {
background: var(--primary-gradient);
color: white;
padding: 40px 30px;
text-align: center;
position: relative;
overflow: hidden;
}
.header::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 1px, transparent 1px);
background-size: 50px 50px;
animation: float 20s ease-in-out infinite;
}
@keyframes float {
0%,
100% {
transform: translate(0, 0) rotate(0deg);
}
50% {
transform: translate(-20px, -20px) rotate(180deg);
}
}
.header h1 {
font-size: 2.8rem;
margin-bottom: 15px;
font-weight: 700;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
position: relative;
z-index: 1;
}
.header p {
font-size: 1.2rem;
opacity: 0.95;
position: relative;
z-index: 1;
font-weight: 300;
}
.content {
padding: 50px 40px;
}
.upload-section {
background: linear-gradient(145deg, #f8f9fa 0%, #e9ecef 100%);
border-radius: var(--border-radius);
padding: 40px;
margin-bottom: 40px;
border: 3px dashed #dee2e6;
transition: var(--transition);
position: relative;
overflow: hidden;
}
.upload-section::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(102, 126, 234, 0.1), transparent);
transition: left 0.5s;
}
.upload-section:hover {
border-color: #667eea;
background: linear-gradient(145deg, #f0f4ff 0%, #e7f3ff 100%);
transform: translateY(-2px);
box-shadow: var(--shadow-medium);
}
.upload-section:hover::before {
left: 100%;
}
.upload-section.dragover {
border-color: #667eea;
background: linear-gradient(145deg, #e7f3ff 0%, #d4e9ff 100%);
transform: scale(1.02);
box-shadow: 0 15px 35px rgba(102, 126, 234, 0.2);
}
.upload-area {
text-align: center;
padding: 30px;
position: relative;
z-index: 1;
}
.upload-icon {
font-size: 4rem;
color: #667eea;
margin-bottom: 25px;
animation: bounce 2s ease-in-out infinite;
}
@keyframes bounce {
0%,
20%,
50%,
80%,
100% {
transform: translateY(0);
}
40% {
transform: translateY(-10px);
}
60% {
transform: translateY(-5px);
}
}
.file-input {
display: none;
}
.upload-btn {
background: var(--primary-gradient);
color: white;
padding: 15px 35px;
border: none;
border-radius: 30px;
font-size: 1.1rem;
font-weight: 600;
cursor: pointer;
transition: var(--transition);
box-shadow: var(--shadow-light);
position: relative;
overflow: hidden;
}
.upload-btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.5s;
}
.upload-btn:hover {
transform: translateY(-3px);
box-shadow: var(--shadow-medium);
}
.upload-btn:hover::before {
left: 100%;
}
.upload-btn:active {
transform: translateY(-1px);
}
.conversion-section {
display: none;
background: linear-gradient(145deg, #f8f9fa 0%, #e9ecef 100%);
border-radius: var(--border-radius);
padding: 40px;
margin-bottom: 40px;
animation: slideInUp 0.6s ease-out;
}
.conversion-options {
display: flex;
gap: 20px;
margin-bottom: 30px;
justify-content: center;
flex-wrap: wrap;
}
.option-card {
background: white;
border-radius: 12px;
padding: 25px;
box-shadow: var(--shadow-light);
cursor: pointer;
transition: var(--transition);
flex: 1;
min-width: 250px;
max-width: 300px;
text-align: center;
position: relative;
overflow: hidden;
border: 2px solid transparent;
}
.option-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--primary-gradient);
opacity: 0;
transition: opacity 0.3s ease;
}
.option-card > * {
position: relative;
z-index: 1;
}
.option-card:hover {
transform: translateY(-8px);
box-shadow: var(--shadow-medium);
border-color: #667eea;
}
.option-card.selected {
border-color: #667eea;
transform: translateY(-8px);
box-shadow: var(--shadow-medium);
}
.option-card.selected::before {
opacity: 1;
}
.option-card.selected .option-title,
.option-card.selected .option-desc {
color: white;
}
.option-title {
font-size: 1.4rem;
font-weight: 700;
margin-bottom: 12px;
color: #333;
transition: color 0.3s ease;
}
.option-desc {
font-size: 0.95rem;
opacity: 0.8;
line-height: 1.5;
color: #666;
transition: color 0.3s ease;
}
.convert-btn {
background: var(--secondary-gradient);
color: white;
padding: 18px 45px;
border: none;
border-radius: 30px;
font-size: 1.3rem;
font-weight: 600;
cursor: pointer;
display: block;
margin: 30px auto;
transition: var(--transition);
box-shadow: var(--shadow-light);
position: relative;
overflow: hidden;
}
.convert-btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.5s;
}
.convert-btn:hover:not(:disabled) {
transform: translateY(-3px);
box-shadow: var(--shadow-medium);
}
.convert-btn:hover:not(:disabled)::before {
left: 100%;
}
.convert-btn:disabled {
background: linear-gradient(135deg, #6c757d 0%, #495057 100%);
cursor: not-allowed;
transform: none;
opacity: 0.6;
}
.progress-bar {
display: none;
background: #e9ecef;
height: 12px;
border-radius: 6px;
overflow: hidden;
margin: 25px 0;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
}
.progress-fill {
height: 100%;
background: var(--primary-gradient);
width: 0%;
transition: width 0.3s ease;
position: relative;
overflow: hidden;
}
.progress-fill::after {
content: '';
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
animation: progressShine 1.5s ease-in-out infinite;
}
@keyframes progressShine {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
.result-section {
display: none;
background: linear-gradient(145deg, #d4edda 0%, #c3e6cb 100%);
border: 2px solid #28a745;
border-radius: var(--border-radius);
padding: 40px;
margin-top: 40px;
animation: slideInUp 0.6s ease-out;
position: relative;
overflow: hidden;
}
.result-section::before {
content: '✨';
position: absolute;
top: 20px;
right: 20px;
font-size: 2rem;
animation: sparkle 2s ease-in-out infinite;
}
@keyframes sparkle {
0%,
100% {
transform: scale(1) rotate(0deg);
opacity: 0.7;
}
50% {
transform: scale(1.2) rotate(180deg);
opacity: 1;
}
}
.download-btn {
background: var(--danger-gradient);
color: white;
padding: 15px 35px;
border: none;
border-radius: 30px;
font-size: 1.2rem;
font-weight: 600;
cursor: pointer;
transition: var(--transition);
box-shadow: var(--shadow-light);
position: relative;
overflow: hidden;
}
.download-btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.5s;
}
.download-btn:hover {
transform: translateY(-3px);
box-shadow: var(--shadow-medium);
}
.download-btn:hover::before {
left: 100%;
}
.file-info {
background: white;
border-radius: 12px;
padding: 25px;
margin-top: 25px;
box-shadow: var(--shadow-light);
border: 1px solid #e9ecef;
transition: var(--transition);
}
.file-info:hover {
box-shadow: var(--shadow-medium);
transform: translateY(-2px);
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding: 8px 0;
border-bottom: 1px solid #f8f9fa;
transition: var(--transition);
}
.info-item:hover {
background: #f8f9fa;
padding: 8px 12px;
border-radius: 6px;
margin: 4px -12px 12px -12px;
}
.info-item:last-child {
border-bottom: none;
margin-bottom: 0;
}
.info-item span:first-child {
font-weight: 600;
color: #495057;
}
.info-item span:last-child {
color: #667eea;
font-weight: 500;
}
.alert {
padding: 18px 25px;
margin: 25px 0;
border-radius: 12px;
display: none;
font-weight: 500;
position: relative;
animation: slideInUp 0.4s ease-out;
border-left: 4px solid;
}
.alert::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
border-radius: 2px;
}
.alert-error {
background: linear-gradient(145deg, #f8d7da 0%, #f5c6cb 100%);
color: #721c24;
border-left-color: #dc3545;
}
.alert-error::before {
background: #dc3545;
}
.alert-success {
background: linear-gradient(145deg, #d4edda 0%, #c3e6cb 100%);
color: #155724;
border-left-color: #28a745;
}
.alert-success::before {
background: #28a745;
}
.scale-section {
background: white;
border-radius: 12px;
padding: 30px;
margin: 25px 0;
box-shadow: var(--shadow-light);
transition: var(--transition);
border: 1px solid #e9ecef;
}
.scale-section:hover {
box-shadow: var(--shadow-medium);
transform: translateY(-2px);
}
.scale-controls {
display: flex;
align-items: center;
justify-content: center;
gap: 20px;
flex-wrap: wrap;
}
.scale-controls label {
font-weight: 700;
color: #495057;
font-size: 1.1rem;
}
.scale-controls input {
padding: 12px 16px;
border: 2px solid #dee2e6;
border-radius: 10px;
font-size: 1.1rem;
width: 140px;
text-align: center;
transition: var(--transition);
font-weight: 600;
}
.scale-controls input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.15);
transform: scale(1.05);
}
.scale-hint {
font-size: 0.95rem;
color: #6c757d;
font-style: italic;
text-align: center;
margin-top: 15px;
line-height: 1.5;
}
.quick-scale-buttons {
display: flex;
gap: 10px;
margin-top: 15px;
flex-wrap: wrap;
justify-content: center;
}
.quick-btn {
padding: 8px 16px;
background: linear-gradient(145deg, #f8f9fa 0%, #e9ecef 100%);
border: 2px solid #dee2e6;
border-radius: 8px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 600;
transition: var(--transition);
color: #495057;
}
.quick-btn:hover {
background: var(--primary-gradient);
color: white;
border-color: #667eea;
transform: translateY(-2px);
box-shadow: var(--shadow-light);
}
.quick-btn:active {
transform: translateY(0);
}
.drag-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(145deg, rgba(102, 126, 234, 0.15) 0%, rgba(118, 75, 162, 0.15) 100%);
border-radius: var(--border-radius);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.4rem;
font-weight: 700;
color: #667eea;
opacity: 0;
pointer-events: none;
transition: var(--transition);
backdrop-filter: blur(5px);
}
.upload-section.dragover .drag-overlay {
opacity: 1;
}
/* 加载状态 */
.loading {
position: relative;
}
.loading::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 20px;
height: 20px;
border: 2px solid #ffffff;
border-top: 2px solid transparent;
border-radius: 50%;
animation: spin 1s linear infinite;
transform: translate(-50%, -50%);
}
@keyframes spin {
0% {
transform: translate(-50%, -50%) rotate(0deg);
}
100% {
transform: translate(-50%, -50%) rotate(360deg);
}
}
/* 响应式设计优化 */
@media (max-width: 768px) {
body {
padding: 10px;
}
.header {
padding: 30px 20px;
}
.header h1 {
font-size: 2.2rem;
}
.header p {
font-size: 1rem;
}
.content {
padding: 30px 20px;
}
.upload-section,
.conversion-section,
.scale-section {
padding: 25px 20px;
}
.conversion-options {
flex-direction: column;
gap: 15px;
}
.option-card {
min-width: auto;
max-width: none;
}
.scale-controls {
flex-direction: column;
gap: 15px;
}
.scale-controls input {
width: 200px;
}
.quick-scale-buttons {
gap: 8px;
}
.quick-btn {
padding: 6px 12px;
font-size: 0.85rem;
}
}
@media (max-width: 480px) {
.header h1 {
font-size: 1.8rem;
}
.upload-btn {
padding: 12px 25px;
font-size: 1rem;
}
.convert-btn {
padding: 15px 30px;
font-size: 1.1rem;
}
.download-btn {
padding: 12px 25px;
font-size: 1rem;
}
}
/* 深色模式支持 */
@media (prefers-color-scheme: dark) {
body {
background: linear-gradient(135deg, #2d3748 0%, #4a5568 100%);
}
.container {
background: #1a202c;
color: #e2e8f0;
}
.upload-section,
.conversion-section,
.scale-section {
background: linear-gradient(145deg, #2d3748 0%, #4a5568 100%);
border-color: #4a5568;
}
.option-card,
.file-info {
background: #2d3748;
color: #e2e8f0;
border-color: #4a5568;
}
.info-item span:first-child {
color: #cbd5e0;
}
.scale-controls input {
background: #2d3748;
color: #e2e8f0;
border-color: #4a5568;
}
.quick-btn {
background: linear-gradient(145deg, #2d3748 0%, #4a5568 100%);
color: #e2e8f0;
border-color: #4a5568;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🗺️ 地图坐标转换工具</h1>
<p>支持仙工地图、标准地图与Scene格式的互相转换并提供坐标缩放功能</p>
</div>
<div class="content">
<!-- 文件上传区域 -->
<div class="upload-section" id="uploadSection">
<div class="upload-area">
<div class="upload-icon">📁</div>
<h3>选择地图文件</h3>
<p>支持JSON、SMAP格式最大100MB</p>
<p style="margin-top: 10px; color: #667eea; font-weight: bold">💡 可以直接拖拽文件到此区域</p>
<input type="file" id="fileInput" class="file-input" accept=".json,.smap" />
<button class="upload-btn" onclick="document.getElementById('fileInput').click()">选择文件</button>
</div>
<div class="drag-overlay">🚀 释放文件开始处理</div>
<div class="file-info" id="fileInfo" style="display: none">
<h4>📊 文件信息</h4>
<div id="fileDetails"></div>
</div>
</div>
<!-- 转换选项 -->
<div class="conversion-section" id="conversionSection">
<h3 style="text-align: center; margin-bottom: 30px; font-size: 1.6rem; color: #495057">🔄 选择转换方向</h3>
<div class="conversion-options">
<div class="option-card" id="xgToStandard" onclick="selectConversion('xgToStandard')">
<div class="option-title">🎯 仙工 → 标准</div>
<div class="option-desc">将仙工地图转换为标准地图格式</div>
</div>
<div class="option-card" id="standardToXg" onclick="selectConversion('standardToXg')">
<div class="option-title">📐 标准 → 仙工</div>
<div class="option-desc">将标准地图转换为仙工地图格式</div>
</div>
<div class="option-card" id="toScene" onclick="selectConversion('toScene')">
<div class="option-title">🎨 转换为 Scene</div>
<div class="option-desc">转换为可导入编辑器的Scene文件格式</div>
</div>
</div>
<!-- 缩放设置 -->
<div class="scale-section">
<h4 style="text-align: center; margin-bottom: 20px; font-size: 1.4rem; color: #495057">📏 坐标缩放设置</h4>
<div class="scale-controls">
<label for="scaleInput">缩放比例:</label>
<input type="number" id="scaleInput" value="1" step="0.01" min="0.01" max="1000" />
<div class="quick-scale-buttons">
<button class="quick-btn" onclick="setScale(0.01)">0.01x</button>
<button class="quick-btn" onclick="setScale(0.1)">0.1x</button>
<button class="quick-btn" onclick="setScale(1)">1x</button>
<button class="quick-btn" onclick="setScale(10)">10x</button>
<button class="quick-btn" onclick="setScale(100)">100x</button>
</div>
</div>
<div class="scale-hint">💡 输入缩放倍数或点击快捷按钮。例如100表示放大100倍0.01表示缩小100倍</div>
</div>
<button class="convert-btn" id="convertBtn" onclick="convertMap()" disabled>🚀 开始转换</button>
<div class="progress-bar" id="progressBar">
<div class="progress-fill" id="progressFill"></div>
</div>
</div>
<!-- 结果显示 -->
<div class="result-section" id="resultSection">
<h3>✅ 转换完成</h3>
<p>地图已成功转换,点击下载转换后的文件。</p>
<button class="download-btn" id="downloadBtn" onclick="downloadResult()">📥 下载转换结果</button>
<div id="conversionStats" style="margin-top: 30px"></div>
</div>
<!-- 提示信息 -->
<div class="alert alert-error" id="errorAlert"></div>
<div class="alert alert-success" id="successAlert"></div>
</div>
</div>
<script>
let originalData = null;
let convertedData = null;
let conversionType = null;
let fileName = '';
// 获取缩放比例
function getScaleRatio() {
const scaleInput = document.getElementById('scaleInput');
const scale = parseFloat(scaleInput.value);
return isNaN(scale) || scale <= 0 ? 1 : scale;
}
// 设置缩放比例 - 增强视觉反馈
function setScale(value) {
const scaleInput = document.getElementById('scaleInput');
scaleInput.value = value;
// 增强视觉反馈
scaleInput.style.borderColor = '#667eea';
scaleInput.style.boxShadow = '0 0 0 3px rgba(102, 126, 234, 0.15)';
scaleInput.style.transform = 'scale(1.05)';
// 添加成功反馈
const event = new Event('input');
scaleInput.dispatchEvent(event);
setTimeout(() => {
scaleInput.style.borderColor = '#dee2e6';
scaleInput.style.boxShadow = 'none';
scaleInput.style.transform = 'scale(1)';
}, 400);
}
// 应用缩放到坐标
function scaleCoordinate(x, y, scale = 1) {
return {
x: Math.round(x * scale * 1000000) / 1000000,
y: Math.round(y * scale * 1000000) / 1000000,
};
}
// 处理文件上传 - 增强反馈
function handleFileUpload(file) {
if (!file) return;
// 显示上传状态
const uploadBtn = document.querySelector('.upload-btn');
const originalText = uploadBtn.textContent;
uploadBtn.classList.add('loading');
uploadBtn.textContent = '处理中...';
uploadBtn.disabled = true;
if (file.size > 100 * 1024 * 1024) {
resetUploadButton(uploadBtn, originalText);
showAlert('❌ 文件大小超过100MB限制', 'error');
return;
}
const fileNameLower = file.name.toLowerCase();
if (!fileNameLower.endsWith('.json') && !fileNameLower.endsWith('.smap')) {
resetUploadButton(uploadBtn, originalText);
showAlert('❌ 请选择JSON或SMAP格式的文件', 'error');
return;
}
fileName = file.name;
loadFile(file);
}
// 重置上传按钮状态
function resetUploadButton(button, originalText) {
button.classList.remove('loading');
button.textContent = originalText;
button.disabled = false;
}
// 文件上传处理
document.getElementById('fileInput').addEventListener('change', function (e) {
handleFileUpload(e.target.files[0]);
});
// 拖拽上传功能 - 增强交互
const uploadSection = document.getElementById('uploadSection');
uploadSection.addEventListener('dragover', function (e) {
e.preventDefault();
uploadSection.classList.add('dragover');
});
uploadSection.addEventListener('dragleave', function (e) {
e.preventDefault();
if (!uploadSection.contains(e.relatedTarget)) {
uploadSection.classList.remove('dragover');
}
});
uploadSection.addEventListener('drop', function (e) {
e.preventDefault();
uploadSection.classList.remove('dragover');
const files = e.dataTransfer.files;
if (files.length > 0) {
handleFileUpload(files[0]);
}
});
// 加载文件 - 增强反馈
function loadFile(file) {
const reader = new FileReader();
reader.onload = function (e) {
try {
originalData = JSON.parse(e.target.result);
// 自动清空normalPosList
if (originalData.normalPosList) {
originalData.normalPosList = [];
}
displayFileInfo(file, originalData);
document.getElementById('conversionSection').style.display = 'block';
// 重置上传按钮
const uploadBtn = document.querySelector('.upload-btn');
resetUploadButton(uploadBtn, '选择文件');
showAlert('✅ 文件加载成功normalPosList已自动清空', 'success');
// 滚动到转换选项
document.getElementById('conversionSection').scrollIntoView({
behavior: 'smooth',
block: 'start',
});
} catch (error) {
const uploadBtn = document.querySelector('.upload-btn');
resetUploadButton(uploadBtn, '选择文件');
showAlert('❌ 文件格式错误请检查JSON格式', 'error');
}
};
reader.onerror = function () {
const uploadBtn = document.querySelector('.upload-btn');
resetUploadButton(uploadBtn, '选择文件');
showAlert('❌ 文件读取失败,请重试', 'error');
};
reader.readAsText(file);
}
// 显示文件信息 - 优化显示
function displayFileInfo(file, data) {
const fileInfo = document.getElementById('fileInfo');
const fileDetails = document.getElementById('fileDetails');
let pointCount = 0;
let curveCount = 0;
let areaCount = 0;
if (data.advancedPointList) pointCount = data.advancedPointList.length;
if (data.advancedCurveList) curveCount = data.advancedCurveList.length;
if (data.advancedAreaList) areaCount = data.advancedAreaList.length;
const fileType = file.name.toLowerCase().endsWith('.smap') ? 'SMAP文件' : 'JSON文件';
const fileSize = (file.size / 1024 / 1024).toFixed(2);
fileDetails.innerHTML = `
<div class="info-item"><span>📄 文件名:</span><span>${file.name}</span></div>
<div class="info-item"><span>📋 文件类型:</span><span>${fileType}</span></div>
<div class="info-item"><span>📊 文件大小:</span><span>${fileSize} MB</span></div>
<div class="info-item"><span>🗺️ 地图名称:</span><span>${data.header?.mapName || '未知'}</span></div>
<div class="info-item"><span>🔖 地图类型:</span><span>${data.header?.mapType || '未知'}</span></div>
<div class="info-item"><span>📐 分辨率:</span><span>${data.header?.resolution || '未知'}</span></div>
<div class="info-item"><span>📍 动作点数量:</span><span>${pointCount}</span></div>
<div class="info-item"><span>📈 曲线数量:</span><span>${curveCount}</span></div>
<div class="info-item"><span>🏢 区域数量:</span><span>${areaCount}</span></div>
`;
fileInfo.style.display = 'block';
// 添加淡入动画
setTimeout(() => {
fileInfo.style.opacity = '0';
fileInfo.style.transform = 'translateY(10px)';
fileInfo.style.transition = 'all 0.3s ease';
requestAnimationFrame(() => {
fileInfo.style.opacity = '1';
fileInfo.style.transform = 'translateY(0)';
});
}, 100);
}
// 选择转换类型 - 增强反馈
function selectConversion(type) {
// 移除所有选中状态并添加动画
document.querySelectorAll('.option-card').forEach((card) => {
card.classList.remove('selected');
card.style.transform = 'scale(0.98)';
});
// 延迟添加选中状态以创建动画效果
setTimeout(() => {
const selectedCard = document.getElementById(type);
selectedCard.classList.add('selected');
selectedCard.style.transform = 'translateY(-8px) scale(1)';
// 重置其他卡片
document.querySelectorAll('.option-card').forEach((card) => {
if (card !== selectedCard) {
card.style.transform = 'scale(1)';
}
});
}, 100);
conversionType = type;
const convertBtn = document.getElementById('convertBtn');
convertBtn.disabled = false;
// 按钮启用动画
convertBtn.style.transform = 'scale(1.05)';
setTimeout(() => {
convertBtn.style.transform = 'scale(1)';
}, 200);
}
// 执行转换 - 增强进度反馈
function convertMap() {
if (!originalData || !conversionType) return;
const convertBtn = document.getElementById('convertBtn');
const originalBtnText = convertBtn.textContent;
// 更换按钮状态
convertBtn.classList.add('loading');
convertBtn.textContent = '🔄 转换中...';
convertBtn.disabled = true;
showProgress(true);
// 模拟转换进度
let progress = 0;
const progressInterval = setInterval(() => {
progress += Math.random() * 15;
if (progress > 90) {
progress = 90;
clearInterval(progressInterval);
}
updateProgress(progress);
}, 100);
setTimeout(() => {
try {
convertedData = JSON.parse(JSON.stringify(originalData));
if (conversionType === 'xgToStandard') {
convertXgToStandard(convertedData);
} else if (conversionType === 'standardToXg') {
convertStandardToXg(convertedData);
} else if (conversionType === 'toScene') {
convertedData = convertToScene(originalData);
}
// 完成进度
clearInterval(progressInterval);
updateProgress(100);
setTimeout(() => {
showConversionResult();
showProgress(false);
document.getElementById('resultSection').style.display = 'block';
showAlert('🎉 地图转换完成!', 'success');
// 滚动到结果区域
document.getElementById('resultSection').scrollIntoView({
behavior: 'smooth',
block: 'start',
});
}, 500);
} catch (error) {
clearInterval(progressInterval);
showAlert('❌ 转换过程中出现错误:' + error.message, 'error');
showProgress(false);
}
// 重置按钮
convertBtn.classList.remove('loading');
convertBtn.textContent = originalBtnText;
convertBtn.disabled = false;
}, 1500);
}
// 更新进度条
function updateProgress(percent) {
const progressFill = document.getElementById('progressFill');
progressFill.style.width = percent + '%';
}
// 仙工地图转标准地图
function convertXgToStandard(data) {
const minPos = data.header?.minPos;
const maxPos = data.header?.maxPos;
if (!minPos || !maxPos) {
throw new Error('缺少地图边界信息');
}
const mapWidth = maxPos.x - minPos.x;
const mapHeight = maxPos.y - minPos.y;
const scale = getScaleRatio();
// 转换函数:仙工坐标 -> 标准坐标
function xgToStandard(x, y) {
const topLeftX = x + mapWidth / 2;
const topLeftY = mapHeight / 2 - y;
return scaleCoordinate(topLeftX, topLeftY, scale);
}
// 转换动作点
if (data.advancedPointList) {
data.advancedPointList.forEach((point) => {
const converted = xgToStandard(point.pos.x, point.pos.y);
point.pos.x = converted.x;
point.pos.y = converted.y;
});
}
// 转换曲线
if (data.advancedCurveList) {
data.advancedCurveList.forEach((curve) => {
if (curve.startPos?.pos) {
const converted = xgToStandard(curve.startPos.pos.x, curve.startPos.pos.y);
curve.startPos.pos.x = converted.x;
curve.startPos.pos.y = converted.y;
}
if (curve.endPos?.pos) {
const converted = xgToStandard(curve.endPos.pos.x, curve.endPos.pos.y);
curve.endPos.pos.x = converted.x;
curve.endPos.pos.y = converted.y;
}
if (curve.controlPos1) {
const converted = xgToStandard(curve.controlPos1.x, curve.controlPos1.y);
curve.controlPos1.x = converted.x;
curve.controlPos1.y = converted.y;
}
if (curve.controlPos2) {
const converted = xgToStandard(curve.controlPos2.x, curve.controlPos2.y);
curve.controlPos2.x = converted.x;
curve.controlPos2.y = converted.y;
}
});
}
// 转换区域
if (data.advancedAreaList) {
data.advancedAreaList.forEach((area) => {
if (area.posGroup) {
area.posGroup.forEach((pos) => {
const converted = xgToStandard(pos.x, pos.y);
pos.x = converted.x;
pos.y = converted.y;
});
}
});
}
// 更新边界
const newMinPos = xgToStandard(minPos.x, minPos.y);
const newMaxPos = xgToStandard(maxPos.x, maxPos.y);
data.header.minPos = {
x: Math.min(newMinPos.x, newMaxPos.x),
y: Math.min(newMinPos.y, newMaxPos.y),
};
data.header.maxPos = {
x: Math.max(newMinPos.x, newMaxPos.x),
y: Math.max(newMinPos.y, newMaxPos.y),
};
}
// 标准地图转仙工地图
function convertStandardToXg(data) {
const minPos = data.header?.minPos;
const maxPos = data.header?.maxPos;
if (!minPos || !maxPos) {
throw new Error('缺少地图边界信息');
}
const mapWidth = maxPos.x - minPos.x;
const mapHeight = maxPos.y - minPos.y;
const scale = getScaleRatio();
// 转换函数:标准坐标 -> 仙工坐标
function standardToXg(x, y) {
const centerX = x - mapWidth / 2;
const centerY = mapHeight / 2 - y;
return scaleCoordinate(centerX, centerY, scale);
}
// 转换动作点
if (data.advancedPointList) {
data.advancedPointList.forEach((point) => {
const converted = standardToXg(point.pos.x, point.pos.y);
point.pos.x = converted.x;
point.pos.y = converted.y;
});
}
// 转换曲线
if (data.advancedCurveList) {
data.advancedCurveList.forEach((curve) => {
if (curve.startPos?.pos) {
const converted = standardToXg(curve.startPos.pos.x, curve.startPos.pos.y);
curve.startPos.pos.x = converted.x;
curve.startPos.pos.y = converted.y;
}
if (curve.endPos?.pos) {
const converted = standardToXg(curve.endPos.pos.x, curve.endPos.pos.y);
curve.endPos.pos.x = converted.x;
curve.endPos.pos.y = converted.y;
}
if (curve.controlPos1) {
const converted = standardToXg(curve.controlPos1.x, curve.controlPos1.y);
curve.controlPos1.x = converted.x;
curve.controlPos1.y = converted.y;
}
if (curve.controlPos2) {
const converted = standardToXg(curve.controlPos2.x, curve.controlPos2.y);
curve.controlPos2.x = converted.x;
curve.controlPos2.y = converted.y;
}
});
}
// 转换区域
if (data.advancedAreaList) {
data.advancedAreaList.forEach((area) => {
if (area.posGroup) {
area.posGroup.forEach((pos) => {
const converted = standardToXg(pos.x, pos.y);
pos.x = converted.x;
pos.y = converted.y;
});
}
});
}
// 更新边界
const newMinPos = standardToXg(minPos.x, minPos.y);
const newMaxPos = standardToXg(maxPos.x, maxPos.y);
data.header.minPos = {
x: Math.min(newMinPos.x, newMaxPos.x),
y: Math.min(newMinPos.y, newMaxPos.y),
};
data.header.maxPos = {
x: Math.max(newMinPos.x, newMaxPos.x),
y: Math.max(newMinPos.y, newMaxPos.y),
};
}
// 转换为Scene格式
function convertToScene(data) {
const scene = {
points: [],
routes: [],
areas: [],
};
const scale = getScaleRatio();
// 转换动作点为场景点位
if (data.advancedPointList) {
data.advancedPointList.forEach((point, index) => {
const scaledPos = scaleCoordinate(point.pos.x, point.pos.y, scale);
scene.points.push({
id: point.instanceName || `point_${index}`,
name: point.instanceName || `点位${index}`,
desc: point.className || '',
x: scaledPos.x,
y: scaledPos.y,
type: getPointType(point.className),
robots: [],
actions: [],
properties: point.property || {},
});
});
}
// 转换曲线为场景路线
if (data.advancedCurveList) {
data.advancedCurveList.forEach((curve, index) => {
const startPointId = curve.startPos?.instanceName || `start_${index}`;
const endPointId = curve.endPos?.instanceName || `end_${index}`;
const scaledC1 = curve.controlPos1
? scaleCoordinate(curve.controlPos1.x, curve.controlPos1.y, scale)
: undefined;
const scaledC2 = curve.controlPos2
? scaleCoordinate(curve.controlPos2.x, curve.controlPos2.y, scale)
: undefined;
scene.routes.push({
id: curve.instanceName || `route_${index}`,
desc: curve.className || '',
from: startPointId,
to: endPointId,
type: getRouteType(curve.className),
pass: 1,
c1: scaledC1 ? { x: scaledC1.x, y: scaledC1.y } : undefined,
c2: scaledC2 ? { x: scaledC2.x, y: scaledC2.y } : undefined,
properties: curve.property || {},
});
});
}
// 转换区域为场景区域
if (data.advancedAreaList) {
data.advancedAreaList.forEach((area, index) => {
// 计算区域的边界框
const bounds = calculateAreaBounds(area.posGroup);
const scaledBounds = {
x: bounds.x * scale,
y: bounds.y * scale,
w: bounds.w * scale,
h: bounds.h * scale,
};
// 直接使用instanceName作为区域名称
console.log(`区域 ${area.instanceName}: 使用实例名称作为标签`);
// 构建区域配置,包含颜色信息
const areaConfig = {
className: area.className,
dir: area.dir,
posGroup: area.posGroup, // 保留原始多边形数据
attribute: area.attribute || {}, // 保留颜色信息
};
// 缩放后的多边形坐标(如果需要的话)
const scaledPosGroup = area.posGroup?.map((pos) => ({
x: pos.x * scale,
y: pos.y * scale,
}));
// 直接使用instanceName作为显示名称
const displayName = area.instanceName || `区域${index}`;
console.log(`区域 ${area.instanceName}: 最终显示名称为 "${displayName}"`);
scene.areas.push({
id: area.instanceName || `area_${index}`,
name: displayName, // 这个name将作为编辑器中的label显示
desc: area.className || '', // 类名作为描述
x: scaledBounds.x,
y: scaledBounds.y,
w: scaledBounds.w,
h: scaledBounds.h,
type: 1, // 统一使用库区类型
points: [],
routes: [],
properties: area.property || {},
config: {
...areaConfig,
scaledPosGroup: scaledPosGroup, // 缩放后的坐标
originalAttribute: area.attribute, // 保留原始颜色属性
displayName: displayName, // 记录显示名称
},
});
});
}
// 计算场景尺寸
if (data.header) {
const { minPos, maxPos } = data.header;
if (minPos && maxPos) {
scene.width = (maxPos.x - minPos.x) * scale;
scene.height = (maxPos.y - minPos.y) * scale;
}
}
return scene;
}
// 计算区域边界框
function calculateAreaBounds(posGroup) {
if (!posGroup || posGroup.length === 0) {
return { x: 0, y: 0, w: 0, h: 0 };
}
let minX = posGroup[0].x;
let maxX = posGroup[0].x;
let minY = posGroup[0].y;
let maxY = posGroup[0].y;
posGroup.forEach((pos) => {
minX = Math.min(minX, pos.x);
maxX = Math.max(maxX, pos.x);
minY = Math.min(minY, pos.y);
maxY = Math.max(maxY, pos.y);
});
return {
x: minX,
y: minY,
w: maxX - minX,
h: maxY - minY,
};
}
// 获取点位类型
function getPointType(className) {
switch (className) {
case 'ActionPoint':
return 11; // 动作点
case 'ChargePoint':
return 12; // 充电点
case 'WaitPoint':
return 13; // 等待点
default:
return 11; // 默认动作点
}
}
// 获取路线类型
function getRouteType(className) {
switch (className) {
case 'DegenerateBezier':
return 'bezier2';
case 'Bezier':
return 'bezier3';
default:
return 'line';
}
}
// 获取区域类型 (暂时统一使用库区)
function getAreaType(className) {
// 当前所有区域都使用库区类型
return 1;
// 保留原始映射逻辑以备将来使用
/*
switch (className) {
case 'AdvancedArea':
return 1; // 库区
case 'AreaDescription':
return 12; // 非互斥区 (用于描述区域)
case 'RestrictedArea':
return 11; // 互斥区 (限制区域)
default:
return 1; // 默认库区
}
*/
}
// 显示转换结果统计
function showConversionResult() {
const stats = document.getElementById('conversionStats');
let pointCount = 0;
let curveCount = 0;
let areaCount = 0;
let directionText = '';
if (conversionType === 'toScene') {
// Scene格式统计
pointCount = convertedData.points ? convertedData.points.length : 0;
curveCount = convertedData.routes ? convertedData.routes.length : 0;
areaCount = convertedData.areas ? convertedData.areas.length : 0;
directionText = '地图 → Scene格式';
} else {
// 原有格式统计
if (convertedData.advancedPointList) pointCount = convertedData.advancedPointList.length;
if (convertedData.advancedCurveList) curveCount = convertedData.advancedCurveList.length;
if (convertedData.advancedAreaList) areaCount = convertedData.advancedAreaList.length;
directionText = conversionType === 'xgToStandard' ? '仙工地图 → 标准地图' : '标准地图 → 仙工地图';
}
const normalPosText = conversionType === 'toScene' ? '已转换为Scene格式包含颜色信息' : '已清空';
const scaleRatio = getScaleRatio();
const scaleText = scaleRatio === 1 ? '无缩放' : `${scaleRatio}倍`;
let additionalInfo = '';
if (conversionType === 'toScene') {
additionalInfo = `
<div class="info-item"><span>区域名称:</span><span>使用实例名称</span></div>
<div class="info-item"><span>区域类型:</span><span>统一设为库区</span></div>
<div class="info-item"><span>颜色保留:</span><span>已保留原始颜色属性</span></div>
`;
}
stats.innerHTML = `
<div class="file-info">
<h4>转换统计</h4>
<div class="info-item"><span>转换方向:</span><span>${directionText}</span></div>
<div class="info-item"><span>缩放比例:</span><span>${scaleText}</span></div>
<div class="info-item"><span>转换的点位:</span><span>${pointCount}</span></div>
<div class="info-item"><span>转换的路线:</span><span>${curveCount}</span></div>
<div class="info-item"><span>转换的区域:</span><span>${areaCount}</span></div>
<div class="info-item"><span>数据状态:</span><span>${normalPosText}</span></div>
${additionalInfo}
</div>
`;
}
// 下载结果
function downloadResult() {
if (!convertedData) return;
const jsonString = JSON.stringify(convertedData, null, 2);
const blob = new Blob([jsonString], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
let suffix = '';
let extension = '.json';
if (conversionType === 'xgToStandard') {
suffix = '_standard';
} else if (conversionType === 'standardToXg') {
suffix = '_xg';
extension = '.smap';
} else if (conversionType === 'toScene') {
suffix = '_scene';
extension = '.scene';
}
// 处理不同的输入文件扩展名
const baseFileName = fileName.replace(/\.(json|smap)$/i, '');
a.download = baseFileName + suffix + extension;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
// 显示进度条
function showProgress(show) {
const progressBar = document.getElementById('progressBar');
const progressFill = document.getElementById('progressFill');
if (show) {
progressBar.style.display = 'block';
progressFill.style.width = '0%';
setTimeout(() => (progressFill.style.width = '100%'), 100);
} else {
setTimeout(() => {
progressBar.style.display = 'none';
progressFill.style.width = '0%';
}, 500);
}
}
// 显示提示信息
function showAlert(message, type) {
const errorAlert = document.getElementById('errorAlert');
const successAlert = document.getElementById('successAlert');
errorAlert.style.display = 'none';
successAlert.style.display = 'none';
if (type === 'error') {
errorAlert.textContent = message;
errorAlert.style.display = 'block';
setTimeout(() => (errorAlert.style.display = 'none'), 5000);
} else {
successAlert.textContent = message;
successAlert.style.display = 'block';
setTimeout(() => (successAlert.style.display = 'none'), 3000);
}
}
</script>
</body>
</html>