add webapp, move frontend to webapp folder
This commit is contained in:
250
webapp/frontend/src/utils.js
Normal file
250
webapp/frontend/src/utils.js
Normal file
@ -0,0 +1,250 @@
|
||||
import { state } from './store.js';
|
||||
import { temperatureFormat, nodesDisconnectedAge, BASE_PATH } from './config.js';
|
||||
import moment from 'moment';
|
||||
|
||||
export const timeSpans = {
|
||||
'1d': {name: '1 Day', amount: 86400},
|
||||
'3d': {name: '3 Days', amount: 259200},
|
||||
'7d': {name: '1 Week', amount: 604800},
|
||||
'14d': {name: '2 Weeks', amount: 1209600},
|
||||
'30d': {name: '1 Month', amount: 2592000},
|
||||
'90d': {name: '90 Days', amount: 7776000},
|
||||
};
|
||||
|
||||
export function getTimeSpans() {
|
||||
return timeSpans;
|
||||
};
|
||||
|
||||
export function getTimeSpan(value) {
|
||||
return timeSpans[value];
|
||||
};
|
||||
|
||||
// convert node id to a hex colour
|
||||
export function getNodeColor(nodeId) {
|
||||
return "#" + (nodeId & 0x00FFFFFF).toString(16).padStart(6, '0');
|
||||
};
|
||||
|
||||
export function getNodeTextColor(nodeId) {
|
||||
// extract rgb components
|
||||
const r = (nodeId & 0xFF0000) >> 16;
|
||||
const g = (nodeId & 0x00FF00) >> 8;
|
||||
const b = nodeId & 0x0000FF;
|
||||
|
||||
// calculate brightness
|
||||
const brightness = ((r * 0.299) + (g * 0.587) + (b * 0.114)) / 255;
|
||||
|
||||
// determine text color based on brightness
|
||||
return brightness > 0.5 ? "#000000" : "#FFFFFF";
|
||||
};
|
||||
|
||||
export function getRegionFrequencyRange(regionName) {
|
||||
// determine lora frequency range based on region_name
|
||||
// https://github.com/meshtastic/firmware/blob/a4c22321fca6fc8da7bab157c3812055603512ba/src/mesh/RadioInterface.cpp#L21
|
||||
const regionNameToLoraFrequencyRange = {
|
||||
"US": "902-928 MHz",
|
||||
"EU_433": "433-434 MHz",
|
||||
"EU_868": "869.4-869.65 MHz",
|
||||
"CN": "470-510 MHz",
|
||||
"JP": "920.8-927.8 MHz",
|
||||
"ANZ": "915-928 MHz",
|
||||
"RU": "868.7-869.2 MHz",
|
||||
"KR": "920-923 MHz",
|
||||
"TW": "920-925 MHz",
|
||||
"IN": "865-867 MHz",
|
||||
"NZ_865": "864-868 MHz",
|
||||
"TH": "920-925 MHz",
|
||||
"UA_433": "433-434.7 MHz",
|
||||
"UA_868": "868-868.6 MHz",
|
||||
"MY_433": "433-435 MHz",
|
||||
"MY_919": "919-924 MHz",
|
||||
"SG_923": "917-925 MHz",
|
||||
"LORA_24": "2.4-2.4835 GHz",
|
||||
"UNSET": "902-928 MHz",
|
||||
}
|
||||
return regionNameToLoraFrequencyRange[regionName] ?? null;
|
||||
};
|
||||
|
||||
export function getShareLinkForNode(nodeId) {
|
||||
return window.location.origin + `/?node_id=${nodeId}`;
|
||||
};
|
||||
|
||||
export function copyShareLinkForNode(nodeId) {
|
||||
// make sure copy to clipboard is supported
|
||||
if (!navigator.clipboard || !navigator.clipboard.writeText) {
|
||||
alert("Clipboard not supported. Site must be served via https on iOS.");
|
||||
return;
|
||||
}
|
||||
// copy share link to clipboard
|
||||
const url = getShareLinkForNode(nodeId);
|
||||
navigator.clipboard.writeText(url);
|
||||
// tell user we copied it
|
||||
alert("Link copied to clipboard!");
|
||||
};
|
||||
|
||||
export function getColorForSnr(snr) {
|
||||
if(snr >= 0) return "#16a34a"; // good
|
||||
if(snr < 0) return "#dc2626"; // bad
|
||||
};
|
||||
|
||||
export function getPositionPrecisionInMeters(positionPrecision) {
|
||||
switch (positionPrecision){
|
||||
case 2: return 5976446;
|
||||
case 3: return 2988223;
|
||||
case 4: return 1494111;
|
||||
case 5: return 747055;
|
||||
case 6: return 373527;
|
||||
case 7: return 186763;
|
||||
case 8: return 93381;
|
||||
case 9: return 46690;
|
||||
case 10: return 23345;
|
||||
case 11: return 11672; // Android LOW_PRECISION
|
||||
case 12: return 5836;
|
||||
case 13: return 2918;
|
||||
case 14: return 1459;
|
||||
case 15: return 729;
|
||||
case 16: return 364; // Android MED_PRECISION
|
||||
case 17: return 182;
|
||||
case 18: return 91;
|
||||
case 19: return 45;
|
||||
case 20: return 22;
|
||||
case 21: return 11;
|
||||
case 22: return 5;
|
||||
case 23: return 2;
|
||||
case 24: return 1;
|
||||
case 32: return 0; // Android HIGH_PRECISION
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export function getTerrainProfileImage(node1, node2) {
|
||||
// line colour between nodes
|
||||
const lineColour = "0000FF"; // blue
|
||||
|
||||
// node 1 (left side of image)
|
||||
const node1MarkerColour = "0000FF"; // blue
|
||||
const node1Latitude = node1.latitude;
|
||||
const node1Longitude = node1.longitude;
|
||||
const node1ElevationMSL = ""; // node1.altitude ?? "";
|
||||
|
||||
// node 2 (right side of image)
|
||||
const node2MarkerColour = "0000FF"; // blue
|
||||
const node2Latitude = node2.latitude;
|
||||
const node2Longitude = node2.longitude;
|
||||
const node2ElevationMSL = ""; // node2.altitude ?? "";
|
||||
|
||||
// generate terrain profile image url
|
||||
return "https://heywhatsthat.com/bin/profile-0904.cgi?" + new URLSearchParams({
|
||||
src: "meshtastic.liamcottle.net",
|
||||
axes: 1, // include grid lines and a scale
|
||||
metric: 1, // show metric units
|
||||
curvature: 0, // don't include the curvature of the earth in the graphic
|
||||
width: 500,
|
||||
height: 200,
|
||||
pt0: `${node1Latitude},${node1Longitude},${lineColour},${node1ElevationMSL},${node1MarkerColour}`,
|
||||
pt1: `${node2Latitude},${node2Longitude},${lineColour},${node2ElevationMSL},${node2MarkerColour}`,
|
||||
}).toString();
|
||||
};
|
||||
|
||||
export function formatPositionPrecision(positionPrecision) {
|
||||
// get position precision in meters
|
||||
const positionPrecisionInMeters = getPositionPrecisionInMeters(positionPrecision);
|
||||
if (positionPrecisionInMeters == null){
|
||||
return "?";
|
||||
}
|
||||
|
||||
// format kilometers
|
||||
if (positionPrecisionInMeters > 1000){
|
||||
const positionPrecisionInKilometers = Math.ceil(positionPrecisionInMeters / 1000);
|
||||
return `±${positionPrecisionInKilometers}km`;
|
||||
}
|
||||
|
||||
// format meters
|
||||
return `±${positionPrecisionInMeters}m`;
|
||||
};
|
||||
|
||||
// escape strings for tooltips etc, to prevent html/script injection
|
||||
// not used in vuejs, as that auto escapes
|
||||
export function escapeString(string) {
|
||||
return string.replace(/</g, "<").replace(/>/g, ">");
|
||||
};
|
||||
|
||||
export function isMobile() {
|
||||
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
};
|
||||
|
||||
// returns true if the element or one of its parents has the class classname
|
||||
export function elementOrAnyAncestorHasClass(element, className) {
|
||||
// check if element contains class
|
||||
if (element.classList && element.classList.contains(className)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// check if parent node has the class
|
||||
if (element.parentNode) {
|
||||
return elementOrAnyAncestorHasClass(element.parentNode, className);
|
||||
}
|
||||
|
||||
// couldn't find the class
|
||||
return false;
|
||||
};
|
||||
|
||||
export function formatUptimeSeconds(secondsToFormat) {
|
||||
secondsToFormat = Number(secondsToFormat);
|
||||
const days = Math.floor(secondsToFormat / (3600 * 24));
|
||||
const hours = Math.floor((secondsToFormat % (3600 * 24)) / 3600);
|
||||
const minutes = Math.floor((secondsToFormat % 3600) / 60);
|
||||
const seconds = Math.floor(secondsToFormat % 60);
|
||||
const daysPlural = days === 1 ? 'day' : 'days';
|
||||
return `${days} ${daysPlural} ${hours}h ${minutes}m ${seconds}s`;
|
||||
};
|
||||
|
||||
export function celsiusToFahrenheit(celsius) {
|
||||
return (celsius * 9/5) + 32;
|
||||
};
|
||||
|
||||
|
||||
/** operates on state */
|
||||
|
||||
// find node by id
|
||||
export function findNodeById(id) {
|
||||
const node = state.nodes.find((node) => node.node_id.toString() === id.toString());
|
||||
if (node) return node;
|
||||
return null;
|
||||
};
|
||||
|
||||
// find node marker by id
|
||||
export function findNodeMarkerById(id) {
|
||||
return state.nodeMarkers[id] ?? null;
|
||||
};
|
||||
|
||||
/** operates on config */
|
||||
|
||||
export function formatTemperature(celsius) {
|
||||
switch (temperatureFormat.value) {
|
||||
case "celsius": {
|
||||
return `${Number(celsius).toFixed(0)}ºC`;
|
||||
}
|
||||
case "fahrenheit": {
|
||||
const fahrenheit = celsiusToFahrenheit(celsius);
|
||||
return `${fahrenheit.toFixed(0)}ºF`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getTemperatureUnit() {
|
||||
switch (temperatureFormat.value) {
|
||||
case "celsius": return "ºC";
|
||||
case "fahrenheit": return "ºF";
|
||||
}
|
||||
};
|
||||
|
||||
export function buildPath(endpoint) {
|
||||
return `${BASE_PATH}${endpoint}`
|
||||
};
|
||||
|
||||
// determine if node was recently heard uplinking packets to mqtt
|
||||
export function hasNodeUplinkedToMqttRecently(node) {
|
||||
const now = moment();
|
||||
const millisecondsSinceNodeLastUplinkedToMqtt = now.diff(moment(node.mqtt_connection_state_updated_at));
|
||||
return millisecondsSinceNodeLastUplinkedToMqtt < nodesDisconnectedAge.value * 1000;
|
||||
}
|
Reference in New Issue
Block a user