added segments
This commit is contained in:
@@ -56,6 +56,9 @@
|
||||
<button class="btn btn-primary" id="download-btn">
|
||||
<i class="bi bi-download"></i> Download
|
||||
</button>
|
||||
<button class="btn btn-success" id="create-segment-btn" onclick="toggleSegmentMode()">
|
||||
<i class="bi bi-bezier2"></i> Create Segment
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -113,6 +116,35 @@
|
||||
<!-- Detailed Metrics Grid -->
|
||||
<h4 class="mb-3">Detailed Metrics</h4>
|
||||
<div class="row g-3">
|
||||
<!-- Segments Card -->
|
||||
<div class="col-12 mb-4">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">Matched Segments</h5>
|
||||
<!-- Could trigger re-scan here -->
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0" id="efforts-table">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Segment</th>
|
||||
<th>Time</th>
|
||||
<th>Awards</th>
|
||||
<th>Rank</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr id="efforts-loading">
|
||||
<td colspan="4" class="text-center text-muted">Loading segments...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Heart Rate -->
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 metric-card border-danger">
|
||||
@@ -417,6 +449,7 @@
|
||||
const res = await fetch(`/api/activities/${activityId}/details`);
|
||||
if (!res.ok) throw new Error("Failed to load details");
|
||||
const data = await res.json();
|
||||
window.currentDbId = data.id; // Store for segment creation
|
||||
|
||||
// Header
|
||||
document.getElementById('act-name').textContent = data.activity_name || 'Untitled Activity';
|
||||
@@ -460,6 +493,11 @@
|
||||
document.getElementById('m-bike-info').innerHTML = txt;
|
||||
}
|
||||
|
||||
// Load Efforts
|
||||
if (window.currentDbId) {
|
||||
loadEfforts(window.currentDbId);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showToast("Error", "Failed to load activity details", "error");
|
||||
@@ -472,6 +510,10 @@
|
||||
if (res.ok) {
|
||||
const geojson = await res.json();
|
||||
if (geojson.features && geojson.features.length > 0 && geojson.features[0].geometry.coordinates.length > 0) {
|
||||
// GeoJSON coords are [lon, lat]. Leaflet wants [lat, lon]
|
||||
const coords = geojson.features[0].geometry.coordinates;
|
||||
trackPoints = coords.map(p => [p[1], p[0]]);
|
||||
|
||||
const layer = L.geoJSON(geojson, {
|
||||
style: { color: 'red', weight: 4, opacity: 0.7 }
|
||||
}).addTo(map);
|
||||
@@ -492,5 +534,190 @@
|
||||
}
|
||||
|
||||
function formatDuration(s) { if (!s) return '-'; const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), sec = s % 60; return `${h}h ${m}m ${sec}s`; }
|
||||
|
||||
// Segment Creation Logic
|
||||
let segmentMode = false;
|
||||
let startMarker = null;
|
||||
let endMarker = null;
|
||||
let trackPoints = []; // List of [lat, lon] from GeoJSON
|
||||
let startIndex = 0;
|
||||
let endIndex = 0;
|
||||
|
||||
function toggleSegmentMode() {
|
||||
segmentMode = !segmentMode;
|
||||
const btn = document.getElementById('create-segment-btn');
|
||||
if (segmentMode) {
|
||||
btn.classList.add('active');
|
||||
btn.innerHTML = '<i class="bi bi-check-lg"></i> Save Segment';
|
||||
btn.onclick = saveSegment;
|
||||
initSegmentMarkers();
|
||||
} else {
|
||||
// Cancelled
|
||||
btn.classList.remove('active');
|
||||
btn.innerHTML = '<i class="bi bi-bezier2"></i> Create Segment';
|
||||
btn.onclick = toggleSegmentMode;
|
||||
removeSegmentMarkers();
|
||||
}
|
||||
}
|
||||
|
||||
function removeSegmentMarkers() {
|
||||
if (startMarker) map.removeLayer(startMarker);
|
||||
if (endMarker) map.removeLayer(endMarker);
|
||||
startMarker = null;
|
||||
endMarker = null;
|
||||
}
|
||||
|
||||
function initSegmentMarkers() {
|
||||
if (trackPoints.length < 2) {
|
||||
alert("Not enough points to create a segment.");
|
||||
toggleSegmentMode();
|
||||
return;
|
||||
}
|
||||
|
||||
// Default positions: 20% and 80%
|
||||
startIndex = Math.floor(trackPoints.length * 0.2);
|
||||
endIndex = Math.floor(trackPoints.length * 0.8);
|
||||
|
||||
const startIcon = L.divIcon({ className: 'bg-success rounded-circle border border-white', iconSize: [12, 12] });
|
||||
const endIcon = L.divIcon({ className: 'bg-danger rounded-circle border border-white', iconSize: [12, 12] });
|
||||
|
||||
startMarker = L.marker(trackPoints[startIndex], { draggable: true, icon: startIcon }).addTo(map);
|
||||
endMarker = L.marker(trackPoints[endIndex], { draggable: true, icon: endIcon }).addTo(map);
|
||||
|
||||
// Snap logic
|
||||
function setupDrag(marker, isStart) {
|
||||
marker.on('drag', function (e) {
|
||||
const ll = e.latlng;
|
||||
let closestDist = Infinity;
|
||||
let closestIdx = -1;
|
||||
// Simple snap for visual feedback during drag
|
||||
for (let i = 0; i < trackPoints.length; i++) {
|
||||
const d = map.distance(ll, trackPoints[i]);
|
||||
if (d < closestDist) {
|
||||
closestDist = d;
|
||||
closestIdx = i;
|
||||
}
|
||||
}
|
||||
// Optional: visual snap? Leaflet handles drag msg.
|
||||
});
|
||||
|
||||
marker.on('dragend', function (e) {
|
||||
const ll = e.target.getLatLng();
|
||||
let closestDist = Infinity;
|
||||
let closestIdx = -1;
|
||||
|
||||
// constrain search
|
||||
let searchStart = 0;
|
||||
let searchEnd = trackPoints.length;
|
||||
|
||||
if (isStart) {
|
||||
// Start marker: can search 0 to trackPoints.length
|
||||
// Heuristic: If we are modifying Start, look for points < endIndex (if valid).
|
||||
if (endIndex > 0) searchEnd = endIndex;
|
||||
} else {
|
||||
// End marker
|
||||
if (startIndex >= 0) searchStart = startIndex;
|
||||
}
|
||||
|
||||
// "Stickiness" logic
|
||||
const currentIndex = isStart ? startIndex : endIndex;
|
||||
const indexPenalty = 0.0001;
|
||||
|
||||
for (let i = searchStart; i < searchEnd; i++) {
|
||||
const d_spatial = map.distance(ll, trackPoints[i]);
|
||||
const d_index = Math.abs(i - currentIndex);
|
||||
|
||||
const score = d_spatial + (d_index * indexPenalty);
|
||||
|
||||
if (score < closestDist) {
|
||||
closestDist = score;
|
||||
closestIdx = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (closestIdx !== -1) {
|
||||
marker.setLatLng(trackPoints[closestIdx]);
|
||||
if (isStart) startIndex = closestIdx;
|
||||
else endIndex = closestIdx;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setupDrag(startMarker, true);
|
||||
setupDrag(endMarker, false);
|
||||
}
|
||||
|
||||
async function saveSegment() {
|
||||
if (startIndex >= endIndex) {
|
||||
alert("Start point must be before End point.");
|
||||
return;
|
||||
}
|
||||
|
||||
const name = prompt("Enter Segment Name:");
|
||||
if (!name) return;
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/segments/create', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: name,
|
||||
activity_id: window.currentDbId,
|
||||
start_index: startIndex,
|
||||
end_index: endIndex
|
||||
})
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
alert("Segment created!");
|
||||
toggleSegmentMode(); // Reset UI
|
||||
} else {
|
||||
const err = await res.json();
|
||||
alert("Error: " + err.detail);
|
||||
}
|
||||
// Load Segments
|
||||
loadEfforts(window.currentDbId);
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert("Error loading activity: " + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadEfforts(dbId) {
|
||||
const tbody = document.querySelector('#efforts-table tbody');
|
||||
try {
|
||||
const res = await fetch(`/api/activities/${dbId}/efforts`);
|
||||
if (res.ok) {
|
||||
const efforts = await res.json();
|
||||
tbody.innerHTML = '';
|
||||
if (efforts.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="4" class="text-center text-muted">No segments matched.</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
efforts.forEach(eff => {
|
||||
const tr = document.createElement('tr');
|
||||
let awards = '';
|
||||
if (eff.is_kom) awards += '<span class="badge bg-warning text-dark me-1"><i class="bi bi-trophy-fill"></i> CR</span>';
|
||||
if (eff.is_pr) awards += '<span class="badge bg-success me-1"><i class="bi bi-award-fill"></i> PR</span>';
|
||||
|
||||
tr.innerHTML = `
|
||||
<td><strong>${eff.segment_name}</strong></td>
|
||||
<td>${formatDuration(eff.elapsed_time)}</td>
|
||||
<td>${awards}</td>
|
||||
<td>${eff.kom_rank ? '#' + eff.kom_rank : '-'}</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
} else {
|
||||
tbody.innerHTML = '<tr><td colspan="4" class="text-center text-muted">Failed to load segments.</td></tr>';
|
||||
}
|
||||
} catch (e) {
|
||||
tbody.innerHTML = '<tr><td colspan="4" class="text-center text-muted">Error loading segments.</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
// Leaflet Map Init
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user