galleria_photo_face_rknn_op.../api_v1/scanner/parsers/parseFit.js
2026-10-03 12:16:58 +02:00

408 lines
8.7 KiB
JavaScript

// api_v1/scanner/parsers/parseFit.js
const { execFile } = require("child_process");
function readFitFrames(absPath) {
return new Promise(
(resolve, reject) => {
execFile(
"/home/orangepi/.local/pipx/venvs/fitdecode/bin/python",
[
"api_v1/scanner/parsers/parse_fit.py",
absPath,
],
{
maxBuffer:
1024 * 1024 * 50,
},
(error, stdout) => {
if (error) {
reject(error);
return;
}
try {
resolve(
JSON.parse(stdout),
);
} catch (parseError) {
reject(
new Error(
"Errore nel parsing JSON FIT: " +
parseError.message,
),
);
}
},
);
},
);
}
// FIT position_lat / position_long sono in semicircles.
// Conversione ufficiale: degrees = semicircles * 180 / 2^31
function fitSemicirclesToDegrees(value) {
if (value == null) return null;
const n = Number(value);
if (!Number.isFinite(n)) return null;
return n * (180 / 2147483648);
}
function buildTrackPointsFromFitRecords(
records,
) {
if (!Array.isArray(records)) {
return [];
}
const points = [];
for (const record of records) {
const fields =
record?.fields || {};
const rawLat =
fields.position_lat ??
fields.latitude ??
fields.lat;
const rawLng =
fields.position_long ??
fields.position_lon ??
fields.longitude ??
fields.lng ??
fields.lon;
const lat =
fitSemicirclesToDegrees(
rawLat,
);
const lng =
fitSemicirclesToDegrees(
rawLng,
);
if (
!Number.isFinite(lat) ||
!Number.isFinite(lng)
) {
continue;
}
const point = {
lat,
lng,
};
const altitudeRaw =
fields.altitude ??
fields.enhanced_altitude;
const altitude =
altitudeRaw != null
? Number(altitudeRaw)
: null;
if (
Number.isFinite(altitude)
) {
point.alt = altitude;
}
points.push(point);
}
return points;
}
function buildTrackPreviewFromFitRecords(
records,
maxPoints = 300,
) {
const points =
buildTrackPointsFromFitRecords(
records,
);
if (
points.length <= maxPoints
) {
return points;
}
const step =
Math.ceil(
points.length / maxPoints,
);
const preview = [];
for (
let index = 0;
index < points.length;
index += step
) {
preview.push(
points[index],
);
}
const last =
points.at(-1);
const previewLast =
preview.at(-1);
if (
last &&
(
!previewLast ||
previewLast.lat !==
last.lat ||
previewLast.lng !==
last.lng
)
) {
preview.push(last);
}
return preview;
}
async function parseFit(absPath) {
const frames =
await readFitFrames(
absPath,
);
// -------------------------------
// 1. Trova i frame principali
// -------------------------------
const sessions = frames.filter(f => f.name === "session");
const laps = frames.filter(f => f.name === "lap");
const records = frames.filter(f => f.name === "record");
const fileId = frames.find(f => f.name === "file_id");
const session = sessions[0]?.fields || {};
const file = fileId?.fields || {};
// -------------------------------
// 2. Tempi
// -------------------------------
const startTime = session.start_time || session.start_time_local || null;
const endTime = session.total_timer_time && startTime
? new Date(new Date(startTime).getTime() + session.total_timer_time * 1000)
: null;
const startMillis = startTime ? new Date(startTime).getTime() : null;
const endMillis = endTime ? endTime.getTime() : null;
const durationMillis = session.total_timer_time
? session.total_timer_time * 1000
: endMillis && startMillis
? endMillis - startMillis
: null;
// -------------------------------
// 3. Distanza / Elevazione
// -------------------------------
const distance = session.total_distance || null;
const elevGain = session.total_ascent || null;
const elevLoss = session.total_descent || null;
// -------------------------------
// 4. HR / Cadenza / Velocità
// -------------------------------
const avgHr = session.avg_heart_rate || null;
const maxHr = session.max_heart_rate || null;
const avgCad = session.avg_cadence || null;
const maxCad = session.max_cadence || null;
const avgSpeed = session.avg_speed || null;
const maxSpeed = session.max_speed || null;
// -------------------------------
// 5. Bounding box GPS
// -------------------------------
let minLat = null;
let maxLat = null;
let minLon = null;
let maxLon = null;
let minElev = null;
let maxElev = null;
for (const r of records) {
const f = r.fields || {};
const lat = fitSemicirclesToDegrees(f.position_lat);
const lon = fitSemicirclesToDegrees(f.position_long);
const eleRaw =
f.altitude ??
f.enhanced_altitude;
const ele = eleRaw != null ? Number(eleRaw) : null;
if (Number.isFinite(lat) && Number.isFinite(lon)) {
minLat = minLat == null ? lat : Math.min(minLat, lat);
maxLat = maxLat == null ? lat : Math.max(maxLat, lat);
minLon = minLon == null ? lon : Math.min(minLon, lon);
maxLon = maxLon == null ? lon : Math.max(maxLon, lon);
}
if (Number.isFinite(ele)) {
minElev = minElev == null ? ele : Math.min(minElev, ele);
maxElev = maxElev == null ? ele : Math.max(maxElev, ele);
}
}
const centerLat =
minLat != null && maxLat != null ? (minLat + maxLat) / 2 : null;
const centerLon =
minLon != null && maxLon != null ? (minLon + maxLon) / 2 : null;
// -------------------------------
// 6. Track preview per gallery
// -------------------------------
const trackPreview = buildTrackPreviewFromFitRecords(records, 300);
// Log utile temporaneo. Puoi rimuoverlo dopo il test.
console.log("[parseFit] track preview", {
file: absPath,
records: records.length,
preview: trackPreview.length,
first: trackPreview[0],
minLat,
maxLat,
minLon,
maxLon,
});
// -------------------------------
// 7. Costruzione oggetto attività finale
// -------------------------------
return {
format: "FIT",
display_name: session.sport || null,
route_name: session.sport || null,
title: session.sport || null,
description: null,
notes: null,
sport: session.sport || null,
activity_type: session.sub_sport || null,
device_manufacturer: file.manufacturer || null,
device_model: file.product || null,
source_app: null,
start_time_millis: startMillis,
end_time_millis: endMillis,
duration_millis: durationMillis,
distance_meters: distance,
elevation_gain_meters: elevGain,
elevation_loss_meters: elevLoss,
min_elevation_meters: minElev,
max_elevation_meters: maxElev,
file_distance_meters: distance,
file_elevation_gain_meters: elevGain,
computed_distance_meters: distance,
computed_elevation_gain_meters: elevGain,
average_speed_mps: avgSpeed,
max_speed_mps: maxSpeed,
average_pace_sec_per_km: avgSpeed ? 1000 / avgSpeed : null,
average_heart_rate: avgHr,
max_heart_rate: maxHr,
average_cadence: avgCad,
max_cadence: maxCad,
calories: session.total_calories || null,
power_avg: session.avg_power || null,
power_max: session.max_power || null,
point_count: records.length,
segment_count: laps.length,
lap_count: laps.length,
min_latitude: minLat,
max_latitude: maxLat,
min_longitude: minLon,
max_longitude: maxLon,
center_latitude: centerLat,
center_longitude: centerLon,
// Preview ridotta del percorso per i thumb della gallery
track_preview: trackPreview,
tags: [],
diagnostics: {
source: "FIT",
sessions: sessions.length,
laps: laps.length,
records: records.length,
track_preview_points: trackPreview.length,
coordinates_converted_from_semicircles: true,
},
};
}
async function parseFitTrackPoints(
absPath,
) {
if (!absPath) {
return [];
}
const frames =
await readFitFrames(
absPath,
);
const records =
frames.filter(
(frame) =>
frame?.name ===
"record",
);
const points =
buildTrackPointsFromFitRecords(
records,
);
/*
console.log(
"[parseFitTrackPoints] traccia completa",
{
file:
absPath,
records:
records.length,
gpsPoints:
points.length,
},
);
*/
return points;
}
module.exports =
parseFit;
module.exports.parseFitTrackPoints =
parseFitTrackPoints;