1673 lines
50 KiB
HTML
1673 lines
50 KiB
HTML
<!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>
|