a few changes to position history feature
This commit is contained in:
21
src/index.js
21
src/index.js
@ -431,10 +431,16 @@ app.get('/api/v1/nodes/:nodeId/traceroutes', async (req, res) => {
|
|||||||
app.get('/api/v1/nodes/:nodeId/position-history', async (req, res) => {
|
app.get('/api/v1/nodes/:nodeId/position-history', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const nodeId = parseInt(req.params.nodeId);
|
// defaults
|
||||||
const timeFrom = req.query.time_from ? parseInt(req.query.time_from) : new Date().getTime() - 3600 * 1000;
|
const nowInMilliseconds = new Date().getTime();
|
||||||
const timeTo = req.query.time_to ? parseInt(req.query.time_to) : new Date().getTime();
|
const oneHourAgoInMilliseconds = new Date().getTime() - (3600 * 1000);
|
||||||
|
|
||||||
|
// get request params
|
||||||
|
const nodeId = parseInt(req.params.nodeId);
|
||||||
|
const timeFrom = req.query.time_from ? parseInt(req.query.time_from) : oneHourAgoInMilliseconds;
|
||||||
|
const timeTo = req.query.time_to ? parseInt(req.query.time_to) : nowInMilliseconds;
|
||||||
|
|
||||||
|
// find node
|
||||||
const node = await prisma.node.findFirst({
|
const node = await prisma.node.findFirst({
|
||||||
where: {
|
where: {
|
||||||
node_id: nodeId,
|
node_id: nodeId,
|
||||||
@ -477,8 +483,7 @@ app.get('/api/v1/nodes/:nodeId/position-history', async (req, res) => {
|
|||||||
latitude: position.latitude,
|
latitude: position.latitude,
|
||||||
longitude: position.longitude,
|
longitude: position.longitude,
|
||||||
altitude: position.altitude,
|
altitude: position.altitude,
|
||||||
accuracy: position.accuracy,
|
created_at: position.created_at,
|
||||||
time: position.created_at,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -488,12 +493,12 @@ app.get('/api/v1/nodes/:nodeId/position-history', async (req, res) => {
|
|||||||
latitude: mapReport.latitude,
|
latitude: mapReport.latitude,
|
||||||
longitude: mapReport.longitude,
|
longitude: mapReport.longitude,
|
||||||
altitude: mapReport.altitude,
|
altitude: mapReport.altitude,
|
||||||
accuracy: mapReport.accuracy,
|
created_at: mapReport.created_at,
|
||||||
time: mapReport.created_at,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
positionHistory.sort((a, b) => a.time - b.time);
|
// sort oldest to newest
|
||||||
|
positionHistory.sort((a, b) => a.created_at - b.created_at);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
position_history: positionHistory,
|
position_history: positionHistory,
|
||||||
|
@ -66,6 +66,12 @@
|
|||||||
border: 1px solid white;
|
border: 1px solid white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-position-history {
|
||||||
|
background-color: #a855f7;
|
||||||
|
border-radius: 25px;
|
||||||
|
border: 1px solid white;
|
||||||
|
}
|
||||||
|
|
||||||
.waypoint-label {
|
.waypoint-label {
|
||||||
font-size: 26px;
|
font-size: 26px;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
@ -751,8 +757,8 @@
|
|||||||
<div @click.stop class="flex bg-gray-200 p-2 font-semibold">
|
<div @click.stop class="flex bg-gray-200 p-2 font-semibold">
|
||||||
<div class="my-auto">Position</div>
|
<div class="my-auto">Position</div>
|
||||||
<div class="ml-auto">
|
<div class="ml-auto">
|
||||||
<button @click="handleNodePositionHistory(selectedNode.node_id)" type="button" class="rounded bg-white px-2 py-1 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50">
|
<button @click="showNodePositionHistory(selectedNode.node_id)" type="button" class="rounded bg-white px-2 py-1 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50">
|
||||||
History
|
Show History
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1610,26 +1616,13 @@
|
|||||||
<div v-show="selectedNodeToShowPositionHistory != null" class="fixed left-0 right-0 bottom-0">
|
<div v-show="selectedNodeToShowPositionHistory != null" class="fixed left-0 right-0 bottom-0">
|
||||||
<div v-if="selectedNodeToShowPositionHistory != null" class="mx-auto w-screen max-w-md p-4">
|
<div v-if="selectedNodeToShowPositionHistory != null" class="mx-auto w-screen max-w-md p-4">
|
||||||
<div class="flex h-full flex-col bg-white shadow-xl rounded-xl border">
|
<div class="flex h-full flex-col bg-white shadow-xl rounded-xl border">
|
||||||
<div class="p-2">
|
<div>
|
||||||
<div class="flex items-start justify-between">
|
<div class="flex p-2 border-b">
|
||||||
<div>
|
<div class="my-auto mr-auto font-bold">{{ selectedNodeToShowPositionHistory.short_name }} Position History</div>
|
||||||
<h2 class="font-bold">{{ selectedNodeToShowPositionHistory.short_name }} Position history</h2>
|
<div class="my-auto ml-3">
|
||||||
<div>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<label for="datetime-from" class="text-sm pr-1">From:</label>
|
|
||||||
<input type="datetime-local" name="datetime-from" id="datetime-from" v-model="this.positionHistoryFromTimeDate" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-1">
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<label for="datetime-to" class="text-sm pr-1">To:</label>
|
|
||||||
<input type="datetime-local" name="datetime-to" id="datetime-to" v-model="this.positionHistoryToTimeDate" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-1">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button @click="loadNodePositionHistory(this.selectedNodeToShowPositionHistory.node_id)" class="bg-blue-500 text-white rounded-lg p-2 mt-2">Fetch</button>
|
|
||||||
</div>
|
|
||||||
<div class="my-auto ml-3 flex h-7 items-center">
|
|
||||||
<a href="javascript:void(0)" class="rounded-full" @click="dismissShowingNodePositionHistory">
|
<a href="javascript:void(0)" class="rounded-full" @click="dismissShowingNodePositionHistory">
|
||||||
<div class="bg-gray-100 hover:bg-gray-200 p-2 rounded-full">
|
<div class="bg-gray-100 hover:bg-gray-200 p-1 rounded-full">
|
||||||
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
<svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
<path d="M18 6l-12 12"></path>
|
<path d="M18 6l-12 12"></path>
|
||||||
<path d="M6 6l12 12"></path>
|
<path d="M6 6l12 12"></path>
|
||||||
@ -1638,6 +1631,21 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="p-2 space-y-1">
|
||||||
|
|
||||||
|
<!-- from -->
|
||||||
|
<div class="flex items-center">
|
||||||
|
<label class="text-sm pr-1 min-w-12 text-right">From:</label>
|
||||||
|
<input v-model="positionHistoryDateTimeFrom" @change="loadNodePositionHistory(selectedNodeToShowPositionHistory.node_id)" type="datetime-local" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-1">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- to -->
|
||||||
|
<div class="flex items-center">
|
||||||
|
<label class="text-sm pr-1 min-w-12 text-right">To:</label>
|
||||||
|
<input v-model="positionHistoryDateTimeTo" @change="loadNodePositionHistory(selectedNodeToShowPositionHistory.node_id)" type="datetime-local" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-1">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1812,8 +1820,9 @@
|
|||||||
selectedNodeMqttMetrics: [],
|
selectedNodeMqttMetrics: [],
|
||||||
selectedNodeTraceroutes: [],
|
selectedNodeTraceroutes: [],
|
||||||
|
|
||||||
positionHistoryFromTimeDate: new Date(new Date().getTime() - 3600 * 1000).toISOString().slice(0, 16),
|
// YYYY-MM-DDTHH:mm is the format expected by the datetime-local input type
|
||||||
positionHistoryToTimeDate: new Date().toISOString().slice(0, 16),
|
positionHistoryDateTimeFrom: moment().subtract(1, "hours").format('YYYY-MM-DDTHH:mm'),
|
||||||
|
positionHistoryDateTimeTo: moment().format('YYYY-MM-DDTHH:mm'),
|
||||||
selectedNodePositionHistory: [],
|
selectedNodePositionHistory: [],
|
||||||
selectedNodeToShowPositionHistory: null,
|
selectedNodeToShowPositionHistory: null,
|
||||||
selectedNodePositionHistoryMarkers: [],
|
selectedNodePositionHistoryMarkers: [],
|
||||||
@ -1847,7 +1856,7 @@
|
|||||||
this.loadNodePowerMetrics(node.node_id);
|
this.loadNodePowerMetrics(node.node_id);
|
||||||
this.loadNodeMqttMetrics(node.node_id);
|
this.loadNodeMqttMetrics(node.node_id);
|
||||||
this.loadNodeTraceroutes(node.node_id);
|
this.loadNodeTraceroutes(node.node_id);
|
||||||
this.loadNodePositionHistory(node.node_id);
|
//this.loadNodePositionHistory(node.node_id);
|
||||||
};
|
};
|
||||||
|
|
||||||
// handle node callback from outside of vue
|
// handle node callback from outside of vue
|
||||||
@ -1944,12 +1953,15 @@
|
|||||||
},
|
},
|
||||||
loadNodePositionHistory: function(nodeId) {
|
loadNodePositionHistory: function(nodeId) {
|
||||||
this.selectedNodePositionHistory = [];
|
this.selectedNodePositionHistory = [];
|
||||||
window.axios.get(`/api/v1/nodes/${nodeId}/position-history?time_from=${new Date(this.positionHistoryFromTimeDate).getTime()}&time_to=${new Date(this.positionHistoryToTimeDate).getTime()}`).then((response) => {
|
window.axios.get(`/api/v1/nodes/${nodeId}/position-history`, {
|
||||||
|
params: {
|
||||||
|
// parse from datetime-local format, and send as unix timestamp in milliseconds
|
||||||
|
time_from: moment(this.positionHistoryDateTimeFrom, "YYYY-MM-DDTHH:mm").format("x"),
|
||||||
|
time_to: moment(this.positionHistoryDateTimeTo, "YYYY-MM-DDTHH:mm").format("x"),
|
||||||
|
},
|
||||||
|
}).then((response) => {
|
||||||
this.selectedNodePositionHistory = response.data.position_history;
|
this.selectedNodePositionHistory = response.data.position_history;
|
||||||
|
if(this.selectedNodeToShowPositionHistory != null){
|
||||||
console.log(this.selectedNodePositionHistory);
|
|
||||||
|
|
||||||
if (this.selectedNodeToShowPositionHistory != null) {
|
|
||||||
clearAllPositionHistory();
|
clearAllPositionHistory();
|
||||||
onPositionHistoryUpdated(response.data.position_history);
|
onPositionHistoryUpdated(response.data.position_history);
|
||||||
}
|
}
|
||||||
@ -2570,10 +2582,21 @@
|
|||||||
getRegionFrequencyRange: function(regionName) {
|
getRegionFrequencyRange: function(regionName) {
|
||||||
return window.getRegionFrequencyRange(regionName);
|
return window.getRegionFrequencyRange(regionName);
|
||||||
},
|
},
|
||||||
handleNodePositionHistory: function(nodeId) {
|
showNodePositionHistory: function(nodeId) {
|
||||||
this.selectedNodeToShowPositionHistory = this.selectedNode
|
|
||||||
this.selectedNode = null
|
// find node
|
||||||
onPositionHistoryUpdated(this.selectedNodePositionHistory)
|
const node = findNodeById(nodeId);
|
||||||
|
if(!node){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// update ui
|
||||||
|
this.selectedNode = null;
|
||||||
|
this.selectedNodeToShowPositionHistory = node;
|
||||||
|
|
||||||
|
// load position history
|
||||||
|
this.loadNodePositionHistory(nodeId);
|
||||||
|
|
||||||
},
|
},
|
||||||
getShareLinkForNode: function(nodeId) {
|
getShareLinkForNode: function(nodeId) {
|
||||||
return window.location.origin + `/?node_id=${nodeId}`;
|
return window.location.origin + `/?node_id=${nodeId}`;
|
||||||
@ -2599,11 +2622,11 @@
|
|||||||
this.selectedNodeToShowNeighbours = null;
|
this.selectedNodeToShowNeighbours = null;
|
||||||
},
|
},
|
||||||
dismissShowingNodePositionHistory: function() {
|
dismissShowingNodePositionHistory: function() {
|
||||||
this.selectedNodePositionHistory = [],
|
this.selectedNodePositionHistory = [];
|
||||||
this.selectedNodeToShowPositionHistory = null
|
this.selectedNodeToShowPositionHistory = null;
|
||||||
this.selectedNodePositionHistoryMarkers = [],
|
this.selectedNodePositionHistoryMarkers = [];
|
||||||
this.selectedNodePositionHistoryPolyLines = [],
|
this.selectedNodePositionHistoryPolyLines = [];
|
||||||
cleanUpPositionHistory()
|
cleanUpPositionHistory();
|
||||||
},
|
},
|
||||||
formatUptimeSeconds: function(secondsToFormat) {
|
formatUptimeSeconds: function(secondsToFormat) {
|
||||||
secondsToFormat = Number(secondsToFormat);
|
secondsToFormat = Number(secondsToFormat);
|
||||||
@ -2792,6 +2815,11 @@
|
|||||||
iconSize: [16, 16], // increase from 12px to 16px to make hover easier
|
iconSize: [16, 16], // increase from 12px to 16px to make hover easier
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var iconPositionHistory = L.divIcon({
|
||||||
|
className: 'icon-position-history',
|
||||||
|
iconSize: [16, 16], // increase from 12px to 16px to make hover easier
|
||||||
|
});
|
||||||
|
|
||||||
// create legend
|
// create legend
|
||||||
var legendLayerGroup = new L.LayerGroup();
|
var legendLayerGroup = new L.LayerGroup();
|
||||||
var legend = L.control({position: 'bottomleft'});
|
var legend = L.control({position: 'bottomleft'});
|
||||||
@ -3608,14 +3636,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onPositionHistoryUpdated(updatedPositionHistories) {
|
function onPositionHistoryUpdated(updatedPositionHistories) {
|
||||||
let positionHistoryMarkers = [];
|
|
||||||
let positionHistoryLines = [];
|
|
||||||
let positionHistoryLinesCords = [];
|
let positionHistoryLinesCords = [];
|
||||||
|
|
||||||
// add nodes
|
// add nodes
|
||||||
for(var positionHistory of updatedPositionHistories) {
|
for(var positionHistory of updatedPositionHistories) {
|
||||||
|
|
||||||
// skip nodes without position
|
// skip position history without position
|
||||||
if(!positionHistory.latitude || !positionHistory.longitude){
|
if(!positionHistory.latitude || !positionHistory.longitude){
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -3623,10 +3650,8 @@
|
|||||||
// fix lat long
|
// fix lat long
|
||||||
positionHistory.latitude = positionHistory.latitude / 10000000;
|
positionHistory.latitude = positionHistory.latitude / 10000000;
|
||||||
positionHistory.longitude = positionHistory.longitude / 10000000;
|
positionHistory.longitude = positionHistory.longitude / 10000000;
|
||||||
|
|
||||||
|
|
||||||
var hasLocation = isValidLatLng(positionHistory.latitude, positionHistory.longitude);
|
var hasLocation = isValidLatLng(positionHistory.latitude, positionHistory.longitude);
|
||||||
|
|
||||||
if(hasLocation){
|
if(hasLocation){
|
||||||
|
|
||||||
// wrap longitude for shortest path, everything to left of australia should be shown on the right
|
// wrap longitude for shortest path, everything to left of australia should be shown on the right
|
||||||
@ -3635,25 +3660,15 @@
|
|||||||
longitude += 360;
|
longitude += 360;
|
||||||
}
|
}
|
||||||
|
|
||||||
positionHistoryLinesCords.push([positionHistory.latitude, longitude])
|
positionHistoryLinesCords.push([positionHistory.latitude, longitude]);
|
||||||
|
|
||||||
// determine emoji to show as marker icon
|
let tooltip = "";
|
||||||
const emoji = 128205;
|
tooltip += `<b>${moment(new Date(positionHistory.created_at)).format("DD/MM/YYYY hh:mm A")}</b></br>`;
|
||||||
const emojiText = String.fromCodePoint(emoji)
|
tooltip += `Position: ${positionHistory.latitude}, ${positionHistory.longitude}</br>`;
|
||||||
|
|
||||||
let tooltip = ""
|
|
||||||
|
|
||||||
tooltip += `Position: ${positionHistory.latitude}, ${positionHistory.longitude}</br>`
|
|
||||||
tooltip += `Heard by: </br>`
|
|
||||||
tooltip += `Timestamp: ${new Date(positionHistory.time).toLocaleString()}</br>`
|
|
||||||
|
|
||||||
// create position history marker
|
// create position history marker
|
||||||
const marker = L.marker([positionHistory.latitude, longitude],{
|
const marker = L.marker([positionHistory.latitude, longitude],{
|
||||||
icon: L.divIcon({
|
icon: iconPositionHistory,
|
||||||
className: 'waypoint-label',
|
|
||||||
iconSize: [26, 26], // increase from 12px to 26px
|
|
||||||
html: emojiText,
|
|
||||||
}),
|
|
||||||
}).bindPopup(tooltip).on('click', function(event) {
|
}).bindPopup(tooltip).on('click', function(event) {
|
||||||
// close tooltip on click to prevent tooltip and popup showing at same time
|
// close tooltip on click to prevent tooltip and popup showing at same time
|
||||||
event.target.closeTooltip();
|
event.target.closeTooltip();
|
||||||
@ -3662,15 +3677,12 @@
|
|||||||
// add marker to position history layer group
|
// add marker to position history layer group
|
||||||
marker.addTo(nodePositionHistoryLayerGroup);
|
marker.addTo(nodePositionHistoryLayerGroup);
|
||||||
|
|
||||||
positionHistoryMarkers.push(marker)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const line = L.polyline(positionHistoryLinesCords).addTo(nodePositionHistoryLayerGroup);
|
// show lines between position history markers
|
||||||
|
L.polyline(positionHistoryLinesCords).addTo(nodePositionHistoryLayerGroup);
|
||||||
positionHistoryLines.push(line)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user