GeoFS GPWS Alerts

GPWS and other alerts for GeoFS

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         GeoFS GPWS Alerts
// @namespace    https://avramovic.info/
// @version      1.0.7
// @description  GPWS and other alerts for GeoFS
// @author       Nemanja Avramovic
// @match        https://www.geo-fs.com/geofs.php*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=geo-fs.com
// @grant        GM.getResourceUrl
// @resource     stall https://github.com/avramovic/geofs-alerts/raw/master/audio/airbus-stall-warning.mp3
// @resource     bankangle https://github.com/avramovic/geofs-alerts/raw/master/audio/bank-angle-bank-angle.mp3
// @resource     overspeed https://github.com/avramovic/geofs-alerts/raw/master/audio/md-80-overspeed.mp3
// @resource     autopilot https://github.com/avramovic/geofs-alerts/raw/master/audio/airbus-autopilot-off.mp3
// @resource     terrain https://github.com/avramovic/geofs-alerts/raw/master/audio/terrain-terrain-pull-up.mp3
// @resource     lowgear https://github.com/avramovic/geofs-alerts/raw/master/audio/too-low-gear.mp3
// @resource     lowflaps https://github.com/avramovic/geofs-alerts/raw/master/audio/too-low-flaps.mp3
// @resource     sinkrate https://github.com/avramovic/geofs-alerts/raw/master/audio/sink-rate.mp3
// @resource     minimums https://github.com/avramovic/geofs-alerts/raw/master/audio/minimums.mp3
// @resource     approaching https://github.com/avramovic/geofs-alerts/raw/master/audio/approaching-minimums.mp3
// @resource     retard https://github.com/avramovic/geofs-alerts/raw/master/audio/airbus-retard.mp3
// @resource     h2500 https://github.com/avramovic/geofs-alerts/raw/master/audio/2500.mp3
// @resource     h1000 https://github.com/avramovic/geofs-alerts/raw/master/audio/1000.mp3
// @resource     h500 https://github.com/avramovic/geofs-alerts/raw/master/audio/500.mp3
// @resource     h400 https://github.com/avramovic/geofs-alerts/raw/master/audio/400.mp3
// @resource     h300 https://github.com/avramovic/geofs-alerts/raw/master/audio/300.mp3
// @resource     h200 https://github.com/avramovic/geofs-alerts/raw/master/audio/200.mp3
// @resource     h100 https://github.com/avramovic/geofs-alerts/raw/master/audio/100.mp3
// @resource     h50 https://github.com/avramovic/geofs-alerts/raw/master/audio/50.mp3
// @resource     h40 https://github.com/avramovic/geofs-alerts/raw/master/audio/40.mp3
// @resource     h30 https://github.com/avramovic/geofs-alerts/raw/master/audio/30.mp3
// @resource     h20 https://github.com/avramovic/geofs-alerts/raw/master/audio/20.mp3
// @resource     h10 https://github.com/avramovic/geofs-alerts/raw/master/audio/10.mp3
// @resource     h5 https://github.com/avramovic/geofs-alerts/raw/master/audio/5.mp3
// ==/UserScript==

(function () {
    'use strict';
    // load the audio clips
    let stickShake;
    GM.getResourceUrl("stall").then((data) => {
        stickShake = new Audio('data:audio/mp3;'+data);
        stickShake.loop = true;
    });

    let bankangle;
    GM.getResourceUrl("bankangle").then((data) => {
        bankangle = new Audio('data:audio/mp3;'+data);
        bankangle.loop = true;
    });

    let overspeed;
    GM.getResourceUrl("overspeed").then((data) => {
        overspeed = new Audio('data:audio/mp3;'+data);
        overspeed.loop = true;
    });

    let autopilot;
    GM.getResourceUrl("autopilot").then((data) => {
        autopilot = new Audio('data:audio/mp3;'+data);
        autopilot.loop = false;
    });

    let terrain;
    GM.getResourceUrl("terrain").then((data) => {
        terrain = new Audio('data:audio/mp3;'+data);
        terrain.loop = true;
    });

    let lowgear;
    GM.getResourceUrl("lowgear").then((data) => {
        lowgear = new Audio('data:audio/mp3;'+data);
        lowgear.loop = true;
    });

    let lowflaps;
    GM.getResourceUrl("lowflaps").then((data) => {
        lowflaps = new Audio('data:audio/mp3;'+data);
        lowflaps.loop = true;
    });

    let sinkrate;
    GM.getResourceUrl("sinkrate").then((data) => {
        sinkrate = new Audio('data:audio/mp3;'+data);
        sinkrate.loop = true;
    });

    let approaching;
    GM.getResourceUrl("approaching").then((data) => {
        approaching = new Audio('data:audio/mp3;'+data);
        approaching.loop = false;
    });

    let h2500;
    GM.getResourceUrl("h2500").then((data) => {
        h2500 = new Audio('data:audio/mp3;'+data);
        h2500.loop = false;
    });

    let h1000;
    GM.getResourceUrl("h1000").then((data) => {
        h1000 = new Audio('data:audio/mp3;'+data);
        h1000.loop = false;
    });

    let h500;
    GM.getResourceUrl("h500").then((data) => {
        h500 = new Audio('data:audio/mp3;'+data);
        h500.loop = false;
    });

    let h400;
    GM.getResourceUrl("h400").then((data) => {
        h400 = new Audio('data:audio/mp3;'+data);
        h400.loop = false;
    });

    let h300;
    GM.getResourceUrl("h300").then((data) => {
        h300 = new Audio('data:audio/mp3;'+data);
        h300.loop = false;
    });

    let h200;
    GM.getResourceUrl("h200").then((data) => {
        h200 = new Audio('data:audio/mp3;'+data);
        h200.loop = false;
    });

    let h100;
    GM.getResourceUrl("h100").then((data) => {
        h100 = new Audio('data:audio/mp3;'+data);
        h100.loop = false;
    });

    let h50;
    GM.getResourceUrl("h50").then((data) => {
        h50 = new Audio('data:audio/mp3;'+data);
        h50.loop = false;
    });

    let h40;
    GM.getResourceUrl("h40").then((data) => {
        h40 = new Audio('data:audio/mp3;'+data);
        h40.loop = false;
    });

    let h30;
    GM.getResourceUrl("h30").then((data) => {
        h30 = new Audio('data:audio/mp3;'+data);
        h30.loop = false;
    });

    let h20;
    GM.getResourceUrl("h20").then((data) => {
        h20 = new Audio('data:audio/mp3;'+data);
        h20.loop = false;
    });

    let h10;
    GM.getResourceUrl("h10").then((data) => {
        h10 = new Audio('data:audio/mp3;'+data);
        h10.loop = false;
    });

    let h5;
    GM.getResourceUrl("h5").then((data) => {
        h5 = new Audio('data:audio/mp3;'+data);
        h5.loop = false;
    });

    let minimums;
    GM.getResourceUrl("minimums").then((data) => {
        minimums = new Audio('data:audio/mp3;'+data);
        minimums.loop = false;
    });

    let retard;
    GM.getResourceUrl("retard").then((data) => {
        retard = new Audio('data:audio/mp3;'+data);
        retard.loop = false;
    });

    let apWasOn = false;
    let apIsOn = false;
    let oldAltitude = 0;
    let altitude = 0;

    // wait until flight sim is fully loaded
    let itv = setInterval(
      function(){
          if(unsafeWindow.ui && unsafeWindow.flight && unsafeWindow.geofs){
              setInterval(function() { mainLoop(); }, 500);
              clearInterval(itv);
          }
      }
      ,500);

    function isGearUp() {
        return unsafeWindow.geofs.animation.values.gearPosition == 1;
    }

    function isGearDown() {
        return unsafeWindow.geofs.animation.values.gearPosition == 0;
    }

    function seaAltitude() {
        return unsafeWindow.geofs.animation.values.altitude;
    }

    function groundAltitude() {
        return seaAltitude() - unsafeWindow.geofs.animation.values.groundElevationFeet - 50;
    }

    function isDescending() {
        return unsafeWindow.geofs.animation.values.verticalSpeed < -50;
    }

    function isSinking() {
        return unsafeWindow.geofs.animation.values.verticalSpeed < -2500;
    }

    function isAscending() {
        return unsafeWindow.geofs.animation.values.verticalSpeed > 50;
    }

    function flapsRetracted() {
        return unsafeWindow.geofs.animation.values.flapsValue == 0;
    }

    function flapsExtended() {
        return unsafeWindow.geofs.animation.values.flapsValue > 0;
    }

    function getLatitude() {
        return unsafeWindow.geofs.aircraft.instance.lastLlaLocation[0];
    }

    function getLongitude() {
        return unsafeWindow.geofs.aircraft.instance.lastLlaLocation[1];
    }

    function isOnGround() {
        return unsafeWindow.geofs.animation.values.groundContact == 1 ? true : false;
    }

    function isStalling() {
        return unsafeWindow.geofs.aircraft.instance.stalling;
    }

    function isCrashed() {
        return unsafeWindow.geofs.aircraft.instance.crashed;
    }

    function isEngineOn() {
        return unsafeWindow.geofs.aircraft.instance.engine.on;
    }

    function haversine(lat1, lon1, lat2, lon2) {
        const R = 6371; // Radius of the Earth in kilometers
        const toRad = (deg) => deg * (Math.PI / 180);

        const dLat = toRad(lat2 - lat1);
        const dLon = toRad(lon2 - lon1);

        const a =
          Math.sin(dLat / 2) * Math.sin(dLat / 2) +
          Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
        const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
        return R * c; // Distance in kilometers
    }

    function findNearestAirport() {
        let aircraftPosition = {
            lat: getLatitude(),
            lon: getLongitude(),
        };

        let nearestAirport = null;
        let minDistance = Infinity;

        for (let i in unsafeWindow.geofs.mainAirportList) {
            let ap = unsafeWindow.geofs.mainAirportList[i];
            let airportPosition = {
                lat: ap[0],
                lon: ap[1]
            };

            let distance = haversine(
              aircraftPosition.lat,
              aircraftPosition.lon,
              airportPosition.lat,
              airportPosition.lon
            );

            if (distance < minDistance) {
                minDistance = distance;
                nearestAirport = {
                    airport: i,
                    distanceInKm: distance
                };
            }

        }

        return nearestAirport;
    }

    Audio.prototype.stop = function() {
        this.pause();
        this.currentTime = 0;
    };

    function mainLoop(){

        if (unsafeWindow.geofs.isPaused()) {
            //paused, do not run the loop
            return;
        }

        // stall alert
        !isOnGround() && isStalling() ? stickShake.play() : stickShake.stop();

        const fastPlanes = ["F-16 Fighting Falcon", "Concorde", "Sukhoi Su-35", "Boeing F/A-18F Super Hornet", "Wingsuit"];

        // bank angle alert
        if (!fastPlanes.includes(unsafeWindow.geofs.aircraft.instance.aircraftRecord.name.trim())) {
            Math.abs(unsafeWindow.geofs.animation.values.aroll) > 40 ? bankangle.play() : bankangle.stop();
        }

        // indicated airspeed overspeed alert
        if (!fastPlanes.includes(unsafeWindow.geofs.aircraft.instance.aircraftRecord.name.trim())) {
            let maxSpeed = unsafeWindow.geofs.animation.values.VNO > 0 ? unsafeWindow.geofs.animation.values.VNO+1 : 350;
            unsafeWindow.geofs.animation.values.kias > maxSpeed ? overspeed.play() : overspeed.stop();
        }

        // autopilot disconnect alert
        apIsOn = unsafeWindow.geofs.autopilot.on;
        if (apWasOn && !apIsOn) {
            autopilot.play();
        } else if (!apWasOn && apIsOn) {
            autopilot.stop();
        }
        apWasOn = apIsOn;

        // terrain alert
        if (isGearUp() && groundAltitude() <= 1000 && !isOnGround() && isEngineOn()) {
            terrain.play();
        } else {
            terrain.stop();
        }


        let nearestAp = findNearestAirport();
        if (isDescending() && groundAltitude() <= 1500 && typeof nearestAp == "object" && nearestAp.distanceInKm < 20) {
            if (isGearUp()) {
                lowgear.play();
            } else {
                lowgear.stop();

                if (flapsRetracted()) {
                    lowflaps.play();
                } else {
                    lowflaps.stop();
                }
            }

        } else {
            lowgear.stop();
            lowflaps.stop();
        }


        // sink rate
        isSinking() ? sinkrate.play() : sinkrate.stop();

        // height callouts when fully configured for landing and near airport
        altitude = groundAltitude();

        if (isDescending() && isGearDown() && flapsExtended() && typeof nearestAp == "object" && nearestAp.distanceInKm < 20) {
            if (oldAltitude > 2500 && altitude <= 2500) {
                h2500.play();
            } else if (oldAltitude > 1000 && altitude <= 1000) {
                h1000.play();
            } else if (oldAltitude > 500 && altitude <= 500) {
                h500.play();
            } else if (oldAltitude > 400 && altitude <= 400) {
                h400.play();
            } else if (oldAltitude > 350 && altitude <= 350) {
                approaching.play();
            } else if (oldAltitude > 300 && altitude <= 300) {
                h300.play();
            } else if (oldAltitude > 200 && altitude <= 200) {
                h200.play();
            } else if (oldAltitude > 150 && altitude <= 150) {
                minimums.play();
            } else if (oldAltitude > 100 && altitude <= 100) {
                h100.play();
            } else if (oldAltitude > 50 && altitude <= 50) {
                h50.play();
            } else if (oldAltitude > 40 && altitude <= 40) {
                h40.play();
            } else if (oldAltitude > 30 && altitude <= 30) {
                h30.play();
            } else if (oldAltitude > 20 && altitude <= 20) {
                h20.play();
                if (isEngineOn()) {
                    retard.play();
                }
            } else if (oldAltitude > 10 && altitude <= 10) {
                h10.play();
            } else if (oldAltitude > 5 && altitude <= 5) {
                h5.play();
            }
        }

        oldAltitude = altitude;
    }
})();