diff --git a/app/src/main/java/com/rtbishop/look4sat/predict4kotlin/PassPredictor.kt b/app/src/main/java/com/rtbishop/look4sat/predict4kotlin/PassPredictor.kt new file mode 100644 index 00000000..3d107d3e --- /dev/null +++ b/app/src/main/java/com/rtbishop/look4sat/predict4kotlin/PassPredictor.kt @@ -0,0 +1,239 @@ +/** + * predict4java: An SDP4 / SGP4 library for satellite orbit predictions + * + * Copyright (C) 2004-2010 David A. B. Johnson, G4DPZ. + * + * This class is a Java port of one of the core elements of + * the Predict program, Copyright John A. Magliacane, + * KD2BD 1991-2003: http://www.qsl.net/kd2bd/predict.html + * + * Dr. T.S. Kelso is the author of the SGP4/SDP4 orbital models, + * originally written in Fortran and Pascal, and released into the + * public domain through his website (http://www.celestrak.com/). + * Neoklis Kyriazis, 5B4AZ, later re-wrote Dr. Kelso's code in C, + * and released it under the GNU GPL in 2002. + * PREDICT's core is based on 5B4AZ's code translation efforts. + * + * Author: David A. B. Johnson, G4DPZ @g4dpz.me.uk> + * + * Comments, questions and bugreports should be submitted via + * http://sourceforge.net/projects/websat/ + * More details can be found at the project home page: + * + * http://websat.sourceforge.net + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, visit http://www.fsf.org/ + */ +package com.rtbishop.look4sat.predict4kotlin + +import com.github.amsacode.predict4java.* +import java.util.* + +class PassPredictor(private val tle: TLE, private val qth: GroundStationPosition) { + private val sat: Satellite = SatelliteFactory.createSatellite(tle) + private val south = "south" + private val north = "north" + private val deadSpotNone = "none" + private val speedOfLight = 2.99792458E8 + private val twoPi = Math.PI * 2.0 + private val timeZone: TimeZone = TimeZone.getTimeZone("UTC") + private var iterationCount = 0 + + fun getDownlinkFreq(freq: Long, date: Date): Long { + val cal = Calendar.getInstance(timeZone).apply { + clear() + timeInMillis = date.time + } + val satPos = getSatPos(cal.time) + val rangeRate = satPos.rangeRate + return (freq.toDouble() * (speedOfLight - rangeRate * 1000.0) / speedOfLight).toLong() + } + + fun getUplinkFreq(freq: Long, date: Date): Long { + val cal = Calendar.getInstance(timeZone).apply { + clear() + timeInMillis = date.time + } + val satPos = getSatPos(cal.time) + val rangeRate = satPos.rangeRate + return (freq.toDouble() * (speedOfLight + rangeRate * 1000.0) / speedOfLight).toLong() + } + + fun getSatPos(time: Date): SatPos { + iterationCount++ + return sat.getPosition(qth, time) + } + + fun getPositions( + referenceDate: Date, + incrementSeconds: Int, + minutesBefore: Int, + minutesAfter: Int + ): List { + val positions: MutableList = ArrayList() + val endDateDate = Date(referenceDate.time + minutesAfter * 60L * 1000L) + var trackDate = Date(referenceDate.time - minutesBefore * 60L * 1000L) + + while (trackDate.before(endDateDate)) { + positions.add(getSatPos(trackDate)) + trackDate = Date(trackDate.time + incrementSeconds * 1000) + } + + return positions + } + + fun getPasses(start: Date, hoursAhead: Int, windBack: Boolean): List { + iterationCount = 0 + + val passes: MutableList = ArrayList() + val trackEndDate = Date(start.time + hoursAhead * 60L * 60L * 1000L) + var trackStartDate = start + var windBackTime = windBack + var lastAOS: Date + var count = 0 + + do { + if (count > 0) windBackTime = false + val pass = nextSatPass(trackStartDate, windBackTime) + lastAOS = pass.startTime + passes.add(pass) + trackStartDate = Date(pass.endTime.time + threeQuarterOrbitMinutes() * 60L * 1000L) + count++ + } while (lastAOS < trackEndDate) + + return passes + } + + private fun nextSatPass(date: Date, windBack: Boolean = false): SatPassTime { + val aosAzimuth: Int + val losAzimuth: Int + var maxElevation = 0.0 + var elevation: Double + var prevPos: SatPos + var tca: Date? = null + var polePassed = deadSpotNone + + // get the current position + val cal = Calendar.getInstance(timeZone).apply { + clear() + timeInMillis = date.time + } + + // wind back time 1/4 of an orbit + if (windBack) { + val meanMotion = tle.meanmo + cal.add(Calendar.MINUTE, (-24.0 * 60.0 / meanMotion / 4.0).toInt()) + } + var satPos = getSatPos(cal.time) + + // test for the elevation being above the horizon + if (satPos.elevation > 0.0) { + // move time forward in 30 second intervals until the sat goes below the horizon + do { + satPos = getPosition(cal, 60) + } while (satPos.elevation > 0.0) + // move time forward 3/4 orbit + cal.add(Calendar.MINUTE, threeQuarterOrbitMinutes()) + } + + // now find the next time it comes above the horizon + do { + satPos = getPosition(cal, 60) + val now = cal.time + elevation = satPos.elevation + if (elevation > maxElevation) { + maxElevation = elevation + tca = now + } + } while (satPos.elevation < 0.0) + + // refine it to 5 seconds + cal.add(Calendar.SECOND, -60) + do { + satPos = getPosition(cal, 5) + val now = cal.time + elevation = satPos.elevation + if (elevation > maxElevation) { + maxElevation = elevation + tca = now + } + prevPos = satPos + } while (satPos.elevation < 0.0) + val startDate = satPos.time + aosAzimuth = (satPos.azimuth / (2.0 * Math.PI) * 360.0).toInt() + + // now find when it goes below + do { + satPos = getPosition(cal, 30) + val now = cal.time + val currPolePassed = getPolePassed(prevPos, satPos) + if (currPolePassed != deadSpotNone) { + polePassed = currPolePassed + } + elevation = satPos.elevation + if (elevation > maxElevation) { + maxElevation = elevation + tca = now + } + prevPos = satPos + } while (satPos.elevation > 0.0) + + // refine it to 5 seconds + cal.add(Calendar.SECOND, -30) + do { + satPos = getPosition(cal, 5) + val now = cal.time + elevation = satPos.elevation + if (elevation > maxElevation) { + maxElevation = elevation + tca = now + } + } while (satPos.elevation > 0.0) + + val endDate = satPos.time + losAzimuth = (satPos.azimuth / (2.0 * Math.PI) * 360.0).toInt() + + return SatPassTime( + startDate, endDate, tca, polePassed, aosAzimuth, + losAzimuth, maxElevation / (2.0 * Math.PI) * 360.0 + ) + } + + private fun getPosition(cal: Calendar, offSet: Int): SatPos { + cal.add(Calendar.SECOND, offSet) + return getSatPos(cal.time) + } + + private fun getPolePassed(prevPos: SatPos, satPos: SatPos): String { + var polePassed = deadSpotNone + val az1 = prevPos.azimuth / twoPi * 360.0 + val az2 = satPos.azimuth / twoPi * 360.0 + if (az1 > az2) { // we may be moving from 350 or greater through north + if (az1 > 350 && az2 < 10) polePassed = north + else { // we may be moving from 190 or greater through south + if (az1 > 180 && az2 < 180) polePassed = south + } + } else { // we may be moving from 10 or less through north + if (az1 < 10 && az2 > 350) polePassed = north + else { // we may be moving from 170 or more through south + if (az1 < 180 && az2 > 180) polePassed = south + } + } + return polePassed + } + + private fun threeQuarterOrbitMinutes(): Int { + return (24.0 * 60.0 / tle.meanmo * 0.75).toInt() + } +} \ No newline at end of file