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