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

1673 lines
50 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>