//ship simulation class and support routines. This unit assumes that a global variable
//called ge is available representing the google earth plugin.
//written by: Paul van Dinther
//            Dinther Product Design
//            Software development and specialists in simulation
//            email: vandinther@gmail.com

//useful precalculated conversion values

    //multiply a meter with this to get a delta latitude (1/60 of a degree is 1852 meters)
    var metersToLocalLat = 0.0000089992800575953923686105111591073; 
    var halfmetersToLocalLat = metersToLocalLat/2; 

    //multiply a delta latitude with this to get meters (1/60 of a degree is 1852 meters)
    var DeltaLatitudeToMeters = 111120;
    
    //multiply with this to get angle in radians
    var degreesToRad = 0.017453292519943;

    //this is a full circle equivalent of 360 degrees expressed in radians
    var radianCircle = 57.295779513082320876798154814105;

    function shipview(){
        this.shipid = 0;
        this.viewMode = 0;
        this.altitudeMode = ge.ALTITUDE_ABSOLUTE;
        this.bearing = 0;
        this.distance = 0;
        this.altitude = 20;
        this.tilt = 0;
        this.heading = 0;
        this.roll = 0;    
        this.viewMode = 0;
        this.setView = function(bearing, distance, altitudeMode, altitude, heading, tilt, roll, viewMode){
            this.bearing = bearing;
            this.distance = distance;
            this.altitudeMode = altitudeMode;
            this.altitude = altitude;
            this.heading = heading;
            this.tilt = tilt;
            this.roll = roll;
            this.viewMode = viewMode;
        };
    }

    function ship(){
        this.URL = '';
        this.shipId = 99;
        this.view1 = new shipview();
        this.view2 = new shipview();
        this.view3 = new shipview();
        this.view4 = new shipview();
        this.hornSound = '';
        this.hornEndSound = '';
        this.anchorSound = '';
        this.engineSoundRange=600;
        this.mechanicSoundRange=1000;
        this.hornSoundRange=4000;
        this.anchorDown = false;
        this.modelObject = null;
        this.model = null;
        this.camAlt = 3; //look at camera target expressed in meters above the ship waterline
        this.shadow = null;
        this.shadowImageURL = '';
        this.shadowOffset = 5;
        this.shadowAngle = 0;
        this.shadowlength = 100;
        this.shadowwidth = 100;
        this.shadowAlpha = 0.5;
        this.wakePoint = null;
        this.wakeOffset = 100;
        this.wakeSize = 10;
        this.wakeImageURL = '';
        this.bowPoint = null;
        this.bowOffset = 100;
        this.bowSize = 10;
        this.bowWaveImageURL = '';
        this.waveRollFactor = 1;
        this.wavePitchFactor = 1;
        this.draft = 0;
        this.latstep = 0; //exposed to outside for camera following. We move the camera the same distance as the ship.
        this.longstep = 0;
        this.cosLatitude = 0;
        this.radLatitude = 0;
        this.metersToLocalLon = 0;
        this.rudderCurrent = 0;
        //inputs
        this.leftPowerValue = 0.0;
        this.rightPowerValue = 0.0;
        this.bowPowerValue = 0;
        
        this.rudderTarget = 0;
        //characteristics
        this.mass = 1000;
        this.turnFriction = 0.08;
        this.turnInertia = 200;
        this.moveFriction = 0.04;
        this.potentialEnginePower = 10;
        this.fuelCapacity = 24; //expressed in hours at full power
        this.rollEffect = 1;
        this.pitchEffect = 1;
        
        //outputs
        this.speed = 0;
        this.turnRate = 0;
        this.deltaHeading = 0;
        this.deltaAltitude = 0;
        this.heading = 0;
        this.altitude = 0;
        this.track = 0;
        this.fuel = 1; //0-1 as how much is remaining. 1=full tanks
        
        //internals   
        var timeSinceLastWake = 0;     
        var wake = new Array();
        var wakeAlphaList = new Array();
        var bow = new Array();
        var bowAlphaList = new Array();
        var wakeDistance = 0;
        var bowDistance = 0;
        var wakeIndex = 0;
        var bowIndex = 0;
        var fuelFactor = 1/(this.fuelCapacity*1800); //convert fuel capacity to fuel use per second at half power.
        this.capableEnginePower = this.potentialEnginePower;
        var initialising = true;
        var lowUpdateCount = 0;

        this.setLocalLatLonBox = function (latLonBox, latitude, longitude, height, width, angle){
            height = height * halfmetersToLocalLat; 
            width = width * halfmetersToLocalLat / this.cosLatitude;
            latLonBox.setNorth(latitude + height);
            latLonBox.setSouth(latitude - height);
            latLonBox.setEast(longitude + width);
            latLonBox.setWest(longitude - width);
            latLonBox.setRotation(-angle);
            return (latLonBox);
        }
        
        //returns approximate location coordinates based on bearing in degrees and distance in meters relative to location1
        this.getlocalBDLocation = function(location1, bearing, distance){
            //calc lat and lon delta distances in meters  
            var radBearing = bearing * degreesToRad;
            var latDelta = Math.cos(radBearing);
            var lonDelta = Math.sin(radBearing);
            var outLatitude = location1.getLatitude() + latDelta * distance * metersToLocalLat;
            var outLongitude = location1.getLongitude() +  lonDelta * distance * this.metersToLocalLon;
            return{latitude:outLatitude, longitude:outLongitude};
        }
        
        //places overlay with size defined in meters corrected for latitude
        this.createLocalOverlay = function(name, latitude, longitude, height, width, angle, href) {
            var icon = ge.createIcon('');
            icon.setHref(href);
            var latLonBox = ge.createLatLonBox('');
            var myGroundOverlay = ge.createGroundOverlay('');
            myGroundOverlay.setName(name);
            myGroundOverlay.setIcon(icon);
            myGroundOverlay.setLatLonBox(latLonBox);
            this.setLocalLatLonBox(myGroundOverlay.getLatLonBox(), latitude, longitude, height, width, angle);
            ge.getFeatures().appendChild(myGroundOverlay);
            return (myGroundOverlay);
        } 
        
        this.growLocalLatLonBox = function(latLonBox, addHeight, addWidth){
            addHeight = addHeight/2;
            addWidth = addWidth/2;
            latLonBox.setNorth(latLonBox.getNorth() + addHeight);
            latLonBox.setSouth(latLonBox.getSouth() - addHeight);
            latLonBox.setEast(latLonBox.getEast() + addWidth);
            latLonBox.setWest(latLonBox.getWest() - addWidth);

            return (latLonBox);
        }        
        
        var removeObject = function (object){
            if (object != null){
                var c = ge.getFeatures().getFirstChild();
                while (c) {
                    var s = c.getNextSibling();
                    if (c == object){
                        ge.getFeatures().removeChild(c);
                    }
                    c = s;
                }   
            }
        }
        
        this.removeObjects = function(){
            removeObject(this.shadow);
            removeObject(this.modelObject);
            for (var i=0; i < wake.length; i++) {
                removeObject(wake[i]);    
            }
            for (var i=0; i < bow.length; i++) {
                removeObject(bow[i]);    
            }
        }   
        
        this.update = function(deltaTime){
            lowUpdateCount += deltaTime;
            if (lowUpdateCount > 10){this.updateLow(lowUpdateCount); lowUpdateCount = 0;}
            this.radLatitude = this.model.getLocation().getLatitude() * degreesToRad;
            this.cosLatitude = Math.cos(this.radLatitude); 
            this.metersToLocalLon = metersToLocalLat/this.cosLatitude;      
            this.simulateStep(deltaTime);
            this.updateShadow();
            this.setWakePosition(deltaTime);    
            this.setBowPosition(deltaTime); 
        }
        this.updateLow = function(deltaTime){
            this.updateFuel(deltaTime);                  
        }
        this.createWakeArray = function (){
            if (this.wakeImageURL != ''){
                var wakeCount = 20;
                for (var i=0; i<wakeCount; i++) {
                    var wakeSize = 0.0001 + i*0.00001;
                    var cosWakeSize = (wakeSize / this.cosLatitude);
                    this.wakePoint = this.getlocalBDLocation(this.model.getLocation(), this.heading + 180, this.wakeOffset);
                    wake[i] = this.createLocalOverlay('wake'+i,this.wakePoint.latitude, this.wakePoint.longitude , wakeSize, cosWakeSize,this.heading, this.wakeImageURL); 
                    var wakeAlpha = Math.round(i/wakeCount*256);
                    wakeAlphaList[i] = 0.0001;
                    wake[i].getColor().setA(0);
                }
            }
        }
        this.createBowArray = function (){
            if (this.bowImageURL != ''){
                var bowCount = 24;
                for (var i=0; i<bowCount; i++) {
                    var bowSize = 0.0001 + i*0.00001;
                    var cosbowSize = (bowSize / this.cosLatitude);
                    this.bowPoint = this.getlocalBDLocation(this.model.getLocation(), this.heading + 180, 55);
                    bow[i] = this.createLocalOverlay('bow'+i,this.bowPoint.latitude, this.bowPoint.longitude , bowSize, cosbowSize,this.heading, this.bowImageURL); 
                    var bowAlpha = Math.round(i/bowCount*256);
                    bowAlphaList[i] = 0.0001;
                    bow[i].getColor().setA(0);
                }
            }
        } 
        this.setFuelCapacity = function(capacity){
            this.fuelCapacity = capacity;
            fuelFactor = 1/(this.fuelCapacity*1800);
        }

        this.setRudderTarget = function(angle){
            if (angle > 80){ angle = 80;}
            if (angle < -80){ angle = -80;}
            this.rudderTarget = angle;
        }
        this.setWakePosition = function (deltaTime) {
            if (this.wakeImageURL != ''){
                //if time has pased more last placed wake to ship stern
                var distanceTraveled = this.speed * deltaTime;
                timeSinceLastWake += deltaTime;
                //var wakeAlphaTarget = this.leftPowerValue;
                wakeAlphaTarget = Math.min(0.7,Math.max(Math.abs(this.leftPowerValue,0.2)));
                //faint wake if out of fuel
                if (this.capableEnginePower == 0){ wakeAlphaTarget = 0.2};
                var growCutOff = wakeAlphaTarget * 0.7;     
    
                var fadeFactor = Math.max(distanceTraveled / (this.wakeSize * 30), deltaTime/30);
                wakeDistance += distanceTraveled;
                if (wakeDistance > this.wakeSize * 1.5 || timeSinceLastWake > 3){
                    wakeIndex -= 1;
                    if (wakeIndex < 0){wakeIndex = wake.length;}
                    this.wakePoint = this.getlocalBDLocation(this.model.getLocation(), this.heading + 180, this.wakeOffset); //
                    this.setLocalLatLonBox(wake[wakeIndex].getLatLonBox(),this.wakePoint.latitude,this.wakePoint.longitude,this.wakeSize,this.wakeSize,this.track - this.rudderCurrent);
                    wakeAlphaList[wakeIndex] = wakeAlphaTarget;
                    wake[wakeIndex].getColor().setA(wakeAlphaList[wakeIndex] * 256);
                    wakeDistance = 0;
                    timeSinceLastWake = 0;
                }

                for (i=0; i < wake.length; i++) {
                    if (wakeAlphaList[i]>0) {
                        wakeAlphaList[i] -= fadeFactor;
                        wake[i].getColor().setA(wakeAlphaList[i] * 256);
                        if (wakeAlphaList[i] > growCutOff){this.growLocalLatLonBox(wake[i].getLatLonBox(), metersToLocalLat * 4 * deltaTime, this.metersToLocalLon * 4 * deltaTime);}
                    } else { wakeAlphaList[i] = 0;}
                }
            }   
        }
        this.setBowPosition = function(deltaTime){
            if (this.bowImageURL != ''){
                //no bow when very slow
                //if time has pased more last placed to ship bow
                var distanceTraveled = this.speed * deltaTime;
                var wakeAlphaTarget = this.speed/10;
                bowDistance += distanceTraveled;
                if (bowDistance > this.bowSize*4){
                    bowIndex -= 1;
                    if (bowIndex < 0){bowIndex = bow.length;}
                    this.bowPoint = this.getlocalBDLocation(this.model.getLocation(), this.heading + 180, this.bowOffset);
                    this.setLocalLatLonBox(bow[bowIndex].getLatLonBox(),this.bowPoint.latitude,this.bowPoint.longitude,this.bowSize,this.bowSize,this.track);
                    bowAlphaList[bowIndex] = wakeAlphaTarget;
                    bow[bowIndex].getColor().setA(wakeAlphaTarget * 256);
                    if (bowAlphaList[bowIndex] > 1) {bowAlphaList[bowIndex] = 1;}
                    bowDistance = 0;
                }
                var growCutOff = wakeAlphaTarget * 0.2;
                var fadeFactor = Math.max(distanceTraveled / (this.bowSize * 60), deltaTime/60);
                for (var i=bowIndex; i<bow.length; i++) {
                    bowAlphaList[i] -= fadeFactor; //Fade bow over 60 seconds
                    if (bowAlphaList[i]<0) {break;}
                    bow[i].getColor().setA(bowAlphaList[i] * 256);
                    if (bowAlphaList[i] > growCutOff){
                        this.growLocalLatLonBox(bow[i].getLatLonBox(), this.metersToLocalLon * 3 * deltaTime, metersToLocalLat * 13 * deltaTime);
                    }
                } 
                for (i=0; i<bow.length; i++) {
                    if (bowAlphaList[i]>0) {
                        bowAlphaList[i] -= fadeFactor;
                        bow[i].getColor().setA(bowAlphaList[i] * 256);
                        if (bowAlphaList[i] > growCutOff){
                            this.growLocalLatLonBox(bow[i].getLatLonBox(), this.metersToLocalLon * 3 * deltaTime, metersToLocalLat * 13 * deltaTime);
                        }
                    } else { bowAlphaList[i] = 0;}
                }   
            } 
        }
        this.cleanupWake = function(){
            for (i=0; i<wakeAlphaList.length; i++) { wakeAlphaList[i] = 0.0001; }
        }
        this.cleanupBow = function(){
            for (i=0; i<bowAlphaList.length; i++) { bowAlphaList[i] = 0.0001; }
        }
        this.simulateStep = function (deltaTime){
            //pass inputs to ship control parameters
            var shaftPower = this.leftPowerValue * this.capableEnginePower;
            this.rudderCurrent += (this.rudderTarget - this.rudderCurrent) * deltaTime;
            this.speed += (((1-Math.abs(this.rudderCurrent)/90)*shaftPower)/this.mass) * deltaTime;
            this.turnRate += ((this.rudderCurrent * shaftPower)/(this.mass * this.turnInertia)) * deltaTime;
            if (this.turnRate > 45){this.turnRate=45;}
            else if (this.turnRate<-45){this.turnRate=-45;}
            this.deltaHeading = this.turnRate*deltaTime;
            this.heading += this.deltaHeading;
            if (this.heading > 360){this.heading -= 360} 
            if (this.heading < -360){this.heading += 360}
            this.track = this.heading;
            var radheading = degreesToRad * this.heading;
            var shipLongitude = this.model.getLocation().getLongitude();
            var shipLatitude = this.model.getLocation().getLatitude();
            this.latstep = Math.cos(radheading) * deltaTime * this.speed * metersToLocalLat;
            this.longstep = Math.sin(radheading) * deltaTime * this.speed * metersToLocalLat / this.cosLatitude;
            this.model.getOrientation().setHeading(this.heading);
            var mylon = shipLongitude + this.longstep;
            var mylat = shipLatitude + this.latstep;
            this.model.getLocation().setLongitude(mylon);
            this.model.getLocation().setLatitude(mylat);
            this.model.setAltitudeMode(ge.ALTITUDE_RELATIVE_TO_GROUND);
            this.model.getLocation().setAltitude(this.draft);
            var oldAltitude = this.altitude;     
            this.altitude = Math.max(0,ge.getGlobe().getGroundAltitude(mylat, mylon));         
            this.deltaAltitude = this.altitude - oldAltitude;
            if (this.speed > 0){ this.speed -= this.speed * this.speed * this.moveFriction * deltaTime; }
            else { this.speed += this.speed * this.speed * this.moveFriction * 4 * deltaTime; }
            this.turnRate -= this.turnRate * this.turnFriction * deltaTime;
        }                   
        this.updateFuel = function(deltaTime){
            //fuel usage on both engines. This is a linear use pattern. Real world would be exponential fuel use at full power 
            this.fuel -= this.leftPowerValue * deltaTime * fuelFactor;
            if (this.fuel < 0){this.fuel = 0; this.capableEnginePower = 0}
            else {this.capableEnginePower = this.potentialEnginePower;}              
        }
        this.updateShadow = function(){
            var shadowLocation = this.getlocalBDLocation(this.model.getLocation(), this.shadowAngle, this.shadowOffset);
            this.setLocalLatLonBox(this.shadow.getLatLonBox(), shadowLocation.latitude, shadowLocation.longitude, this.shadowLength, this.shadowWidth, this.heading);
        }
        this.initialise = function(){
            initialising = true;
            this.setFuelCapacity(this.fuelCapacity);
            this.radLatitude = this.model.getLocation().getLatitude() * degreesToRad;
            this.cosLatitude = Math.cos(this.radLatitude); 
            this.metersToLocalLon = metersToLocalLat/this.cosLatitude;              
            this.shadow = this.createLocalOverlay('shadow'+this.id, this.model.getLocation().getLatitude(), this.model.getLocation().getLongitude(), this.shadowWidth, this.shadowLength, this.heading, this.shadowImageURL);
            this.shadow.getColor().setA(Math.round(this.shadowAlpha * 255));
            this.updateLow(0.001);
            this.update(0.001);           
            this.createBowArray();
            this.createWakeArray();          
            initialising = false;
        }
    }
