mirror of
https://github.com/dedicatedcode/reitti.git
synced 2026-01-09 17:37:57 -05:00
1134 lines
38 KiB
HTML
1134 lines
38 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>GPX Test Data Generator - Create and Edit GPS Tracks</title>
|
||
<meta name="description" content="Interactive GPX track generator and editor. Create realistic GPS test data with customizable speed, elevation, and timing. Perfect for testing location-based applications.">
|
||
<meta name="keywords" content="GPX, GPS, track generator, test data, location testing, GPS simulation, route planning">
|
||
<meta name="author" content="Reitti Project">
|
||
<meta property="og:title" content="GPX Test Data Generator">
|
||
<meta property="og:description" content="Interactive tool to create and edit GPX tracks for testing location-based applications">
|
||
<meta property="og:type" content="website">
|
||
<meta name="twitter:card" content="summary">
|
||
<meta name="twitter:title" content="GPX Test Data Generator">
|
||
<meta name="twitter:description" content="Interactive tool to create and edit GPX tracks for testing location-based applications">
|
||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root {
|
||
--bg-primary: #ffffff;
|
||
--bg-secondary: #f8fafc;
|
||
--bg-tertiary: #f1f5f9;
|
||
--bg-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
--text-primary: #2d3748;
|
||
--text-secondary: #4a5568;
|
||
--text-muted: #718096;
|
||
--border-color: #e2e8f0;
|
||
--border-color-light: rgba(226, 232, 240, 0.8);
|
||
--shadow-light: rgba(0, 0, 0, 0.06);
|
||
--shadow-medium: rgba(0, 0, 0, 0.1);
|
||
--shadow-heavy: rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
[data-theme="dark"] {
|
||
--bg-primary: #1a202c;
|
||
--bg-secondary: #2d3748;
|
||
--bg-tertiary: #4a5568;
|
||
--bg-gradient: linear-gradient(135deg, #2d3748 0%, #1a202c 100%);
|
||
--text-primary: #f7fafc;
|
||
--text-secondary: #e2e8f0;
|
||
--text-muted: #a0aec0;
|
||
--border-color: #4a5568;
|
||
--border-color-light: rgba(74, 85, 104, 0.8);
|
||
--shadow-light: rgba(0, 0, 0, 0.2);
|
||
--shadow-medium: rgba(0, 0, 0, 0.3);
|
||
--shadow-heavy: rgba(0, 0, 0, 0.4);
|
||
}
|
||
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||
background: var(--bg-gradient);
|
||
min-height: 100vh;
|
||
color: var(--text-primary);
|
||
line-height: 1.5;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.container {
|
||
max-width: 100%;
|
||
margin: 0;
|
||
padding: 0;
|
||
}
|
||
|
||
.main-container {
|
||
background: rgba(255, 255, 255, 0.95);
|
||
backdrop-filter: blur(20px);
|
||
box-shadow: 0 25px 50px var(--shadow-heavy);
|
||
overflow: hidden;
|
||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||
position: relative;
|
||
}
|
||
|
||
.edit-drawer {
|
||
position: fixed;
|
||
left: -400px;
|
||
top: 0;
|
||
width: 380px;
|
||
height: 100vh;
|
||
background: var(--bg-primary);
|
||
border-right: 1px solid var(--border-color);
|
||
box-shadow: 4px 0 20px var(--shadow-medium);
|
||
transition: left 0.3s ease;
|
||
z-index: 2000;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.edit-drawer.open {
|
||
left: 0;
|
||
}
|
||
|
||
.edit-drawer-header {
|
||
padding: 16px 20px;
|
||
background: var(--bg-secondary);
|
||
border-bottom: 1px solid var(--border-color);
|
||
font-weight: 600;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.edit-drawer-close {
|
||
background: none;
|
||
border: none;
|
||
font-size: 18px;
|
||
cursor: pointer;
|
||
color: var(--text-secondary);
|
||
padding: 4px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.edit-drawer-close:hover {
|
||
background: var(--bg-tertiary);
|
||
}
|
||
|
||
.edit-drawer-content {
|
||
padding: 20px;
|
||
}
|
||
|
||
.drawer-section {
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.drawer-section-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: var(--text-secondary);
|
||
margin-bottom: 12px;
|
||
padding-bottom: 8px;
|
||
border-bottom: 1px solid var(--border-color);
|
||
}
|
||
|
||
.mode-toggle {
|
||
background: var(--bg-primary);
|
||
border: 2px solid var(--border-color);
|
||
border-radius: 8px;
|
||
padding: 8px 16px;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
color: var(--text-secondary);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.mode-toggle.edit-mode {
|
||
border-color: #48bb78;
|
||
background: #48bb78;
|
||
color: white;
|
||
}
|
||
|
||
.mode-toggle:hover {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 12px var(--shadow-medium);
|
||
}
|
||
|
||
.about-dialog {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: none;
|
||
justify-content: center;
|
||
align-items: center;
|
||
z-index: 2000;
|
||
}
|
||
|
||
.about-dialog.open {
|
||
display: flex;
|
||
}
|
||
|
||
.about-content {
|
||
background: var(--bg-primary);
|
||
border-radius: 12px;
|
||
padding: 32px;
|
||
max-width: 600px;
|
||
margin: 20px;
|
||
box-shadow: 0 20px 60px var(--shadow-heavy);
|
||
border: 1px solid var(--border-color);
|
||
}
|
||
|
||
.about-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.about-title {
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.about-close {
|
||
background: none;
|
||
border: none;
|
||
font-size: 24px;
|
||
cursor: pointer;
|
||
color: var(--text-secondary);
|
||
padding: 4px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.about-close:hover {
|
||
background: var(--bg-tertiary);
|
||
}
|
||
|
||
.about-text {
|
||
color: var(--text-secondary);
|
||
line-height: 1.6;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.about-link {
|
||
color: #667eea;
|
||
text-decoration: none;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.about-link:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
.header-section {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.header-divider {
|
||
width: 1px;
|
||
height: 24px;
|
||
background: var(--border-color);
|
||
margin: 0 8px;
|
||
}
|
||
|
||
[data-theme="dark"] .main-container {
|
||
background: rgba(26, 32, 44, 0.95);
|
||
border: 1px solid rgba(74, 85, 104, 0.3);
|
||
}
|
||
|
||
.main-container::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: 1px;
|
||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.8), transparent);
|
||
}
|
||
|
||
.header {
|
||
background: var(--bg-secondary);
|
||
color: var(--text-secondary);
|
||
padding: 8px 16px;
|
||
border-bottom: 1px solid var(--border-color);
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
height: 56px;
|
||
}
|
||
|
||
.header-content {
|
||
flex: 1;
|
||
}
|
||
|
||
.header-controls {
|
||
display: flex;
|
||
gap: 12px;
|
||
align-items: center;
|
||
}
|
||
|
||
.theme-toggle {
|
||
background: var(--bg-primary);
|
||
border: 2px solid var(--border-color);
|
||
border-radius: 6px;
|
||
padding: 6px 12px;
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.theme-toggle:hover {
|
||
border-color: #667eea;
|
||
background: #667eea;
|
||
color: white;
|
||
}
|
||
|
||
.layer-toggle {
|
||
background: var(--bg-primary);
|
||
border: 2px solid var(--border-color);
|
||
border-radius: 6px;
|
||
padding: 6px 12px;
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.layer-toggle:hover {
|
||
border-color: #48bb78;
|
||
background: #48bb78;
|
||
color: white;
|
||
}
|
||
|
||
.header h1 {
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
.header p {
|
||
opacity: 0.8;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.controls {
|
||
padding: 12px 16px;
|
||
background: var(--bg-secondary);
|
||
border-bottom: 1px solid var(--border-color-light);
|
||
backdrop-filter: blur(10px);
|
||
}
|
||
|
||
.control-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||
gap: 12px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.control-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.control-label {
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
color: var(--text-secondary);
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.control-input {
|
||
padding: 8px 12px;
|
||
border: 2px solid var(--border-color);
|
||
border-radius: 8px;
|
||
font-size: 13px;
|
||
transition: all 0.3s ease;
|
||
background: var(--bg-primary);
|
||
color: var(--text-primary);
|
||
font-family: inherit;
|
||
box-shadow: inset 0 2px 4px var(--shadow-light);
|
||
}
|
||
|
||
.control-input:focus {
|
||
outline: none;
|
||
border-color: #667eea;
|
||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1), inset 0 2px 4px rgba(0, 0, 0, 0.06);
|
||
background: linear-gradient(145deg, #ffffff, #f1f5f9);
|
||
}
|
||
|
||
[data-theme="dark"] .control-input {
|
||
background: var(--bg-secondary);
|
||
color: var(--text-primary);
|
||
border-color: var(--border-color);
|
||
}
|
||
|
||
[data-theme="dark"] .control-input:focus {
|
||
background: var(--bg-tertiary);
|
||
border-color: #667eea;
|
||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2), inset 0 2px 4px var(--shadow-light);
|
||
}
|
||
|
||
.control-button {
|
||
padding: 8px 16px;
|
||
border: 2px solid #e2e8f0;
|
||
border-radius: 8px;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
background: linear-gradient(145deg, #ffffff, #f8fafc);
|
||
color: #4a5568;
|
||
font-family: inherit;
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.control-button::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: -100%;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent);
|
||
transition: left 0.5s;
|
||
}
|
||
|
||
.control-button:hover {
|
||
border-color: #667eea;
|
||
background: linear-gradient(145deg, #667eea, #5a67d8);
|
||
color: white;
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
|
||
}
|
||
|
||
.control-button:hover::before {
|
||
left: 100%;
|
||
}
|
||
|
||
.control-button.active {
|
||
background: #667eea;
|
||
border-color: #667eea;
|
||
color: white;
|
||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
|
||
}
|
||
|
||
.control-button.success {
|
||
background: #48bb78;
|
||
border-color: #48bb78;
|
||
color: white;
|
||
}
|
||
|
||
.control-button.success:hover {
|
||
background: #38a169;
|
||
border-color: #38a169;
|
||
}
|
||
|
||
.control-button.danger {
|
||
background: #f56565;
|
||
border-color: #f56565;
|
||
color: white;
|
||
}
|
||
|
||
.control-button.danger:hover {
|
||
background: #e53e3e;
|
||
border-color: #e53e3e;
|
||
}
|
||
|
||
.paint-mode-info {
|
||
font-size: 12px;
|
||
color: #718096;
|
||
margin-top: 6px;
|
||
font-style: italic;
|
||
}
|
||
|
||
.button-group {
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
|
||
.main-content {
|
||
display: flex;
|
||
height: 91vh;
|
||
min-height: 650px;
|
||
}
|
||
|
||
.map-container {
|
||
flex: 3;
|
||
position: relative;
|
||
}
|
||
|
||
#map {
|
||
height: 100%;
|
||
width: 100%;
|
||
}
|
||
|
||
.points-panel {
|
||
flex: 1;
|
||
min-width: 350px;
|
||
border-left: 1px solid rgba(226, 232, 240, 0.8);
|
||
background: rgba(255, 255, 255, 0.9);
|
||
backdrop-filter: blur(10px);
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.speed-legend {
|
||
padding: 12px 20px;
|
||
background: rgba(247, 250, 252, 0.9);
|
||
border-bottom: 1px solid rgba(226, 232, 240, 0.8);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.speed-legend-title {
|
||
font-weight: 600;
|
||
margin-bottom: 8px;
|
||
color: #4a5568;
|
||
}
|
||
|
||
.speed-legend-items {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.speed-legend-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.speed-color-indicator {
|
||
width: 12px;
|
||
height: 12px;
|
||
border-radius: 2px;
|
||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.points-header {
|
||
padding: 20px;
|
||
background: linear-gradient(135deg, #4a5568, #2d3748);
|
||
color: white;
|
||
font-weight: 600;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.points-summary {
|
||
padding: 16px 20px;
|
||
background: rgba(247, 250, 252, 0.9);
|
||
border-bottom: 1px solid rgba(226, 232, 240, 0.8);
|
||
font-size: 13px;
|
||
color: #4a5568;
|
||
}
|
||
|
||
.points-list {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.point-item {
|
||
padding: 6px 20px;
|
||
border-bottom: 1px solid rgba(237, 242, 247, 0.8);
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
font-size: 10px;
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
min-height: 28px;
|
||
border-left: 4px solid #e2e8f0;
|
||
}
|
||
|
||
.point-item:hover {
|
||
background: rgba(102, 126, 234, 0.05);
|
||
transform: translateX(4px);
|
||
}
|
||
|
||
.point-item.selected {
|
||
background: rgba(102, 126, 234, 0.1);
|
||
border-left: 4px solid #667eea !important;
|
||
}
|
||
|
||
.point-content {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex: 1;
|
||
font-family: 'Monaco', 'Menlo', monospace;
|
||
}
|
||
|
||
.point-coords {
|
||
color: #4a5568;
|
||
font-weight: 500;
|
||
min-width: 120px;
|
||
}
|
||
|
||
.point-time {
|
||
color: #718096;
|
||
min-width: 80px;
|
||
}
|
||
|
||
.point-speed {
|
||
font-weight: 500;
|
||
min-width: 45px;
|
||
text-align: right;
|
||
}
|
||
|
||
.point-delete {
|
||
background: #f56565;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
padding: 4px 8px;
|
||
font-size: 11px;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
opacity: 0;
|
||
}
|
||
|
||
.point-item:hover .point-delete {
|
||
opacity: 1;
|
||
}
|
||
|
||
.point-delete:hover {
|
||
background: #e53e3e;
|
||
transform: scale(1.1);
|
||
}
|
||
|
||
.status {
|
||
padding: 20px;
|
||
background: rgba(247, 250, 252, 0.9);
|
||
border-top: 1px solid rgba(226, 232, 240, 0.8);
|
||
font-size: 14px;
|
||
color: #4a5568;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.speed-legend-inline {
|
||
display: flex;
|
||
gap: 12px;
|
||
align-items: center;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.speed-legend-inline .speed-legend-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
|
||
.speed-legend-inline .speed-color-indicator {
|
||
width: 10px;
|
||
height: 10px;
|
||
border-radius: 2px;
|
||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.hover-tooltip {
|
||
position: absolute;
|
||
background: rgba(26, 32, 44, 0.95);
|
||
color: white;
|
||
padding: 12px 16px;
|
||
border-radius: 12px;
|
||
font-size: 13px;
|
||
pointer-events: none;
|
||
z-index: 1000;
|
||
white-space: nowrap;
|
||
backdrop-filter: blur(10px);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.speed-indicator {
|
||
font-size: 12px;
|
||
margin-top: 4px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.speed-ok { color: #48bb78; }
|
||
.speed-fast { color: #ed8936; }
|
||
.speed-unrealistic { color: #f56565; }
|
||
|
||
.track-header {
|
||
padding: 12px 20px;
|
||
background: #2d3748;
|
||
color: white;
|
||
cursor: pointer;
|
||
border-bottom: 1px solid rgba(226, 232, 240, 0.8);
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
font-weight: 600;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.track-header:hover {
|
||
background: #4a5568;
|
||
}
|
||
|
||
.track-header.active {
|
||
background: #667eea;
|
||
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.3);
|
||
}
|
||
|
||
.track-header.active:hover {
|
||
background: #5a67d8;
|
||
}
|
||
|
||
.track-color {
|
||
width: 16px;
|
||
height: 16px;
|
||
border-radius: 50%;
|
||
margin-right: 8px;
|
||
border: 2px solid white;
|
||
}
|
||
|
||
.track-info {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.track-controls {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
.track-export-btn {
|
||
padding: 4px 8px;
|
||
background: #48bb78;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
font-size: 11px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.track-export-btn:hover {
|
||
background: #38a169;
|
||
}
|
||
|
||
.track-points {
|
||
display: block;
|
||
}
|
||
|
||
.track-points.collapsed {
|
||
display: none;
|
||
}
|
||
|
||
.collapse-icon {
|
||
transition: transform 0.3s ease;
|
||
}
|
||
|
||
.collapsed .collapse-icon {
|
||
transform: rotate(-90deg);
|
||
}
|
||
|
||
.range-input {
|
||
-webkit-appearance: none;
|
||
appearance: none;
|
||
height: 6px;
|
||
border-radius: 3px;
|
||
background: #e2e8f0;
|
||
outline: none;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.range-input::-webkit-slider-thumb {
|
||
-webkit-appearance: none;
|
||
appearance: none;
|
||
width: 20px;
|
||
height: 20px;
|
||
border-radius: 50%;
|
||
background: #667eea;
|
||
cursor: pointer;
|
||
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.range-input::-webkit-slider-thumb:hover {
|
||
transform: scale(1.2);
|
||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.5);
|
||
}
|
||
|
||
.paint-active {
|
||
background: #48bb78 !important;
|
||
border-color: #48bb78 !important;
|
||
color: white !important;
|
||
animation: pulse 2s infinite;
|
||
}
|
||
|
||
.paint-ready {
|
||
background: #ed8936 !important;
|
||
border-color: #ed8936 !important;
|
||
color: white !important;
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0% { box-shadow: 0 0 0 0 rgba(72, 187, 120, 0.7); }
|
||
70% { box-shadow: 0 0 0 10px rgba(72, 187, 120, 0); }
|
||
100% { box-shadow: 0 0 0 0 rgba(72, 187, 120, 0); }
|
||
}
|
||
|
||
|
||
/* Scrollbar styling */
|
||
.points-list::-webkit-scrollbar {
|
||
width: 6px;
|
||
}
|
||
|
||
.points-list::-webkit-scrollbar-track {
|
||
background: rgba(237, 242, 247, 0.5);
|
||
}
|
||
|
||
.points-list::-webkit-scrollbar-thumb {
|
||
background: rgba(102, 126, 234, 0.3);
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.points-list::-webkit-scrollbar-thumb:hover {
|
||
background: rgba(102, 126, 234, 0.5);
|
||
}
|
||
|
||
/* Date Picker Styles */
|
||
.date-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||
gap: 8px;
|
||
margin: 20px 0;
|
||
max-height: 300px;
|
||
overflow-y: auto;
|
||
padding: 4px;
|
||
}
|
||
|
||
.date-item {
|
||
padding: 8px;
|
||
border: 1px solid var(--border-color);
|
||
border-radius: 4px;
|
||
text-align: center;
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
background: var(--bg-primary);
|
||
transition: all 0.2s;
|
||
user-select: none;
|
||
}
|
||
|
||
.date-item:hover {
|
||
background: var(--bg-tertiary);
|
||
}
|
||
|
||
.date-item.selected {
|
||
background: #667eea;
|
||
color: white;
|
||
border-color: #667eea;
|
||
}
|
||
|
||
.date-item.in-range {
|
||
background: rgba(102, 126, 234, 0.2);
|
||
border-color: #667eea;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="main-container">
|
||
<div class="header">
|
||
<div class="header-content">
|
||
<h1>GPX Test Data Generator</h1>
|
||
<p id="headerDescription">Interactive tool for creating and editing GPS tracks</p>
|
||
</div>
|
||
<div class="header-controls">
|
||
<div class="header-section">
|
||
<button id="modeToggle" class="mode-toggle" onclick="toggleEditMode()">
|
||
<span>📍</span> View Mode
|
||
</button>
|
||
</div>
|
||
|
||
<div class="header-divider"></div>
|
||
|
||
<div class="header-section">
|
||
<button onclick="document.getElementById('gpxFileInput').click()" class="control-button">📁 Import</button>
|
||
<button onclick="exportAllGPX()" class="control-button success">💾 Export</button>
|
||
<button onclick="clearAll()" class="control-button danger">🗑️ Clear</button>
|
||
</div>
|
||
|
||
<div class="header-divider"></div>
|
||
|
||
<div class="header-section">
|
||
<button onclick="openAboutDialog()" class="control-button">ℹ️ About</button>
|
||
<button id="layerToggle" class="layer-toggle" onclick="toggleMapLayer()">🛰️ Satellite</button>
|
||
<button id="themeToggle" class="theme-toggle" onclick="toggleTheme()">🌙</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Edit Drawer -->
|
||
<div class="edit-drawer" id="editDrawer">
|
||
<div class="edit-drawer-header">
|
||
<span>Edit Controls</span>
|
||
<button class="edit-drawer-close" onclick="toggleEditDrawer()">×</button>
|
||
</div>
|
||
<div class="edit-drawer-content">
|
||
<div class="drawer-section">
|
||
<div class="drawer-section-title">Track Settings</div>
|
||
<div class="control-group">
|
||
<label class="control-label" for="startDateTime">Start Date & Time</label>
|
||
<input type="datetime-local" id="startDateTime" step="1" class="control-input">
|
||
</div>
|
||
<div class="control-group">
|
||
<label class="control-label" for="timeInterval">Interval (seconds)</label>
|
||
<input type="number" id="timeInterval" value="30" min="1" max="3600" class="control-input">
|
||
</div>
|
||
<div class="control-group">
|
||
<label class="control-label" for="maxSpeed">Max Speed (km/h)</label>
|
||
<input type="number" id="maxSpeed" value="25" min="1" max="300" class="control-input">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="drawer-section">
|
||
<div class="drawer-section-title">Elevation & GPS</div>
|
||
<div class="control-group">
|
||
<label class="control-label" for="elevation">Elevation (m)</label>
|
||
<input type="number" id="elevation" value="10" step="0.1" class="control-input">
|
||
</div>
|
||
<div class="control-group">
|
||
<label class="control-label" for="elevationVariation">±Variation (m)</label>
|
||
<input type="number" id="elevationVariation" value="0" min="0" step="0.1" class="control-input">
|
||
</div>
|
||
<div class="control-group">
|
||
<label class="control-label" for="accuracySlider">GPS Accuracy: <span id="accuracyValue">5</span>m</label>
|
||
<input type="range" id="accuracySlider" min="0" max="100" value="5" class="range-input">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="drawer-section">
|
||
<div class="drawer-section-title">Options</div>
|
||
<div class="control-group">
|
||
<label class="control-label">
|
||
<input type="checkbox" id="autoNewTrack" checked style="margin-right: 8px;">
|
||
Auto new track on day change
|
||
</label>
|
||
</div>
|
||
<div class="control-group">
|
||
<label class="control-label">
|
||
<input type="checkbox" id="autoStops" style="margin-right: 8px;">
|
||
Add realistic stops
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="drawer-section">
|
||
<div class="drawer-section-title">Drawing Tools</div>
|
||
<div class="control-group">
|
||
<button id="paintModeToggle" class="control-button" onclick="togglePaintMode()">🎨 Paint Mode: OFF</button>
|
||
<div class="paint-mode-info">Click map to start/stop painting</div>
|
||
</div>
|
||
<div class="control-group">
|
||
<button onclick="newTrack()" class="control-button">➕ New Track</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="drawer-section">
|
||
<div class="drawer-section-title">Time Adjustment</div>
|
||
<div class="control-group">
|
||
<label class="control-label">Shift Current Track</label>
|
||
<div style="display: flex; gap: 8px; margin-bottom: 8px;">
|
||
<button onclick="shiftTrackTime(-1, 'hour')" class="control-button" style="flex: 1; font-size: 11px;">-1h</button>
|
||
<button onclick="shiftTrackTime(1, 'hour')" class="control-button" style="flex: 1; font-size: 11px;">+1h</button>
|
||
</div>
|
||
<div style="display: flex; gap: 8px;">
|
||
<button onclick="shiftTrackTime(-1, 'day')" class="control-button" style="flex: 1; font-size: 11px;">-1d</button>
|
||
<button onclick="shiftTrackTime(1, 'day')" class="control-button" style="flex: 1; font-size: 11px;">+1d</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<input type="file" id="gpxFileInput" accept=".gpx,.json" multiple style="display: none;" onchange="handleGPXFiles(event)">
|
||
|
||
<div class="main-content">
|
||
<div class="map-container">
|
||
<div id="map"></div>
|
||
</div>
|
||
|
||
<div class="points-panel">
|
||
<div class="points-header">
|
||
Points (<span id="pointCount">0</span>)
|
||
</div>
|
||
<div class="points-summary" id="pointsSummary">
|
||
Click on the map to add points
|
||
</div>
|
||
<div class="points-list" id="pointsList">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="status">
|
||
<span id="statusText">Ready to create GPX track</span>
|
||
<div class="speed-legend-inline" id="speedLegendInline">
|
||
<!-- Speed legend will be populated by JavaScript -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- About Dialog -->
|
||
<div class="about-dialog" id="aboutDialog">
|
||
<div class="about-content">
|
||
<div class="about-header">
|
||
<h2 class="about-title">About GPX Generator</h2>
|
||
<button class="about-close" onclick="closeAboutDialog()">×</button>
|
||
</div>
|
||
<div class="about-text">
|
||
<p>This interactive GPX generator is designed for creating realistic GPS test data for location-based applications. It's particularly useful for testing and development of GPS tracking systems, route planning applications, and location analytics tools.</p>
|
||
|
||
<p><strong>Features:</strong></p>
|
||
<ul style="margin: 12px 0; padding-left: 20px;">
|
||
<li>Interactive map-based point creation</li>
|
||
<li>Realistic speed and elevation simulation</li>
|
||
<li>Customizable GPS accuracy and timing</li>
|
||
<li>Multiple track support with export capabilities</li>
|
||
<li>Paint mode for continuous track drawing</li>
|
||
</ul>
|
||
|
||
<p><strong>Use Cases:</strong></p>
|
||
<ul style="margin: 12px 0; padding-left: 20px;">
|
||
<li>Testing location-based mobile applications</li>
|
||
<li>Creating sample data for GPS analytics</li>
|
||
<li>Simulating user movement patterns</li>
|
||
<li>Generating test routes for navigation systems</li>
|
||
</ul>
|
||
|
||
<p>This tool is part of the <a href="https://github.com/dedicatedcode/reitti" target="_blank" rel="noopener" class="about-link">Reitti project</a> - an open-source location tracking and analytics platform.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- JSON Date Picker Dialog -->
|
||
<div class="about-dialog" id="jsonDatePicker">
|
||
<div class="about-content">
|
||
<div class="about-header">
|
||
<h2 class="about-title">Select Dates to Import</h2>
|
||
<button class="about-close" onclick="closeJsonPicker()">×</button>
|
||
</div>
|
||
<div class="about-text">
|
||
<p>Select a range of dates from the Google Records file. Click a date to select it, or use Shift+Click to select a range.</p>
|
||
<div id="dateGrid" class="date-grid"></div>
|
||
<div style="display: flex; justify-content: flex-end; gap: 12px; margin-top: 20px;">
|
||
<button class="control-button" onclick="closeJsonPicker()">Cancel</button>
|
||
<button class="control-button success" onclick="importSelectedJsonDates()">Import Selected</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||
<script src="gpx-generator.js"></script>
|
||
<script>
|
||
// Initialize edit mode state
|
||
let isEditMode = false;
|
||
let isEditDrawerOpen = false;
|
||
|
||
function toggleEditMode() {
|
||
isEditMode = !isEditMode;
|
||
const modeToggle = document.getElementById('modeToggle');
|
||
const headerDescription = document.getElementById('headerDescription');
|
||
|
||
if (isEditMode) {
|
||
modeToggle.innerHTML = '<span>✏️</span> Edit Mode';
|
||
modeToggle.classList.add('edit-mode');
|
||
headerDescription.textContent = 'Click on the map to add points. Right-click points to remove them.';
|
||
// Open the edit drawer automatically
|
||
if (!isEditDrawerOpen) {
|
||
toggleEditDrawer();
|
||
}
|
||
// Enable point addition (this will be handled in gpx-generator.js)
|
||
if (window.enableEditMode) {
|
||
window.enableEditMode();
|
||
}
|
||
} else {
|
||
modeToggle.innerHTML = '<span>📍</span> View Mode';
|
||
modeToggle.classList.remove('edit-mode');
|
||
headerDescription.textContent = 'Interactive tool for creating and editing GPS tracks';
|
||
// Close edit drawer if open
|
||
if (isEditDrawerOpen) {
|
||
toggleEditDrawer();
|
||
}
|
||
// Disable point addition (this will be handled in gpx-generator.js)
|
||
if (window.disableEditMode) {
|
||
window.disableEditMode();
|
||
}
|
||
}
|
||
}
|
||
|
||
function toggleEditDrawer() {
|
||
isEditDrawerOpen = !isEditDrawerOpen;
|
||
const drawer = document.getElementById('editDrawer');
|
||
|
||
if (isEditDrawerOpen) {
|
||
drawer.classList.add('open');
|
||
} else {
|
||
drawer.classList.remove('open');
|
||
}
|
||
}
|
||
|
||
function openAboutDialog() {
|
||
document.getElementById('aboutDialog').classList.add('open');
|
||
}
|
||
|
||
function closeAboutDialog() {
|
||
document.getElementById('aboutDialog').classList.remove('open');
|
||
}
|
||
|
||
// Close about dialog when clicking outside
|
||
document.getElementById('aboutDialog').addEventListener('click', function(e) {
|
||
if (e.target === this) {
|
||
closeAboutDialog();
|
||
}
|
||
});
|
||
|
||
// Initialize the page in view mode
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
// Ensure we start in view mode
|
||
isEditMode = false;
|
||
isEditDrawerOpen = false;
|
||
|
||
// Set up initial UI state
|
||
const modeToggle = document.getElementById('modeToggle');
|
||
const headerDescription = document.getElementById('headerDescription');
|
||
|
||
modeToggle.innerHTML = '<span>📍</span> View Mode';
|
||
modeToggle.classList.remove('edit-mode');
|
||
headerDescription.textContent = 'Interactive tool for creating and editing GPS tracks';
|
||
|
||
// Ensure drawer is closed
|
||
const drawer = document.getElementById('editDrawer');
|
||
drawer.classList.remove('open');
|
||
|
||
// Disable edit mode in the map (this will be handled in gpx-generator.js)
|
||
setTimeout(() => {
|
||
if (window.disableEditMode) {
|
||
window.disableEditMode();
|
||
}
|
||
}, 100);
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|