595 feature request add google records import to test data tool (#597)

This commit is contained in:
Daniel Graf
2025-12-28 10:25:46 +01:00
committed by GitHub
parent 5aafbd8e77
commit 852aaeae09
2 changed files with 312 additions and 54 deletions

View File

@@ -18,6 +18,12 @@ let currentMapLayer = 'street'; // 'street' or 'satellite'
let streetLayer, satelliteLayer;
let stopProbability = 0.05; // 5% chance of adding a stop per point when auto-stops enabled
// Google JSON Import State
let pendingJsonData = null;
let pendingJsonFilename = "";
let selectedDates = new Set();
let lastClickedDate = null;
// Track colors for visual distinction
const TRACK_COLORS = [
'#3498db', '#e74c3c', '#2ecc71', '#f39c12', '#9b59b6',
@@ -245,7 +251,7 @@ function showSpeedChangeNotification(speed) {
}, 2000);
}
function createNewTrack() {
function createNewTrack(name = null, startTime = null) {
const trackIndex = tracks.length;
const color = TRACK_COLORS[trackIndex % TRACK_COLORS.length];
@@ -255,23 +261,23 @@ function createNewTrack() {
});
// Set start time based on last point of previous track or current time
let startTime = new Date();
if (tracks.length > 0) {
let trackStartTime = startTime || new Date();
if (!startTime && tracks.length > 0) {
const lastTrack = tracks[tracks.length - 1];
if (lastTrack.points.length > 0) {
const lastPoint = lastTrack.points[lastTrack.points.length - 1];
const timeInterval = parseInt(document.getElementById('timeInterval').value);
startTime = new Date(lastPoint.timestamp.getTime() + (timeInterval * 1000));
trackStartTime = new Date(lastPoint.timestamp.getTime() + (timeInterval * 1000));
}
}
const track = {
id: trackIndex,
name: `Track ${trackIndex + 1}`,
name: name || `Track ${trackIndex + 1}`,
points: [],
color: color,
collapsed: false,
startTime: startTime
startTime: trackStartTime
};
tracks.push(track);
@@ -287,6 +293,7 @@ function createNewTrack() {
updatePointsList();
updateStatus();
return track;
}
function newTrack() {
@@ -331,7 +338,7 @@ function onMapClick(e) {
}
}
function addPoint(lat, lng) {
function addPoint(lat, lng, options = {}) {
if (tracks.length === 0) {
createNewTrack();
}
@@ -339,29 +346,35 @@ function addPoint(lat, lng) {
const currentTrack = tracks[currentTrackIndex];
const pointIndex = currentTrack.points.length;
// Use current datetime input value as timestamp
const startDateTimeValue = document.getElementById('startDateTime').value;
let timestamp = new Date(startDateTimeValue);
// Use provided timestamp or current datetime input value
let timestamp = options.timestamp;
if (!timestamp) {
const startDateTimeValue = document.getElementById('startDateTime').value;
timestamp = new Date(startDateTimeValue);
}
// Check if we need to create a new track due to day change
if (shouldCreateNewTrackForDayChange(timestamp, currentTrack)) {
if (!options.skipDayChangeCheck && shouldCreateNewTrackForDayChange(timestamp, currentTrack)) {
createNewTrack();
return addPoint(lat, lng); // Recursively add to new track
return addPoint(lat, lng, options); // Recursively add to new track
}
// Check for realistic stops
if (document.getElementById('autoStops').checked && shouldAddStop(currentTrack)) {
if (!options.skipStops && document.getElementById('autoStops').checked && shouldAddStop(currentTrack)) {
timestamp = addRealisticStop(timestamp);
}
// Apply GPS accuracy simulation
const accuracy = parseFloat(document.getElementById('accuracySlider').value);
const adjustedCoords = applyGPSNoise(lat, lng, accuracy);
const accuracy = options.accuracy !== undefined ? options.accuracy : parseFloat(document.getElementById('accuracySlider').value);
const adjustedCoords = options.skipNoise ? {lat, lng} : applyGPSNoise(lat, lng, accuracy);
// Calculate elevation with variation
const baseElevation = parseFloat(document.getElementById('elevation').value);
const elevationVariation = parseFloat(document.getElementById('elevationVariation').value);
const elevation = baseElevation + (Math.random() - 0.5) * 2 * elevationVariation;
let elevation = options.elevation;
if (elevation === undefined) {
const baseElevation = parseFloat(document.getElementById('elevation').value);
const elevationVariation = parseFloat(document.getElementById('elevationVariation').value);
elevation = baseElevation + (Math.random() - 0.5) * 2 * elevationVariation;
}
const point = {
id: pointIndex,
@@ -396,23 +409,27 @@ function addPoint(lat, lng) {
redrawMarkers();
// Update datetime input to next interval for next point
const timeInterval = parseInt(document.getElementById('timeInterval').value);
const nextTimestamp = new Date(timestamp.getTime() + (timeInterval * 1000));
// Format for datetime-local input (YYYY-MM-DDTHH:mm:ss)
const year = nextTimestamp.getFullYear();
const month = String(nextTimestamp.getMonth() + 1).padStart(2, '0');
const day = String(nextTimestamp.getDate()).padStart(2, '0');
const hours = String(nextTimestamp.getHours()).padStart(2, '0');
const minutes = String(nextTimestamp.getMinutes()).padStart(2, '0');
const seconds = String(nextTimestamp.getSeconds()).padStart(2, '0');
const datetimeString = `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
document.getElementById('startDateTime').value = datetimeString;
if (!options.skipTimeUpdate) {
const timeInterval = parseInt(document.getElementById('timeInterval').value);
const nextTimestamp = new Date(timestamp.getTime() + (timeInterval * 1000));
// Format for datetime-local input (YYYY-MM-DDTHH:mm:ss)
const year = nextTimestamp.getFullYear();
const month = String(nextTimestamp.getMonth() + 1).padStart(2, '0');
const day = String(nextTimestamp.getDate()).padStart(2, '0');
const hours = String(nextTimestamp.getHours()).padStart(2, '0');
const minutes = String(nextTimestamp.getMinutes()).padStart(2, '0');
const seconds = String(nextTimestamp.getSeconds()).padStart(2, '0');
const datetimeString = `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
document.getElementById('startDateTime').value = datetimeString;
}
// Update UI
updatePointsList();
updateStatus();
if (!options.skipUIUpdate) {
updatePointsList();
updateStatus();
}
// Clear preview line
previewLine.setLatLngs([]);
@@ -881,26 +898,17 @@ function handleGPXFiles(event) {
const files = Array.from(event.target.files);
if (files.length === 0) return;
let processedFiles = 0;
let totalPoints = 0;
let totalTracks = 0;
files.forEach((file, index) => {
const reader = new FileReader();
reader.onload = function(e) {
try {
const result = parseAndImportGPX(e.target.result, file.name, index === 0);
totalPoints += result.pointsCount;
totalTracks += result.tracksCount;
processedFiles++;
// Show summary when all files are processed
if (processedFiles === files.length) {
alert(`Successfully imported ${files.length} GPX file(s):\n${totalPoints} total points split into ${totalTracks} track(s).`);
if (file.name.toLowerCase().endsWith('.json')) {
handleGoogleJson(e.target.result, file.name);
} else {
try {
const result = parseAndImportGPX(e.target.result, file.name, index === 0);
} catch (error) {
alert(`Error parsing GPX file "${file.name}": ${error.message}`);
}
} catch (error) {
alert(`Error parsing GPX file "${file.name}": ${error.message}`);
processedFiles++;
}
};
reader.readAsText(file);
@@ -910,9 +918,151 @@ function handleGPXFiles(event) {
event.target.value = '';
}
function handleGPXFile(event) {
// Keep backward compatibility
handleGPXFiles(event);
function handleGoogleJson(content, filename) {
try {
const data = JSON.parse(content);
const locations = data.locations || [];
if (locations.length === 0) {
alert("No location data found in JSON.");
return;
}
// Group points by date
const days = {};
locations.forEach(loc => {
const timestamp = loc.timestamp || loc.timestampMs;
if (!timestamp) return;
const date = new Date(timestamp);
const dateStr = date.toISOString().split('T')[0];
if (!days[dateStr]) days[dateStr] = [];
days[dateStr].push({
lat: loc.latitudeE7 / 1e7,
lng: loc.longitudeE7 / 1e7,
timestamp: date,
elevation: loc.altitude || 0,
accuracy: loc.accuracy || 0
});
});
pendingJsonData = days;
pendingJsonFilename = filename.replace('.json', '');
showJsonDatePicker();
} catch (e) {
alert("Error parsing Google Records JSON: " + e.message);
}
}
function showJsonDatePicker() {
const modal = document.getElementById('jsonDatePicker');
const grid = document.getElementById('dateGrid');
grid.innerHTML = '';
selectedDates.clear();
lastClickedDate = null;
const sortedDates = Object.keys(pendingJsonData).sort();
sortedDates.forEach(date => {
const div = document.createElement('div');
div.className = 'date-item';
div.textContent = date;
div.dataset.date = date;
div.onclick = (e) => handleDateClick(date, e.shiftKey);
grid.appendChild(div);
});
modal.classList.add('open');
}
function handleDateClick(date, isShift) {
const sortedDates = Object.keys(pendingJsonData).sort();
if (isShift && lastClickedDate) {
const startIdx = sortedDates.indexOf(lastClickedDate);
const endIdx = sortedDates.indexOf(date);
const [low, high] = [Math.min(startIdx, endIdx), Math.max(startIdx, endIdx)];
selectedDates.clear();
for (let i = low; i <= high; i++) {
selectedDates.add(sortedDates[i]);
}
} else {
if (selectedDates.has(date)) {
selectedDates.delete(date);
} else {
selectedDates.add(date);
}
lastClickedDate = date;
}
updateDateGridUI();
}
function updateDateGridUI() {
const items = document.querySelectorAll('.date-item');
items.forEach(item => {
if (selectedDates.has(item.dataset.date)) {
item.classList.add('selected');
} else {
item.classList.remove('selected');
}
});
}
function closeJsonPicker() {
document.getElementById('jsonDatePicker').classList.remove('open');
pendingJsonData = null;
}
function importSelectedJsonDates() {
if (selectedDates.size === 0) {
alert("Please select at least one date.");
return;
}
const sortedSelected = Array.from(selectedDates).sort();
let totalPoints = 0;
const bounds = L.latLngBounds();
// Collapse all existing tracks first
tracks.forEach(track => {
track.collapsed = true;
});
sortedSelected.forEach((dateStr, index) => {
const dayPoints = pendingJsonData[dateStr];
// Sort points within the day
dayPoints.sort((a, b) => a.timestamp - b.timestamp);
const trackName = `${pendingJsonFilename} - ${dateStr}`;
const track = createNewTrack(trackName, dayPoints[0].timestamp);
// Ensure the newly created track is expanded if it's part of the batch
track.collapsed = false;
dayPoints.forEach(p => {
addPoint(p.lat, p.lng, {
timestamp: p.timestamp,
elevation: p.elevation,
accuracy: p.accuracy,
skipNoise: true,
skipDayChangeCheck: true,
skipStops: true,
skipTimeUpdate: true,
skipUIUpdate: true // Skip UI updates during batch
});
bounds.extend([p.lat, p.lng]);
totalPoints++;
});
});
if (totalPoints > 0) {
map.fitBounds(bounds.pad(0.1));
}
closeJsonPicker();
updatePointsList();
updateStatus();
redrawMarkers();
}
function parseAndImportGPX(gpxContent, filename, isFirstFile = true) {
@@ -1126,6 +1276,32 @@ function groupPointsByLocalDay(points) {
// New functions for Phase 2 features
function onMapMouseMove(e) {
// Check if we are hovering over a marker first
const containerPoint = map.mouseEventToContainerPoint(e.originalEvent);
const tolerance = 10; // pixels
let hoveredMarker = null;
for (let i = 0; i < markers.length; i++) {
const markerData = markers[i];
const markerPoint = map.latLngToContainerPoint([markerData.lat, markerData.lng]);
const dist = Math.sqrt(
Math.pow(containerPoint.x - markerPoint.x, 2) +
Math.pow(containerPoint.y - markerPoint.y, 2)
);
if (dist <= tolerance) {
hoveredMarker = markerData;
break;
}
}
if (hoveredMarker) {
showMarkerTooltip(e.originalEvent, hoveredMarker);
previewLine.setLatLngs([]);
return;
}
// Don't show preview or allow painting if edit mode is disabled
if (!editModeEnabled) {
hideHoverTooltip();
@@ -1184,6 +1360,33 @@ function onMapMouseOut(e) {
previewLine.setLatLngs([]);
}
function showMarkerTooltip(mouseEvent, markerData) {
const track = tracks[markerData.trackIndex];
const point = track.points[markerData.pointIndex];
const tooltip = hoverTooltip;
let speedText = '-';
let speedClass = 'speed-ok';
if (markerData.pointIndex > 0) {
const prevPoint = track.points[markerData.pointIndex - 1];
const speedInfo = calculateSpeedInfo(prevPoint, point);
speedText = `${speedInfo.speed.toFixed(1)} km/h`;
speedClass = getSpeedClass(speedInfo.speed);
}
tooltip.innerHTML = `
<div style="font-weight: 600; margin-bottom: 4px; color: ${markerData.color}">${track.name} - Point ${markerData.pointIndex + 1}</div>
<div>Time: ${formatTimestamp(point.timestamp)}</div>
<div class="${speedClass}">Speed: ${speedText}</div>
<div>Elevation: ${point.elevation.toFixed(1)}m</div>
`;
tooltip.style.left = (mouseEvent.pageX + 10) + 'px';
tooltip.style.top = (mouseEvent.pageY - 10) + 'px';
tooltip.style.display = 'block';
}
function showHoverTooltip(mouseEvent, latLng, distance, speed, speedClass) {
const tooltip = hoverTooltip;
const maxSpeed = parseFloat(document.getElementById('maxSpeed').value);
@@ -1579,4 +1782,3 @@ function shiftTrackTime(amount, unit) {
const direction = amount > 0 ? 'forward' : 'backward';
const absAmount = Math.abs(amount);
}

View File

@@ -805,6 +805,44 @@
.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>
@@ -923,7 +961,7 @@
</div>
</div>
<input type="file" id="gpxFileInput" accept=".gpx" multiple style="display: none;" onchange="handleGPXFiles(event)">
<input type="file" id="gpxFileInput" accept=".gpx,.json" multiple style="display: none;" onchange="handleGPXFiles(event)">
<div class="main-content">
<div class="map-container">
@@ -982,6 +1020,24 @@
</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>