Watermark Settings
@@ -3112,6 +3164,7 @@
Advanced User Settings
+
diff --git a/js/flightlog.js b/js/flightlog.js
index 43423a03..8eef811a 100644
--- a/js/flightlog.js
+++ b/js/flightlog.js
@@ -1059,6 +1059,10 @@ function FlightLog(logData) {
return true;
};
+
+ this.hasGpsData = function() {
+ return this.getStats()?.frame?.G ? true : false;;
+ };
}
FlightLog.prototype.accRawToGs = function(value) {
diff --git a/js/graph_map.js b/js/graph_map.js
new file mode 100644
index 00000000..0fe7e047
--- /dev/null
+++ b/js/graph_map.js
@@ -0,0 +1,409 @@
+"use strict";
+
+function MapGrapher() {
+ let userSettings,
+ myMap,
+ currentLogStartDateTime,
+ currentTime,
+ craftPosition,
+ groundCourse,
+ homePosition,
+ craftMarker,
+ homeMarker,
+ trailLayers = new Map(),
+ previousLogIndex,
+ latIndexAtFrame,
+ lngIndexAtFrame,
+ altitudeIndexAtFrame,
+ groundCourseIndexAtFrame,
+ flightLog;
+
+ const coordinateDivider = 10000000;
+ const altitudeDivider = 10;
+ const grounCourseDivider = 10;
+
+ const mapOptions = {
+ center: [0, 0],
+ zoom: 1,
+ };
+
+ const craftIcon = L.icon({
+ iconUrl: "../images/markers/craft.png",
+ iconSize: [30, 30],
+ iconAnchor: [15, 15],
+ className: "icon",
+ });
+
+ const homeIcon = L.icon({
+ iconUrl: "../images/markers/home.png",
+ iconSize: [40, 40],
+ iconAnchor: [20, 35],
+ className: "icon",
+ });
+
+ const polylineOptions = {
+ color: "#2db0e3",
+ opacity: 0.8,
+ smoothFactor: 1,
+ };
+
+ // flight trail colors
+ const colorTrailGradient = [
+ { color: "#00ffe0bf" },
+ { color: "#00ff8cbf" },
+ { color: "#00ff02bf" },
+ { color: "#75ff00bf" },
+ { color: "#e5ff00bf" },
+ { color: "#ffb100bf" },
+ { color: "#ff4c00bf" },
+ { color: "#ff1414" },
+ ];
+
+ // debug circles can be used to aligh icons at the correct coordinates
+ const debugCircle = false;
+ const debugCircleOptions = {
+ color: "red",
+ fillColor: "red",
+ fillOpacity: 0.8,
+ radius: 1,
+ };
+
+ this.initialize = function () {
+ if (myMap) {
+ return;
+ }
+
+ myMap = L.map("mapContainer", mapOptions);
+
+ L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
+ maxZoom: 19,
+ minZoom: 1,
+ attribution:
+ '©
OpenStreetMap',
+ }).addTo(myMap);
+ };
+
+ this.reset = function () {
+ if (!myMap) {
+ return;
+ }
+ this.clearMap(previousLogIndex);
+ previousLogIndex = null;
+ currentTime = null;
+ craftPosition = null;
+ groundCourse = null;
+ homePosition = null;
+ craftMarker = null;
+ homeMarker = null;
+ trailLayers = new Map();
+ previousLogIndex = null;
+ latIndexAtFrame = null;
+ lngIndexAtFrame = null;
+ altitudeIndexAtFrame = null;
+ groundCourseIndexAtFrame = null;
+ myMap.setView(mapOptions.center, mapOptions.zoom);
+ };
+
+ this.setFlightLog = function (newFlightLog) {
+ flightLog = newFlightLog;
+
+ const newLogStartDateTime = flightLog.getSysConfig()["Log start datetime"];
+ if (currentLogStartDateTime != newLogStartDateTime) {
+ this.reset();
+ currentLogStartDateTime = newLogStartDateTime;
+ }
+
+ const logIndex = flightLog.getLogIndex();
+
+ // if this log is already proccesed its skipped
+ if (trailLayers.has(logIndex)) {
+ return;
+ }
+
+ this.setFlightLogIndexs();
+
+ let { latlngs, maxAlt, minAlt } = this.getPolylinesData();
+
+ const polyline = L.polyline(latlngs, polylineOptions);
+
+ const polylineC = this.createAltitudeColoredPolyline(
+ latlngs,
+ maxAlt,
+ minAlt
+ );
+
+ trailLayers.set(logIndex, { polyline, polylineC });
+
+ if (latlngs.length > 0) {
+ homePosition = this.getHomeCoordinatesFromFlightLog(flightLog);
+ }
+ };
+
+ this.setFlightLogIndexs = function () {
+ latIndexAtFrame = flightLog.getMainFieldIndexByName("GPS_coord[0]");
+ lngIndexAtFrame = flightLog.getMainFieldIndexByName("GPS_coord[1]");
+ altitudeIndexAtFrame = flightLog.getMainFieldIndexByName("GPS_altitude");
+ groundCourseIndexAtFrame =
+ flightLog.getMainFieldIndexByName("GPS_ground_course");
+ };
+
+ this.getPolylinesData = function () {
+ let latlngs = [];
+ let maxAlt = Number.MIN_VALUE;
+ let minAlt = Number.MAX_VALUE;
+
+ const chunks = flightLog.getChunksInTimeRange(
+ flightLog.getMinTime(),
+ flightLog.getMaxTime()
+ );
+
+ for (const chunk of chunks) {
+ for (const fi in chunk.frames) {
+ const frame = chunk.frames[fi];
+ const coordinates = this.getCoordinatesFromFrame(
+ frame,
+ latIndexAtFrame,
+ lngIndexAtFrame,
+ altitudeIndexAtFrame
+ );
+
+ // if there are no coordinates the frame is skipped
+ if (!coordinates) {
+ continue;
+ }
+
+ // Altitude max and min values can be obtained from the stats but fixing the index at 4 doesn't seem safe
+ // const maxAlt = flightLog.getStats().frame.G.field[4].max / altitudeDivider;
+ // const minAlt = flightLog.getStats().frame.G.field[4].min / altitudeDivider;
+ maxAlt = coordinates.alt > maxAlt ? coordinates.alt : maxAlt;
+ minAlt = coordinates.alt < minAlt ? coordinates.alt : minAlt;
+
+ // 1/4 of the dots is enough to draw the line
+ if (fi % 4 == 0) {
+ latlngs.push(coordinates);
+ }
+ }
+ }
+ return { latlngs, maxAlt, minAlt };
+ };
+
+ this.createAltitudeColoredPolyline = function (latlngs, maxAlt, minAlt) {
+ const divider = colorTrailGradient.length - 1;
+
+ const delta = maxAlt - minAlt;
+
+ const thresholdIncrement = delta / divider;
+
+ let altThresholds = [];
+ let threshold = minAlt;
+ for (let i = 0; i < divider; i++) {
+ //amount of colors - min and max that are set
+ threshold += thresholdIncrement;
+ altThresholds.push(threshold);
+ }
+
+ return L.multiOptionsPolyline(latlngs, {
+ multiOptions: {
+ optionIdxFn: function (latLng) {
+ for (let i = 0; i < altThresholds.length; i++) {
+ if (latLng.alt <= altThresholds[i]) {
+ return i;
+ }
+ }
+ return altThresholds.length;
+ },
+ options: colorTrailGradient,
+ },
+ weight: 3,
+ lineCap: "butt",
+ opacity: 1,
+ smoothFactor: 1,
+ });
+ };
+
+ this.updateCurrentPosition = function () {
+ try {
+ const frame = flightLog.getCurrentFrameAtTime(currentTime);
+ craftPosition = this.getCoordinatesFromFrame(
+ frame.current,
+ latIndexAtFrame,
+ lngIndexAtFrame,
+ altitudeIndexAtFrame
+ );
+ groundCourse = this.getGroundCourseFromFrame(
+ frame.current,
+ groundCourseIndexAtFrame
+ );
+ } catch (e) {}
+ };
+
+ this.setUserSettings = function (newUserSettings) {
+ userSettings = newUserSettings;
+ };
+
+ this.redrawAll = function () {
+ if (trailLayers.size <= 0 || !myMap) {
+ return;
+ }
+
+ this.redrawFlightTrail();
+ this.redrawHomeMarker();
+ this.redrawCraftMarker();
+ };
+
+ this.redrawFlightTrail = function () {
+ // If flightLog has changed redraw flight trail
+ const currentLogIndex = flightLog.getLogIndex();
+ if (previousLogIndex != currentLogIndex) {
+ this.clearMap(previousLogIndex);
+ if (trailLayers.has(currentLogIndex)) {
+ const polyline = userSettings.mapTrailAltitudeColored
+ ? trailLayers.get(currentLogIndex).polylineC
+ : trailLayers.get(currentLogIndex).polyline;
+ polyline.addTo(myMap);
+ myMap.fitBounds(polyline.getBounds());
+ }
+
+ previousLogIndex = currentLogIndex;
+ }
+ };
+
+ this.redrawHomeMarker = function () {
+ if (homePosition) {
+ if (homeMarker) {
+ homeMarker.icon.setLatLng(homePosition).addTo(myMap);
+
+ // debug circle
+ if (debugCircle) {
+ homeMarker.circle.setLatLng(homePosition).addTo(myMap);
+ }
+ } else {
+ homeMarker = {};
+
+ homeMarker.icon = L.marker(homePosition, {
+ icon: homeIcon,
+ }).addTo(myMap);
+
+ // debug circle
+ if (debugCircle) {
+ homeMarker.circle = L.circle(homePosition, debugCircleOptions).addTo(
+ myMap
+ );
+ }
+ }
+ }
+ };
+
+ this.redrawCraftMarker = function () {
+ if (craftPosition) {
+ if (craftMarker) {
+ craftMarker.icon.setLatLng(craftPosition);
+ craftMarker.icon.setRotationAngle(groundCourse).addTo(myMap);
+ // debug circle
+ if (debugCircle) {
+ homeMarker.circle.setLatLng(craftPosition).addTo(myMap);
+ }
+ } else {
+ craftMarker = {};
+ craftMarker.icon = L.rotatedMarker(craftPosition, {
+ icon: craftIcon,
+ rotationAngle: groundCourse,
+ rotationOrigin: "center center",
+ }).addTo(myMap);
+
+ // debug circle
+ if (debugCircle) {
+ craftMarker.circle = L.circle(
+ craftPosition,
+ debugCircleOptions
+ ).addTo(myMap);
+ }
+ }
+ }
+ };
+
+ this.clearMap = function (trailIndex) {
+ this.clearMapFlightTrails(trailIndex);
+ this.clearMapMarkers();
+ };
+
+ this.clearMapFlightTrails = function (trailIndex) {
+ if (trailLayers.has(trailIndex)) {
+ const p = trailLayers.get(trailIndex).polyline;
+ const pc = trailLayers.get(trailIndex).polylineC;
+ if (p) {
+ myMap.removeLayer(p);
+ }
+ if (pc) {
+ myMap.removeLayer(pc);
+ }
+ }
+ };
+
+ this.clearMapMarkers = function () {
+ if (homeMarker) {
+ if (myMap.hasLayer(homeMarker.icon)) {
+ myMap.removeLayer(homeMarker.icon);
+ }
+ if (debugCircle && myMap.hasLayer(homeMarker.circle)) {
+ myMap.removeLayer(homeMarker.circle);
+ }
+ }
+ if (craftMarker) {
+ if (myMap.hasLayer(craftMarker.icon)) {
+ myMap.removeLayer(craftMarker.icon);
+ }
+ if (debugCircle && myMap.hasLayer(craftMarker.circle)) {
+ myMap.removeLayer(craftMarker.circle);
+ }
+ }
+ };
+
+ this.resize = function (width, height) {
+ if (!userSettings) {
+ return;
+ }
+ const containerstyle = {
+ height: (height * parseInt(userSettings.map.size)) / 100.0,
+ width: (width * parseInt(userSettings.map.size)) / 100.0,
+ left: (width * parseInt(userSettings.map.left)) / 100.0,
+ top: (height * parseInt(userSettings.map.top)) / 100.0,
+ };
+ $("#mapContainer").css(containerstyle);
+ };
+
+ this.getCoordinatesFromFrame = function (
+ frame,
+ latIndex,
+ lngIndex,
+ altitudeIndex
+ ) {
+ const lat = frame[latIndex];
+ const lng = frame[lngIndex];
+ const alt = frame[altitudeIndex];
+
+ return typeof lat == "number" || typeof lng == "number"
+ ? L.latLng(
+ lat / coordinateDivider,
+ lng / coordinateDivider,
+ alt / altitudeDivider
+ )
+ : null;
+ };
+
+ this.getGroundCourseFromFrame = function (frame, groundCourseIndex) {
+ const gc = frame[groundCourseIndex];
+ return typeof gc == "number" ? gc / grounCourseDivider : 0;
+ };
+
+ this.getHomeCoordinatesFromFlightLog = function (flightLog) {
+ const home = flightLog.getStats().frame.H.field;
+ return [home[0].min / coordinateDivider, home[1].min / coordinateDivider];
+ };
+
+ this.setCurrentTime = function (newTime) {
+ currentTime = newTime;
+ this.updateCurrentPosition();
+ this.redrawAll();
+ };
+}
diff --git a/js/main.js b/js/main.js
index e6ca375d..b7a1793b 100644
--- a/js/main.js
+++ b/js/main.js
@@ -58,7 +58,6 @@ function BlackboxLogViewer() {
// JSON graph configuration:
graphConfig = {},
-
offsetCache = [], // Storage for the offset cache (last 20 files)
currentOffsetCache = {log:null, index:null, video:null, offset:null},
@@ -77,7 +76,7 @@ function BlackboxLogViewer() {
fieldPresenter = FlightLogFieldPresenter,
hasVideo = false, hasLog = false, hasMarker = false, // add measure feature
- hasTable = true, hasAnalyser, hasAnalyserFullscreen,
+ hasTable = true, hasAnalyser, hasMap, hasAnalyserFullscreen,
hasAnalyserSticks = false, viewVideo = true, hasTableOverlay = false, hadTable,
hasConfig = false, hasConfigOverlay = false,
@@ -112,9 +111,12 @@ function BlackboxLogViewer() {
animationFrameIsQueued = false,
playbackRate = PLAYBACK_DEFAULT_RATE,
-
+
graphZoom = GRAPH_DEFAULT_ZOOM,
- lastGraphZoom = GRAPH_DEFAULT_ZOOM; // QuickZoom function.
+ lastGraphZoom = GRAPH_DEFAULT_ZOOM, // QuickZoom function.
+
+ mapGrapher = new MapGrapher();
+
function createNewBlackboxWindow(fileToOpen) {
@@ -269,6 +271,10 @@ function BlackboxLogViewer() {
seekBar.setCurrentTime(currentBlackboxTime);
seekBar.setWindow(graph.getWindowWidthTime());
+ if (flightLog.hasGpsData()) {
+ mapGrapher.setCurrentTime(currentBlackboxTime);
+ }
+
updateValuesChartRateLimited();
if (graphState == GRAPH_STATE_PLAY) {
@@ -300,6 +306,9 @@ function BlackboxLogViewer() {
if (graph) {
graph.resize(width, height);
seekBar.resize(canvas.offsetWidth, 50);
+ if(flightLog.hasGpsData()) {
+ mapGrapher.resize(width, height);
+ }
invalidateGraph();
}
@@ -408,6 +417,14 @@ function BlackboxLogViewer() {
seekBar.setActivity(activity.times, activity.avgThrottle, activity.hasEvent);
seekBar.repaint();
+
+ // Add flightLog to map
+ html.toggleClass("has-gps", flightLog.hasGpsData());
+ if(flightLog.hasGpsData()) {
+ mapGrapher.setUserSettings(userSettings);
+ mapGrapher.setFlightLog(flightLog);
+ }
+
}
function setGraphState(newState) {
@@ -1110,6 +1127,15 @@ function BlackboxLogViewer() {
invalidateGraph();
});
+ $(".view-map").click(function() {
+ hasMap = !hasMap;
+ html.toggleClass("has-map", hasMap);
+ prefs.set('hasMap', hasMap);
+ if(flightLog.hasGpsData()) {
+ mapGrapher.initialize(userSettings);
+ }
+ });
+
$(".view-analyser-fullscreen").click(function() {
if(hasAnalyser) {
hasAnalyserFullscreen = !hasAnalyserFullscreen;
@@ -1370,6 +1396,9 @@ function BlackboxLogViewer() {
graph.refreshOptions(newSettings);
graph.refreshLogo();
graph.initializeCraftModel();
+ if(flightLog.hasGpsData()) {
+ mapGrapher.setUserSettings(newSettings);
+ }
updateCanvasSize();
}
diff --git a/js/user_settings_dialog.js b/js/user_settings_dialog.js
index dffb0765..9f3aea00 100644
--- a/js/user_settings_dialog.js
+++ b/js/user_settings_dialog.js
@@ -66,7 +66,7 @@ function UserSettingsDialog(dialog, onLoad, onSave) {
overdrawSpectrumType: 0, // By default, show all filters
craft : {
left : '15%', // position from left (as a percentage of width)
- top : '25%', // position from top (as a percentage of height)
+ top : '48%', // position from top (as a percentage of height)
size : '40%' // size (as a percentage of width)
},
sticks : {
@@ -75,10 +75,15 @@ function UserSettingsDialog(dialog, onLoad, onSave) {
size : '30%' // size (as a percentage of width)
},
analyser : {
- left : '5%', // position from left (as a percentage of width)
+ left : '2%', // position from left (as a percentage of width)
top : '60%', // position from top (as a percentage of height)
size : '35%' // size (as a percentage of width)
},
+ map : {
+ left : '2%', // position from left (as a percentage of width)
+ top : '5%', // position from top (as a percentage of height)
+ size : '35%' // size (as a percentage of width)
+ },
watermark : {
left : '3%', // position from left (as a percentage of width)
top : '90%', // position from top (as a percentage of height)
@@ -125,9 +130,12 @@ function UserSettingsDialog(dialog, onLoad, onSave) {
craft: {top: $('.craft-settings input[name="craft-top"]').val() + '%',
left: $('.craft-settings input[name="craft-left"]').val() + '%',
size: $('.craft-settings input[name="craft-size"]').val() + '%', },
- analyser: {top: $('.analyser-settings input[name="analyser-top"]').val() + '%',
+ analyser: {top: $('.analyser-settings input[name="analyser-top"]').val() + '%',
left: $('.analyser-settings input[name="analyser-left"]').val() + '%',
size: $('.analyser-settings input[name="analyser-size"]').val() + '%', },
+ map: {top: $('.map-settings input[name="map-top"]').val() + '%',
+ left: $('.map-settings input[name="map-left"]').val() + '%',
+ size: $('.map-settings input[name="map-size"]').val() + '%', },
watermark: {top: $('.watermark-settings input[name="watermark-top"]').val() + '%',
left: $('.watermark-settings input[name="watermark-left"]').val() + '%',
size: $('.watermark-settings input[name="watermark-size"]').val() + '%',
@@ -298,6 +306,10 @@ function UserSettingsDialog(dialog, onLoad, onSave) {
currentSettings.analyserHanning = $(this).is(":checked");
});
+ $(".map-trail-altitude-colored").click(function() {
+ currentSettings.mapTrailAltitudeColored = $(this).is(":checked");
+ });
+
$(".legend-units").click(function() {
currentSettings.legendUnits = $(this).is(":checked");
});
@@ -368,6 +380,11 @@ function UserSettingsDialog(dialog, onLoad, onSave) {
$(".analyser-hanning").prop('checked', currentSettings.analyserHanning);
}
+ if(currentSettings.mapTrailAltitudeColored!=null) {
+ // set the toggle switch
+ $(".map-trail-altitude-colored").prop('checked', currentSettings.mapTrailAltitudeColored);
+ }
+
if(currentSettings.legendUnits!=null) {
// set the toggle switch
$(".legend-units").prop('checked', currentSettings.legendUnits);
@@ -390,6 +407,9 @@ function UserSettingsDialog(dialog, onLoad, onSave) {
$('.analyser-settings input[name="analyser-top"]').val(parseInt(currentSettings.analyser.top));
$('.analyser-settings input[name="analyser-left"]').val(parseInt(currentSettings.analyser.left));
$('.analyser-settings input[name="analyser-size"]').val(parseInt(currentSettings.analyser.size));
+ $('.map-settings input[name="map-top"]').val(parseInt(currentSettings.map.top));
+ $('.map-settings input[name="map-left"]').val(parseInt(currentSettings.map.left));
+ $('.map-settings input[name="map-size"]').val(parseInt(currentSettings.map.size));
if(currentSettings.drawWatermark!=null) {
// set the toggle switch
diff --git a/package.json b/package.json
index 82620a8c..b988ad53 100644
--- a/package.json
+++ b/package.json
@@ -25,8 +25,11 @@
"author": "The Betaflight open source project.",
"license": "GPL-3.0",
"dependencies": {
+ "Leaflet.MultiOptionsPolyline": "hgoebl/Leaflet.MultiOptionsPolyline",
"bootstrap": "~3.4.1",
"html2canvas": "^1.0.0-rc.5",
+ "leaflet": "^1.9.3",
+ "leaflet-marker-rotation": "^0.4.0",
"lodash": "^4.17.21"
},
"devDependencies": {
diff --git a/yarn.lock b/yarn.lock
index 1e77fdad..c4da4cc6 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -72,6 +72,10 @@
dependencies:
"@types/node" "*"
+Leaflet.MultiOptionsPolyline@hgoebl/Leaflet.MultiOptionsPolyline:
+ version "1.0.0"
+ resolved "https://codeload.github.com/hgoebl/Leaflet.MultiOptionsPolyline/tar.gz/ebd929f3f3c9f0eca9ef13d46ff04533aa1d3116"
+
aggregate-error@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a"
@@ -2494,6 +2498,16 @@ lead@^1.0.0:
dependencies:
flush-write-stream "^1.0.2"
+leaflet-marker-rotation@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/leaflet-marker-rotation/-/leaflet-marker-rotation-0.4.0.tgz#c375db65e8e8f0ef7449c5d7ffb9d96d475e41de"
+ integrity sha512-7I+l/Ky3mP7LQ+nfVA+Gzjl5NOb02mph3v9SD2dPoCEQ/vV3zuZp+7Sewm2Pgk2zH9Aa4+mPqNTSSSLe3D7lmg==
+
+leaflet@^1.9.3:
+ version "1.9.3"
+ resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.9.3.tgz#52ec436954964e2d3d39e0d433da4b2500d74414"
+ integrity sha512-iB2cR9vAkDOu5l3HAay2obcUHZ7xwUBBjph8+PGtmW/2lYhbLizWtG7nTeYht36WfOslixQF9D/uSIzhZgGMfQ==
+
liftoff@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/liftoff/-/liftoff-3.1.0.tgz#c9ba6081f908670607ee79062d700df062c52ed3"