forked from FOSS/BangleApps
528 lines
16 KiB
JavaScript
528 lines
16 KiB
JavaScript
/*
|
|
* Flight Dashboard - Bangle.js
|
|
*/
|
|
|
|
const COLOUR_BLACK = 0x0000; // same as: g.setColor(0, 0, 0)
|
|
const COLOUR_WHITE = 0xffff; // same as: g.setColor(1, 1, 1)
|
|
const COLOUR_GREEN = 0x07e0; // same as: g.setColor(0, 1, 0)
|
|
const COLOUR_YELLOW = 0xffe0; // same as: g.setColor(1, 1, 0)
|
|
const COLOUR_MAGENTA = 0xf81f; // same as: g.setColor(1, 0, 1)
|
|
const COLOUR_CYAN = 0x07ff; // same as: g.setColor(0, 1, 1)
|
|
const COLOUR_LIGHT_BLUE = 0x841f; // same as: g.setColor(0.5, 0.5, 1)
|
|
|
|
const APP_NAME = 'flightdash';
|
|
|
|
const horizontalCenter = g.getWidth() / 2;
|
|
//const verticalCenter = g.getHeight() / 2;
|
|
|
|
const dataFontHeight = 22;
|
|
const secondaryFontHeight = 18;
|
|
const labelFontHeight = 12;
|
|
|
|
|
|
//globals
|
|
var settings = {};
|
|
|
|
//var updateInterval;
|
|
|
|
var speed = '-'; var speedPrev = -1;
|
|
var track = '-'; var trackPrev = -1;
|
|
var lat = 0; var lon = 0;
|
|
var distance = '-'; var distancePrev = -1;
|
|
var bearing = '-'; var bearingPrev = -1;
|
|
var relativeBearing = 0; var relativeBearingPrev = -1;
|
|
var fromCardinal = '-';
|
|
var ETAdate = new Date();
|
|
var ETA = '-'; var ETAPrev = '';
|
|
|
|
var QNH = Math.round(Bangle.getOptions().seaLevelPressure); var QNHPrev = -1;
|
|
|
|
var altitude = '-'; var altitudePrev = -1;
|
|
|
|
var VSI = '-'; var VSIPrev = -1;
|
|
var VSIraw = 0;
|
|
var VSIprevTimestamp = Date.now();
|
|
var VSIprevAltitude;
|
|
var VSIsamples = 0; var VSIsamplesCount = 0;
|
|
|
|
var speedUnit = 'N/A';
|
|
var distanceUnit = 'N/A';
|
|
var altUnit = 'N/A';
|
|
|
|
|
|
// date object to time string in format (HH:MM[:SS])
|
|
function timeStr(date, seconds) {
|
|
let timeStr = date.getHours().toString();
|
|
if (timeStr.length == 1) timeStr = '0' + timeStr;
|
|
let minutes = date.getMinutes().toString();
|
|
if (minutes.length == 1) minutes = '0' + minutes;
|
|
timeStr += ':' + minutes;
|
|
if (seconds) {
|
|
let seconds = date.getSeconds().toString();
|
|
if (seconds.length == 1) seconds = '0' + seconds;
|
|
timeStr += ':' + seconds;
|
|
}
|
|
return timeStr;
|
|
}
|
|
|
|
// add thousands separator to number
|
|
function addThousandSeparator(n) {
|
|
let s = n.toString();
|
|
if (s.length > 3) {
|
|
return s.substr(0, s.length - 3) + ',' + s.substr(s.length - 3, 3);
|
|
} else {
|
|
return s;
|
|
}
|
|
}
|
|
|
|
|
|
// update VSI
|
|
function updateVSI(alt) {
|
|
VSIsamples += alt; VSIsamplesCount += 1;
|
|
let VSInewTimestamp = Date.now();
|
|
if (VSIprevTimestamp + 1000 <= VSInewTimestamp) { // update VSI every 1 second
|
|
let VSInewAltitude = VSIsamples / VSIsamplesCount;
|
|
if (VSIprevAltitude) {
|
|
let VSIinterval = (VSInewTimestamp - VSIprevTimestamp) / 1000;
|
|
VSIraw = (VSInewAltitude - VSIprevAltitude) * 60 / VSIinterval; // extrapolate to change / minute
|
|
}
|
|
VSIprevTimestamp = VSInewTimestamp;
|
|
VSIprevAltitude = VSInewAltitude;
|
|
VSIsamples = 0; VSIsamplesCount = 0;
|
|
}
|
|
|
|
VSI = Math.floor(VSIraw / 10) * 10; // "smooth" VSI value
|
|
if (settings.altimeterUnits == 0) { // Feet
|
|
VSI = Math.round(VSI * 3.28084);
|
|
} // nothing else required since VSI is already in meters ("smoothed")
|
|
|
|
if (VSI > 9999) VSI = 9999;
|
|
else if (VSI < -9999) VSI = -9999;
|
|
}
|
|
|
|
// update GPS-derived information
|
|
function updateGPS(fix) {
|
|
if (!('fix' in fix) || fix.fix == 0 || fix.satellites < 4) return;
|
|
|
|
speed = 'N/A';
|
|
if (settings.speedUnits == 0) { // Knots
|
|
speed = Math.round(fix.speed * 0.539957);
|
|
} else if (settings.speedUnits == 1) { // km/h
|
|
speed = Math.round(fix.speed);
|
|
} else if (settings.speedUnits == 2) { // MPH
|
|
speed = Math.round(fix.speed * 0.621371);
|
|
}
|
|
if (speed > 9999) speed = 9999;
|
|
|
|
if (! settings.useBaro) { // use GPS altitude
|
|
altitude = 'N/A';
|
|
if (settings.altimeterUnits == 0) { // Feet
|
|
altitude = Math.round(fix.alt * 3.28084);
|
|
} else if (settings.altimeterUnits == 1) { // Meters
|
|
altitude = Math.round(fix.alt);
|
|
}
|
|
if (altitude > 99999) altitude = 99999;
|
|
|
|
updateVSI(fix.alt);
|
|
}
|
|
|
|
track = Math.round(fix.course);
|
|
if (isNaN(track)) track = '-';
|
|
else if (track < 10) track = '00'+track;
|
|
else if (track < 100) track = '0'+track;
|
|
|
|
lat = fix.lat;
|
|
lon = fix.lon;
|
|
|
|
// calculation from https://www.movable-type.co.uk/scripts/latlong.html
|
|
const latRad1 = lat * Math.PI/180;
|
|
const latRad2 = settings.destLat * Math.PI/180;
|
|
const lonRad1 = lon * Math.PI/180;
|
|
const lonRad2 = settings.destLon * Math.PI/180;
|
|
|
|
// distance (using "Equirectangular approximation")
|
|
let x = (lonRad2 - lonRad1) * Math.cos((latRad1 + latRad2) / 2);
|
|
let y = (latRad2 - latRad1);
|
|
let distanceNumber = Math.sqrt(x*x + y*y) * 6371; // in km - 6371 = mean Earth radius
|
|
if (settings.speedUnits == 0) { // NM
|
|
distanceNumber = distanceNumber * 0.539957;
|
|
} else if (settings.speedUnits == 2) { // miles
|
|
distanceNumber = distanceNumber * 0.621371;
|
|
}
|
|
if (distanceNumber > 99.9) {
|
|
distance = '>100';
|
|
} else {
|
|
distance = (Math.round(distanceNumber * 10) / 10).toString();
|
|
if (! distance.includes('.'))
|
|
distance += '.0';
|
|
}
|
|
|
|
// bearing
|
|
y = Math.sin(lonRad2 - lonRad1) * Math.cos(latRad2);
|
|
x = Math.cos(latRad1) * Math.sin(latRad2) -
|
|
Math.sin(latRad1) * Math.cos(latRad2) * Math.cos(lonRad2 - lonRad1);
|
|
let nonNormalisedBearing = Math.atan2(y, x);
|
|
bearing = Math.round((nonNormalisedBearing * 180 / Math.PI + 360) % 360);
|
|
|
|
if (bearing > 337 || bearing < 23) {
|
|
fromCardinal = 'S';
|
|
} else if (bearing < 68) {
|
|
fromCardinal = 'SW';
|
|
} else if (bearing < 113) {
|
|
fromCardinal = 'W';
|
|
} else if (bearing < 158) {
|
|
fromCardinal = 'NW';
|
|
} else if (bearing < 203) {
|
|
fromCardinal = 'N';
|
|
} else if (bearing < 248) {
|
|
fromCardinal = 'NE';
|
|
} else if (bearing < 293) {
|
|
fromCardinal = 'E';
|
|
} else{
|
|
fromCardinal = 'SE';
|
|
}
|
|
|
|
if (bearing < 10) bearing = '00'+bearing;
|
|
else if (bearing < 100) bearing = '0'+bearing;
|
|
|
|
relativeBearing = parseInt(bearing) - parseInt(track);
|
|
if (isNaN(relativeBearing)) relativeBearing = 0;
|
|
if (relativeBearing > 180) relativeBearing -= 360;
|
|
else if (relativeBearing < -180) relativeBearing += 360;
|
|
|
|
// ETA
|
|
if (speed) {
|
|
let ETE = distanceNumber * 3600 / speed;
|
|
let now = new Date();
|
|
ETAdate = new Date(now + (now.getTimezoneOffset() * 1000 * 60) + ETE*1000);
|
|
if (ETE < 86400) {
|
|
ETA = timeStr(ETAdate, false);
|
|
} else {
|
|
ETA = '>24h';
|
|
}
|
|
} else {
|
|
ETAdate = new Date();
|
|
ETA = '-';
|
|
}
|
|
}
|
|
|
|
|
|
// update barometric information
|
|
function updatePressure(e) {
|
|
altitude = 'N/A';
|
|
if (settings.altimeterUnits == 0) { // Feet
|
|
altitude = Math.round(e.altitude * 3.28084);
|
|
} else if (settings.altimeterUnits == 1) { // Meters
|
|
altitude = Math.round(e.altitude); // altitude is given in meters
|
|
}
|
|
if (altitude > 99999) altitude = 99999;
|
|
|
|
updateVSI(e.altitude);
|
|
}
|
|
|
|
|
|
// (re-)draw all read-outs
|
|
function draw(initial) {
|
|
|
|
g.setBgColor(COLOUR_BLACK);
|
|
|
|
// speed
|
|
if (speed != speedPrev || initial) {
|
|
g.setFontAlign(-1, -1).setFont("Vector", dataFontHeight).setColor(COLOUR_GREEN);
|
|
g.clearRect(0, 0, 55, dataFontHeight);
|
|
g.drawString(speed.toString(), 0, 0, false);
|
|
if (initial) {
|
|
g.setFontAlign(-1, -1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN);
|
|
g.drawString(speedUnit, 0, dataFontHeight, false);
|
|
}
|
|
speedPrev = speed;
|
|
}
|
|
|
|
|
|
// distance
|
|
if (distance != distancePrev || initial) {
|
|
g.setFontAlign(1, -1).setFont("Vector", dataFontHeight).setColor(COLOUR_WHITE);
|
|
g.clearRect(g.getWidth() - 58, 0, g.getWidth(), dataFontHeight);
|
|
g.drawString(distance, g.getWidth(), 0, false);
|
|
if (initial) {
|
|
g.setFontAlign(1, -1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN);
|
|
g.drawString(distanceUnit, g.getWidth(), dataFontHeight, false);
|
|
}
|
|
distancePrev = distance;
|
|
}
|
|
|
|
|
|
// track (+ static track/bearing content)
|
|
let trackY = 18;
|
|
let destInfoY = trackY + 53;
|
|
if (track != trackPrev || initial) {
|
|
g.setFontAlign(0, -1).setFont("Vector", dataFontHeight).setColor(COLOUR_WHITE);
|
|
g.clearRect(horizontalCenter - 29, trackY, horizontalCenter + 28, trackY + dataFontHeight);
|
|
g.drawString(track.toString() + "\xB0", horizontalCenter + 3, trackY, false);
|
|
if (initial) {
|
|
let y = trackY + dataFontHeight + 1;
|
|
g.setColor(COLOUR_YELLOW);
|
|
g.drawRect(horizontalCenter - 30, trackY - 3, horizontalCenter + 29, y);
|
|
g.drawLine(0, y, g.getWidth(), y);
|
|
y += dataFontHeight + 5;
|
|
g.drawLine(0, y, g.getWidth(), y);
|
|
|
|
g.setFontAlign(1, -1).setFont("Vector", secondaryFontHeight).setColor(COLOUR_MAGENTA);
|
|
g.drawString(settings.destID, horizontalCenter, destInfoY, false);
|
|
}
|
|
trackPrev = track;
|
|
}
|
|
|
|
|
|
// bearing
|
|
if (bearing != bearingPrev || relativeBearing != relativeBearingPrev || initial) {
|
|
let bearingY = trackY + 27;
|
|
|
|
g.clearRect(0, bearingY, g.getWidth(), bearingY + dataFontHeight);
|
|
|
|
g.setColor(COLOUR_YELLOW);
|
|
for (let i = Math.floor(relativeBearing * 2.5) % 25; i <= g.getWidth(); i += 25) {
|
|
g.drawLine(i, bearingY + 3, i, bearingY + 16);
|
|
}
|
|
|
|
let bearingX = horizontalCenter + relativeBearing * 2.5;
|
|
if (bearingX > g.getWidth() - 26) bearingX = g.getWidth() - 26;
|
|
else if (bearingX < 26) bearingX = 26;
|
|
g.setFontAlign(0, -1).setFont("Vector", dataFontHeight).setColor(COLOUR_MAGENTA);
|
|
g.drawString(bearing.toString() + "\xB0", bearingX + 3, bearingY, false);
|
|
|
|
g.clearRect(horizontalCenter + 42, destInfoY, horizontalCenter + 69, destInfoY + secondaryFontHeight);
|
|
g.setFontAlign(-1, -1).setFont("Vector", secondaryFontHeight).setColor(COLOUR_MAGENTA);
|
|
g.drawString(fromCardinal, horizontalCenter + 42, destInfoY, false);
|
|
if (initial) {
|
|
g.setFontAlign(-1, -1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN);
|
|
g.drawString(' from', horizontalCenter, destInfoY, false);
|
|
}
|
|
|
|
bearingPrev = bearing;
|
|
relativeBearingPrev = relativeBearing;
|
|
}
|
|
|
|
|
|
let row3y = g.getHeight() - 48;
|
|
|
|
// QNH
|
|
if (settings.useBaro) {
|
|
if (QNH != QNHPrev || initial) {
|
|
let QNHy = row3y - secondaryFontHeight - 2;
|
|
g.setFontAlign(0, 1).setFont("Vector", secondaryFontHeight).setColor(COLOUR_WHITE);
|
|
g.clearRect(horizontalCenter - 29, QNHy - secondaryFontHeight, horizontalCenter + 22, QNHy);
|
|
g.drawString(QNH.toString(), horizontalCenter - 3, QNHy, false);
|
|
if (initial) {
|
|
g.setFontAlign(0, -1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN);
|
|
g.drawString('QNH', horizontalCenter - 3, QNHy, false);
|
|
}
|
|
QNHPrev = QNH;
|
|
}
|
|
}
|
|
|
|
|
|
// VSI
|
|
if (VSI != VSIPrev || initial) {
|
|
g.setFontAlign(-1, 1).setFont("Vector", secondaryFontHeight).setColor(COLOUR_WHITE);
|
|
g.clearRect(0, row3y - secondaryFontHeight, 51, row3y);
|
|
g.drawString(VSI.toString(), 0, row3y, false);
|
|
if (initial) {
|
|
g.setFontAlign(-1, 1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN);
|
|
g.drawString(altUnit + '/min', 0, row3y - secondaryFontHeight, false);
|
|
}
|
|
|
|
let VSIarrowX = 6;
|
|
let VSIarrowY = row3y - 42;
|
|
g.clearRect(VSIarrowX - 7, VSIarrowY - 10, VSIarrowX + 6, VSIarrowY + 10);
|
|
g.setColor(COLOUR_WHITE);
|
|
if (VSIraw > 30) { // climbing
|
|
g.fillRect(VSIarrowX - 1, VSIarrowY, VSIarrowX + 1, VSIarrowY + 10);
|
|
g.fillPoly([ VSIarrowX , VSIarrowY - 11,
|
|
VSIarrowX + 7, VSIarrowY,
|
|
VSIarrowX - 7, VSIarrowY]);
|
|
} else if (VSIraw < -30) { // descending
|
|
g.fillRect(VSIarrowX - 1, VSIarrowY - 10, VSIarrowX + 1, VSIarrowY);
|
|
g.fillPoly([ VSIarrowX , VSIarrowY + 11,
|
|
VSIarrowX + 7, VSIarrowY,
|
|
VSIarrowX - 7, VSIarrowY ]);
|
|
}
|
|
}
|
|
|
|
|
|
// altitude
|
|
if (altitude != altitudePrev || initial) {
|
|
g.setFontAlign(1, 1).setFont("Vector", secondaryFontHeight).setColor(COLOUR_WHITE);
|
|
g.clearRect(g.getWidth() - 65, row3y - secondaryFontHeight, g.getWidth(), row3y);
|
|
g.drawString(addThousandSeparator(altitude), g.getWidth(), row3y, false);
|
|
if (initial) {
|
|
g.setFontAlign(1, 1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN);
|
|
g.drawString(altUnit, g.getWidth(), row3y - secondaryFontHeight, false);
|
|
}
|
|
altitudePrev = altitude;
|
|
}
|
|
|
|
|
|
// time
|
|
let now = new Date();
|
|
let nowUTC = new Date(now + (now.getTimezoneOffset() * 1000 * 60));
|
|
g.setFontAlign(-1, 1).setFont("Vector", dataFontHeight).setColor(COLOUR_LIGHT_BLUE);
|
|
let timeStrMetrics = g.stringMetrics(timeStr(now, false));
|
|
g.drawString(timeStr(now, false), 0, g.getHeight(), true);
|
|
|
|
let seconds = now.getSeconds().toString();
|
|
if (seconds.length == 1) seconds = '0' + seconds;
|
|
g.setFontAlign(-1, 1).setFont("Vector", secondaryFontHeight);
|
|
g.drawString(seconds, timeStrMetrics.width + 2, g.getHeight() - 1, true);
|
|
|
|
if (initial) {
|
|
g.setFontAlign(-1, 1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN);
|
|
g.drawString('LOCAL', 0, g.getHeight() - dataFontHeight, false);
|
|
}
|
|
|
|
|
|
// ETE
|
|
let ETEy = g.getHeight() - dataFontHeight;
|
|
let ETE = '-';
|
|
if (ETA != '-') {
|
|
let ETEseconds = Math.floor((ETAdate - nowUTC) / 1000);
|
|
if (ETEseconds < 0) ETEseconds = 0;
|
|
ETE = ETEseconds % 60;
|
|
if (ETE < 10) ETE = '0' + ETE;
|
|
ETE = Math.floor(ETEseconds / 60) + ':' + ETE;
|
|
if (ETE.length > 6) ETE = '>999m';
|
|
}
|
|
g.clearRect(horizontalCenter - 35, ETEy - secondaryFontHeight, horizontalCenter + 29, ETEy);
|
|
g.setFontAlign(0, 1).setFont("Vector", secondaryFontHeight).setColor(COLOUR_WHITE);
|
|
g.drawString(ETE, horizontalCenter - 3, ETEy, false);
|
|
if (initial) {
|
|
g.setFontAlign(0, 1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN);
|
|
g.drawString('ETE', horizontalCenter - 3, ETEy - secondaryFontHeight, false);
|
|
}
|
|
|
|
|
|
// ETA
|
|
if (ETA != ETAPrev || initial) {
|
|
g.clearRect(g.getWidth() - 63, g.getHeight() - dataFontHeight, g.getWidth(), g.getHeight());
|
|
g.setFontAlign(1, 1).setFont("Vector", dataFontHeight).setColor(COLOUR_WHITE);
|
|
g.drawString(ETA, g.getWidth(), g.getHeight(), false);
|
|
if (initial) {
|
|
g.setFontAlign(1, 1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN);
|
|
g.drawString('UTC ETA', g.getWidth(), g.getHeight() - dataFontHeight, false);
|
|
}
|
|
ETAPrev = ETA;
|
|
}
|
|
}
|
|
|
|
|
|
function handleSwipes(directionLR, directionUD) {
|
|
if (directionUD == -1) { // up -> increase QNH
|
|
QNH = Math.round(Bangle.getOptions().seaLevelPressure);
|
|
QNH++;
|
|
Bangle.setOptions({'seaLevelPressure': QNH});
|
|
} else if (directionUD == 1) { // down -> decrease QNH
|
|
QNH = Math.round(Bangle.getOptions().seaLevelPressure);
|
|
QNH--;
|
|
Bangle.setOptions({'seaLevelPressure': QNH});
|
|
}
|
|
}
|
|
|
|
function handleTouch(button, xy) {
|
|
if ('handled' in xy && xy.handled) return;
|
|
Bangle.removeListener('touch', handleTouch);
|
|
if (settings.useBaro) {
|
|
Bangle.removeListener('swipe', handleSwipes);
|
|
}
|
|
|
|
// any touch -> show settings
|
|
clearInterval(updateTimeInterval);
|
|
Bangle.setGPSPower(false, APP_NAME);
|
|
if (settings.useBaro)
|
|
Bangle.setBarometerPower(false, APP_NAME);
|
|
|
|
eval(require("Storage").read(APP_NAME+'.settings.js'))( () => {
|
|
E.showMenu();
|
|
// "clear" values potentially affected by a settings change
|
|
speed = '-'; distance = '-';
|
|
altitude = '-'; VSI = '-';
|
|
// re-launch
|
|
start();
|
|
});
|
|
}
|
|
|
|
|
|
/*
|
|
* main
|
|
*/
|
|
function start() {
|
|
|
|
// read in the settings
|
|
settings = Object.assign({
|
|
useBaro: false,
|
|
speedUnits: 0, // KTS
|
|
altimeterUnits: 0, // FT
|
|
destID: 'KOSH',
|
|
destLat: 43.9844,
|
|
destLon: -88.5570,
|
|
}, require('Storage').readJSON(APP_NAME+'.json', true) || {});
|
|
|
|
// set units
|
|
if (settings.speedUnits == 0) { // Knots
|
|
speedUnit = 'KTS';
|
|
distanceUnit = 'NM';
|
|
} else if (settings.speedUnits == 1) { // km/h
|
|
speedUnit = 'KPH';
|
|
distanceUnit = 'KM';
|
|
} else if (settings.speedUnits == 2) { // MPH
|
|
speedUnit = 'MPH';
|
|
distanceUnit = 'SM';
|
|
}
|
|
|
|
if (settings.altimeterUnits == 0) { // Feet
|
|
altUnit = 'FT';
|
|
} else if (settings.altimeterUnits == 1) { // Meters
|
|
altUnit = 'M';
|
|
}
|
|
|
|
// initialise
|
|
g.reset();
|
|
g.setBgColor(COLOUR_BLACK);
|
|
g.clear();
|
|
|
|
// draw incl. static components
|
|
draw(true);
|
|
|
|
// enable timeout/interval and sensors
|
|
setTimeout(function() {
|
|
draw();
|
|
updateTimeInterval = setInterval(draw, 1000);
|
|
}, 1000 - (Date.now() % 1000));
|
|
|
|
Bangle.setGPSPower(true, APP_NAME);
|
|
Bangle.on('GPS', updateGPS);
|
|
|
|
if (settings.useBaro) {
|
|
Bangle.setBarometerPower(true, APP_NAME);
|
|
Bangle.on('pressure', updatePressure);
|
|
}
|
|
|
|
// handle interaction
|
|
if (settings.useBaro) {
|
|
Bangle.on('swipe', handleSwipes);
|
|
}
|
|
Bangle.on('touch', handleTouch);
|
|
setWatch(e => { Bangle.showClock(); }, BTN1); // exit on button press
|
|
}
|
|
|
|
start();
|
|
|
|
|
|
/*
|
|
// TMP for testing:
|
|
//settings.speedUnits = 1;
|
|
//settings.altimeterUnits = 1;
|
|
QNH = 1013;
|
|
updateGPS({"fix":1,"speed":228,"alt":3763,"course":329,"lat":36.0182,"lon":-75.6713});
|
|
updatePressure({"altitude":3700});
|
|
*/
|