commit
7d76ac513e
9 changed files with 136 additions and 28 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
presets: [['@babel/preset-env', {}]],
|
presets: [['@babel/preset-env', {}]],
|
||||||
sourceType: 'script',
|
sourceType: 'script',
|
||||||
exclude: [/node_modules\/(?!overpass-layer|leaflet.locatecontrol\/).*/],
|
exclude: [/node_modules\/(?!overpass-layer|leaflet.locatecontrol|fit-file-writer\/).*/],
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -590,6 +590,10 @@ table.dataTable.display tbody tr:hover.selected {
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.format-turns-enabled {
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
/* tooltip */
|
/* tooltip */
|
||||||
|
|
||||||
.editing-tooltip,
|
.editing-tooltip,
|
||||||
|
|
|
||||||
16
index.html
16
index.html
|
|
@ -450,6 +450,7 @@
|
||||||
checked
|
checked
|
||||||
/>
|
/>
|
||||||
<span data-i18n="export.format_gpx">GPX</span>
|
<span data-i18n="export.format_gpx">GPX</span>
|
||||||
|
<i class="fa fa-location-arrow format-turns-enabled" hidden></i>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
|
|
@ -488,6 +489,19 @@
|
||||||
<span data-i18n="export.format_csv">CSV</span>
|
<span data-i18n="export.format_csv">CSV</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<label class="form-check-label" for="format-fit">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
id="format-fit"
|
||||||
|
type="radio"
|
||||||
|
name="format"
|
||||||
|
value="fit"
|
||||||
|
/>
|
||||||
|
<span data-i18n="export.format_fit">FIT</span>
|
||||||
|
<i class="fa fa-location-arrow format-turns-enabled" hidden></i>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
@ -520,7 +534,7 @@
|
||||||
<i class="fa fa-cloud-download"></i>
|
<i class="fa fa-cloud-download"></i>
|
||||||
<i
|
<i
|
||||||
hidden
|
hidden
|
||||||
id="export-beeline-warning"
|
id="export-download-warning"
|
||||||
class="fa fa-exclamation-triangle"
|
class="fa fa-exclamation-triangle"
|
||||||
style="font-size: 10px; position: absolute; margin-top: -3px; margin-left: -1px"
|
style="font-size: 10px; position: absolute; margin-top: -3px; margin-left: -1px"
|
||||||
></i>
|
></i>
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,9 @@ BR.Export = L.Class.extend({
|
||||||
|
|
||||||
L.DomEvent.addListener(document, 'keydown', this._keydownListener, this);
|
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([]);
|
this.update([]);
|
||||||
},
|
},
|
||||||
|
|
@ -47,22 +49,34 @@ BR.Export = L.Class.extend({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_warnStraightLine: function () {
|
_warnDownload: function () {
|
||||||
const hasBeeline = BR.Routing.hasBeeline(this.segments);
|
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)';
|
let title = 'Download from server (deprecated)';
|
||||||
if (hasBeeline) {
|
if (hasBeeline) {
|
||||||
title = '[Warning: straight lines not supported] ' + title;
|
title = '[Warning: straight lines not supported] ' + title;
|
||||||
}
|
}
|
||||||
|
if (isFit) {
|
||||||
|
title = '[Warning: FIT not supported] ' + title;
|
||||||
|
}
|
||||||
document.getElementById('serverExport').title = 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) {
|
_getMimeType: function (format) {
|
||||||
const mimeTypeMap = {
|
const mimeTypeMap = {
|
||||||
gpx: 'application/gpx+xml',
|
gpx: 'application/gpx+xml;charset=utf-8',
|
||||||
kml: 'application/vnd.google-earth.kml+xml',
|
kml: 'application/vnd.google-earth.kml+xml;charset=utf-8',
|
||||||
geojson: 'application/vnd.geo+json',
|
geojson: 'application/vnd.geo+json;charset=utf-8',
|
||||||
csv: 'text/tab-separated-values',
|
csv: 'text/tab-separated-values;charset=utf-8',
|
||||||
|
fit: 'application/vnd.ant.fit',
|
||||||
};
|
};
|
||||||
|
|
||||||
return mimeTypeMap[format];
|
return mimeTypeMap[format];
|
||||||
|
|
@ -98,15 +112,18 @@ BR.Export = L.Class.extend({
|
||||||
const track = this._formatTrack(format, name, includeWaypoints);
|
const track = this._formatTrack(format, name, includeWaypoints);
|
||||||
const fileName = (name || 'brouter') + '.' + format;
|
const fileName = (name || 'brouter') + '.' + format;
|
||||||
|
|
||||||
const mimeType = this._getMimeType(format);
|
|
||||||
const blob = new Blob([track], {
|
const blob = new Blob([track], {
|
||||||
type: mimeType + ';charset=utf-8',
|
type: this._getMimeType(format),
|
||||||
});
|
});
|
||||||
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (e) => this._triggerDownload(reader.result, fileName);
|
reader.onload = (e) => this._triggerDownload(reader.result, fileName);
|
||||||
reader.readAsDataURL(blob);
|
reader.readAsDataURL(blob);
|
||||||
} else {
|
} else {
|
||||||
|
if (format === 'fit') {
|
||||||
|
// Server can't handle fit - downgrade to gpx
|
||||||
|
format = 'gpx';
|
||||||
|
}
|
||||||
var serverUrl = this.router.getUrl(
|
var serverUrl = this.router.getUrl(
|
||||||
this.latLngs,
|
this.latLngs,
|
||||||
null,
|
null,
|
||||||
|
|
@ -141,6 +158,8 @@ BR.Export = L.Class.extend({
|
||||||
return JSON.stringify(track, null, 2);
|
return JSON.stringify(track, null, 2);
|
||||||
case 'csv':
|
case 'csv':
|
||||||
return BR.Csv.format(track);
|
return BR.Csv.format(track);
|
||||||
|
case 'fit':
|
||||||
|
return BR.Fit.format(track);
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
63
js/format/Fit.js
Normal file
63
js/format/Fit.js
Normal 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]);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
(function () {
|
(function () {
|
||||||
class Command {
|
class Command {
|
||||||
constructor(name, locus, orux, symbol, message) {
|
constructor(name, locus, orux, symbol, fit, message) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.locus = locus;
|
this.locus = locus;
|
||||||
this.orux = orux;
|
this.orux = orux;
|
||||||
this.symbol = symbol;
|
this.symbol = symbol;
|
||||||
this.message = message;
|
(this.fit = fit), (this.message = message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
command.locus + exitNumber,
|
command.locus + exitNumber,
|
||||||
command.orux + exitNumber,
|
command.orux + exitNumber,
|
||||||
command.symbol + exitNumber,
|
command.symbol + exitNumber,
|
||||||
|
command.fit,
|
||||||
command.message + exitNumber
|
command.message + exitNumber
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -28,6 +29,7 @@
|
||||||
command.locus + -exitNumber,
|
command.locus + -exitNumber,
|
||||||
command.orux + exitNumber,
|
command.orux + exitNumber,
|
||||||
command.symbol + -exitNumber,
|
command.symbol + -exitNumber,
|
||||||
|
command.fit,
|
||||||
command.message + -exitNumber
|
command.message + -exitNumber
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -114,20 +116,20 @@
|
||||||
// from BRouter btools.router.VoiceHint
|
// from BRouter btools.router.VoiceHint
|
||||||
VoiceHints.commands = (function () {
|
VoiceHints.commands = (function () {
|
||||||
return {
|
return {
|
||||||
1: new Command('C', 1, 1002, 'Straight', 'straight'),
|
1: new Command('C', 1, 1002, 'Straight', 'straight', 'straight'),
|
||||||
2: new Command('TL', 4, 1000, 'Left', 'left'),
|
2: new Command('TL', 4, 1000, 'Left', 'left', 'left'),
|
||||||
3: new Command('TSLL', 3, 1017, 'TSLL', 'slight left'),
|
3: new Command('TSLL', 3, 1017, 'TSLL', 'slight_left', 'slight left'),
|
||||||
4: new Command('TSHL', 5, 1019, 'TSHL', 'sharp left'),
|
4: new Command('TSHL', 5, 1019, 'TSHL', 'sharp_left', 'sharp left'),
|
||||||
5: new Command('TR', 7, 1001, 'Right', 'right'),
|
5: new Command('TR', 7, 1001, 'Right', 'right', 'right'),
|
||||||
6: new Command('TSLR', 6, 1016, 'TSLR', 'slight right'),
|
6: new Command('TSLR', 6, 1016, 'TSLR', 'slight_right', 'slight right'),
|
||||||
7: new Command('TSHR', 8, 1018, 'TSHR', 'sharp right'),
|
7: new Command('TSHR', 8, 1018, 'TSHR', 'sharp_right', 'sharp right'),
|
||||||
8: new Command('KL', 9, 1015, 'TSLL', 'keep left'),
|
8: new Command('KL', 9, 1015, 'TSLL', 'left_fork', 'keep left'),
|
||||||
9: new Command('KR', 10, 1014, 'TSLR', 'keep right'),
|
9: new Command('KR', 10, 1014, 'TSLR', 'right_fork', 'keep right'),
|
||||||
10: new Command('TU', 13, 1003, 'TU', 'u-turn'),
|
10: new Command('TU', 13, 1003, 'TU', 'u_turn', 'u-turn'),
|
||||||
11: new Command('TRU', 14, 1003, 'TU', 'u-turn'), // Right U-turn
|
11: new Command('TRU', 14, 1003, 'TU', 'u_turn', 'u-turn'), // Right U-turn
|
||||||
12: new Command('OFFR'), // Off route
|
12: new Command('OFFR', undefined, undefined, undefined, 'danger', undefined), // Off route
|
||||||
13: new Command('RNDB', 26, 1008, 'RNDB', 'Take exit '), // Roundabout
|
13: new Command('RNDB', 26, 1008, 'RNDB', 'generic', 'Take exit '), // Roundabout
|
||||||
14: new Command('RNLB', 26, 1008, 'RNLB', 'Take exit '), // Roundabout left
|
14: new Command('RNLB', 26, 1008, 'RNLB', 'generic', 'Take exit '), // Roundabout left
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,8 @@
|
||||||
"route-from-to": "{{from}} - {{to}} ({{distance}}km)",
|
"route-from-to": "{{from}} - {{to}} ({{distance}}km)",
|
||||||
"route-loop": "{{from}} ({{distance}}km)",
|
"route-loop": "{{from}} ({{distance}}km)",
|
||||||
"title": "Export route",
|
"title": "Export route",
|
||||||
"trackname": "Name"
|
"trackname": "Name",
|
||||||
|
"turns_enabled": "Includes turn instructions"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"ascend": "Ascend",
|
"ascend": "Ascend",
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@
|
||||||
"codemirror": "5.65.8",
|
"codemirror": "5.65.8",
|
||||||
"core-js-bundle": "3.25.1",
|
"core-js-bundle": "3.25.1",
|
||||||
"datatables": "1.10.18",
|
"datatables": "1.10.18",
|
||||||
|
"fit-file-writer": "tbsmark86/fit-file-writer#3eebe13",
|
||||||
"font-awesome": "4.7.0",
|
"font-awesome": "4.7.0",
|
||||||
"geo-data-exchange": "alexcojocaru/geo-data-exchange#v1.1.0",
|
"geo-data-exchange": "alexcojocaru/geo-data-exchange#v1.1.0",
|
||||||
"i18next": "19.9.2",
|
"i18next": "19.9.2",
|
||||||
|
|
|
||||||
|
|
@ -6111,6 +6111,10 @@ first-chunk-stream@^2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
readable-stream "^2.0.2"
|
readable-stream "^2.0.2"
|
||||||
|
|
||||||
|
fit-file-writer@tbsmark86/fit-file-writer#3eebe13:
|
||||||
|
version "0.2.0"
|
||||||
|
resolved "https://codeload.github.com/tbsmark86/fit-file-writer/tar.gz/3eebe137c3516c96fcfb437896676e805957ba7a"
|
||||||
|
|
||||||
flagged-respawn@^1.0.0:
|
flagged-respawn@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/flagged-respawn/-/flagged-respawn-1.0.1.tgz#e7de6f1279ddd9ca9aac8a5971d618606b3aab41"
|
resolved "https://registry.yarnpkg.com/flagged-respawn/-/flagged-respawn-1.0.1.tgz#e7de6f1279ddd9ca9aac8a5971d618606b3aab41"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue