GeoFS GPWS Alerts

GPWS and other alerts for GeoFS

目前為 2025-01-14 提交的版本,檢視 最新版本

// ==UserScript==
// @name         GeoFS GPWS Alerts
// @namespace    https://avramovic.info/
// @version      1.0.6
// @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;
    let wasOnGround = true;


    // 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;
    }


    function mainLoop(){

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

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

        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.pause();
        }

        // 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.pause();
        }

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

        }
        apWasOn = apIsOn;

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


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

                if (flapsRetracted()) {
                    lowflaps.play();
                } else {
                    lowflaps.pause();
                    lowflaps.currentTime = 0;
                }
            }

        } else {
            lowgear.pause();
            lowgear.currentTime = 0;
            lowflaps.pause();
            lowflaps.currentTime = 0;
        }


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

        // 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;
        wasOnGround = isOnGround();
    }
})();