Merge pull request #641 from tbsmark86/master

Exporting to FIT #322
This commit is contained in:
Norbert Renner 2022-10-01 12:08:51 +02:00 committed by GitHub
commit 7d76ac513e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 136 additions and 28 deletions

View file

@ -31,7 +31,9 @@ BR.Export = L.Class.extend({
L.DomEvent.addListener(document, 'keydown', this._keydownListener, this);
$('#export').on('show.bs.modal', this._warnStraightLine.bind(this));
$('#export').on('show.bs.modal', this._warnDownload.bind(this));
$('#export input[name=format]').on('change', this._warnDownload.bind(this));
$('#export').on('show.bs.modal', this._turnInstructionInfo.bind(this));
this.update([]);
},
@ -47,22 +49,34 @@ BR.Export = L.Class.extend({
}
},
_warnStraightLine: function () {
_warnDownload: function () {
const hasBeeline = BR.Routing.hasBeeline(this.segments);
document.getElementById('export-beeline-warning').hidden = !hasBeeline;
const isFit = $('#format-fit').prop('checked');
$('#export-download-warning').prop('hidden', !hasBeeline && !isFit);
let title = 'Download from server (deprecated)';
if (hasBeeline) {
title = '[Warning: straight lines not supported] ' + title;
}
if (isFit) {
title = '[Warning: FIT not supported] ' + title;
}
document.getElementById('serverExport').title = title;
},
_turnInstructionInfo: function () {
const turnInstructionMode = +this.profile.getProfileVar('turnInstructionMode');
$('.format-turns-enabled')
.prop('hidden', turnInstructionMode <= 1)
.attr('title', i18next.t('export.turns_enabled'));
},
_getMimeType: function (format) {
const mimeTypeMap = {
gpx: 'application/gpx+xml',
kml: 'application/vnd.google-earth.kml+xml',
geojson: 'application/vnd.geo+json',
csv: 'text/tab-separated-values',
gpx: 'application/gpx+xml;charset=utf-8',
kml: 'application/vnd.google-earth.kml+xml;charset=utf-8',
geojson: 'application/vnd.geo+json;charset=utf-8',
csv: 'text/tab-separated-values;charset=utf-8',
fit: 'application/vnd.ant.fit',
};
return mimeTypeMap[format];
@ -98,15 +112,18 @@ BR.Export = L.Class.extend({
const track = this._formatTrack(format, name, includeWaypoints);
const fileName = (name || 'brouter') + '.' + format;
const mimeType = this._getMimeType(format);
const blob = new Blob([track], {
type: mimeType + ';charset=utf-8',
type: this._getMimeType(format),
});
const reader = new FileReader();
reader.onload = (e) => this._triggerDownload(reader.result, fileName);
reader.readAsDataURL(blob);
} else {
if (format === 'fit') {
// Server can't handle fit - downgrade to gpx
format = 'gpx';
}
var serverUrl = this.router.getUrl(
this.latLngs,
null,
@ -141,6 +158,8 @@ BR.Export = L.Class.extend({
return JSON.stringify(track, null, 2);
case 'csv':
return BR.Csv.format(track);
case 'fit':
return BR.Fit.format(track);
default:
break;
}

63
js/format/Fit.js Normal file
View file

@ -0,0 +1,63 @@
BR.Fit = {
format: function (geoJson, turnInstructionMode = 0) {
if (!geoJson?.features) return '';
function calcDistance(p1, p2) {
const [ilon1, ilat1] = btools.util.CheapRuler.toIntegerLngLat(p1);
const [ilon2, ilat2] = btools.util.CheapRuler.toIntegerLngLat(p2);
return btools.util.CheapRuler.distance(ilon1, ilat1, ilon2, ilat2);
}
const course = geoJson.features[0];
const track = course.geometry.coordinates;
const startPoint = track[0];
const endPoint = track[track.length - 1];
let trackStamps = course.properties.times || course.properties.coordTimes || [];
let last = 0;
trackStamps = trackStamps.map((stamp) => {
stamp = Math.round(stamp); // FIT only support seconds
// avoid strange & identical timestamps. This is required(?) for matching
// of turn instructions with the route.
if (stamp <= last) {
stamp = last + 1;
}
last = stamp;
// FIT epoch starts at 1989-12-31T00:00 - avoid wrapping
return (631065600 + stamp) * 1000;
});
const encoder = new FITCourseFile(
course.properties.name,
trackStamps[0],
course.properties['total-time'],
startPoint,
endPoint,
course.properties['filtered-ascend']
);
let distanceTotal = 0;
let distanceTillPoint = [0];
encoder.point(trackStamps[0], track[0], track[0][2], 0);
for (let i = 1; i < track.length; i++) {
distanceTotal += calcDistance(track[i - 1], track[i]);
distanceTillPoint[i] = distanceTotal;
encoder.point(trackStamps[i], track[i], track[i][2], distanceTotal);
}
// Note 1: Mixing points and hints is also legal should some devices
// require it - until then keep it simple.
// Note 2: The distance inside the FIT files seems somewhat redundant.
// Not sure if it is really required. May depend on the device?
// Note 3: Can't use hint.distance that is the value to the next turn.
const voiceHints = BR.voiceHints(geoJson, 2);
voiceHints._loopHints((hint, cmd, coord) => {
encoder.turn(
trackStamps[hint.indexInTrack],
coord,
cmd.fit,
cmd.fit === 'generic' ? cmd.message : undefined,
distanceTillPoint[hint.indexInTrack]
);
});
return encoder.finalize(trackStamps[trackStamps.length - 1]);
},
};

View file

@ -1,11 +1,11 @@
(function () {
class Command {
constructor(name, locus, orux, symbol, message) {
constructor(name, locus, orux, symbol, fit, message) {
this.name = name;
this.locus = locus;
this.orux = orux;
this.symbol = symbol;
this.message = message;
(this.fit = fit), (this.message = message);
}
}
@ -16,6 +16,7 @@
command.locus + exitNumber,
command.orux + exitNumber,
command.symbol + exitNumber,
command.fit,
command.message + exitNumber
);
}
@ -28,6 +29,7 @@
command.locus + -exitNumber,
command.orux + exitNumber,
command.symbol + -exitNumber,
command.fit,
command.message + -exitNumber
);
}
@ -114,20 +116,20 @@
// from BRouter btools.router.VoiceHint
VoiceHints.commands = (function () {
return {
1: new Command('C', 1, 1002, 'Straight', 'straight'),
2: new Command('TL', 4, 1000, 'Left', 'left'),
3: new Command('TSLL', 3, 1017, 'TSLL', 'slight left'),
4: new Command('TSHL', 5, 1019, 'TSHL', 'sharp left'),
5: new Command('TR', 7, 1001, 'Right', 'right'),
6: new Command('TSLR', 6, 1016, 'TSLR', 'slight right'),
7: new Command('TSHR', 8, 1018, 'TSHR', 'sharp right'),
8: new Command('KL', 9, 1015, 'TSLL', 'keep left'),
9: new Command('KR', 10, 1014, 'TSLR', 'keep right'),
10: new Command('TU', 13, 1003, 'TU', 'u-turn'),
11: new Command('TRU', 14, 1003, 'TU', 'u-turn'), // Right U-turn
12: new Command('OFFR'), // Off route
13: new Command('RNDB', 26, 1008, 'RNDB', 'Take exit '), // Roundabout
14: new Command('RNLB', 26, 1008, 'RNLB', 'Take exit '), // Roundabout left
1: new Command('C', 1, 1002, 'Straight', 'straight', 'straight'),
2: new Command('TL', 4, 1000, 'Left', 'left', 'left'),
3: new Command('TSLL', 3, 1017, 'TSLL', 'slight_left', 'slight left'),
4: new Command('TSHL', 5, 1019, 'TSHL', 'sharp_left', 'sharp left'),
5: new Command('TR', 7, 1001, 'Right', 'right', 'right'),
6: new Command('TSLR', 6, 1016, 'TSLR', 'slight_right', 'slight right'),
7: new Command('TSHR', 8, 1018, 'TSHR', 'sharp_right', 'sharp right'),
8: new Command('KL', 9, 1015, 'TSLL', 'left_fork', 'keep left'),
9: new Command('KR', 10, 1014, 'TSLR', 'right_fork', 'keep right'),
10: new Command('TU', 13, 1003, 'TU', 'u_turn', 'u-turn'),
11: new Command('TRU', 14, 1003, 'TU', 'u_turn', 'u-turn'), // Right U-turn
12: new Command('OFFR', undefined, undefined, undefined, 'danger', undefined), // Off route
13: new Command('RNDB', 26, 1008, 'RNDB', 'generic', 'Take exit '), // Roundabout
14: new Command('RNLB', 26, 1008, 'RNLB', 'generic', 'Take exit '), // Roundabout left
};
})();