├── .github └── workflows │ ├── build.yml │ └── docs.yml ├── .gitignore ├── LICENSE ├── README.md ├── db ├── README.md ├── bigBody.csv ├── feature.csv ├── satellite.csv ├── satelliteHistorical.csv └── satelliteNames.csv ├── demo ├── Anton-Regular.ttf ├── horizonsys.png ├── orbitsDemo.nim └── spkfiles.png ├── nim.cfg ├── orbits.nimble ├── src ├── orbits.nim └── orbits │ ├── astrometry.nim │ ├── brightstars.json │ ├── elements.json │ ├── featuredb.nim │ ├── horizon.nim │ ├── simple.nim │ ├── simulate.nim │ ├── solarbodydb.nim │ ├── solvers.nim │ ├── spk.nim │ ├── spkfiles.txt │ ├── tle.nim │ └── utils.nim └── tests ├── all.nim ├── bigdipper.png ├── bigdipper_stars.png ├── brightstars3d.png ├── earthToMarsSim.nim ├── earthToMarsSim.png ├── generateOrbitalElements.nim ├── generateStats.nim ├── generatedOE.png ├── matching.png ├── merkuryPrecession.nim ├── merkuryPrecession.png ├── milkyway.png ├── nim.cfg ├── orbitsHorizon.nim ├── orbitsHorizon.png ├── orbitsSimple.nim ├── orbitsSimple.png ├── orbitsSimulated.nim ├── orbitsSimulated.png ├── orbitsSpk.nim ├── orbitsSpk.png ├── solarsystemSpk.nim ├── solvertest.nim ├── spacecraft.nim ├── spacecraft.png ├── starman.nim ├── starman.png ├── starman2.nim ├── test.c └── test.nim /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Github Actions 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | strategy: 6 | fail-fast: false 7 | matrix: 8 | os: [ubuntu-latest, windows-latest] 9 | 10 | runs-on: ${{ matrix.os }} 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: jiro4989/setup-nim-action@v1 15 | with: 16 | repo-token: ${{ secrets.GITHUB_TOKEN }} 17 | - run: nimble test -y 18 | - run: nimble test --gc:orc -y 19 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | on: 3 | push: 4 | branches: 5 | - master 6 | env: 7 | nim-version: 'stable' 8 | nim-src: src/${{ github.event.repository.name }}.nim 9 | deploy-dir: .gh-pages 10 | jobs: 11 | docs: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: jiro4989/setup-nim-action@v1 16 | with: 17 | nim-version: ${{ env.nim-version }} 18 | - run: nimble install -Y 19 | - run: nimble doc --index:on --project --git.url:https://github.com/${{ github.repository }} --git.commit:master --out:${{ env.deploy-dir }} ${{ env.nim-src }} 20 | - name: "Copy to index.html" 21 | run: cp ${{ env.deploy-dir }}/${{ github.event.repository.name }}.html ${{ env.deploy-dir }}/index.html 22 | - name: Deploy documents 23 | uses: peaceiris/actions-gh-pages@v3 24 | with: 25 | github_token: ${{ secrets.GITHUB_TOKEN }} 26 | publish_dir: ${{ env.deploy-dir }} 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore files with no extention: 2 | * 3 | !*/ 4 | !*.* 5 | 6 | # normal ignores: 7 | *.exe 8 | nimcache 9 | cache 10 | *.bsp 11 | 12 | # files to big! 13 | smallBody.csv -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT 2 | 3 | Copyright 2018 Andre von Houck 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Orbits - Orbital Mechanics Library for Nim. 2 | 3 | `nimble install orbits` 4 | 5 | ![Github Actions](https://github.com/treeform/orbits/workflows/Github%20Actions/badge.svg) 6 | 7 | [API reference](https://treeform.github.io/orbits) 8 | 9 | 10 | Orbits are beautiful, with their gentle curves and sweeping arcs. They have captivated astronomers for thousands of years and have made mathematics and physics to what they are today. There is so much to the orbits if you dig dipper. Where do you get most accurate data? How do you compute transfer windows? Can you plot trajectories of spacecraft? This is what this library is about. An exploration of deep beyond with Nim at your side. 11 | 12 | # Simple Elliptical Orbits. 13 | 14 | Let's start out with some simple orbits that follow the simplest elliptical model. Here is how you can get position of the planets at any time and plot it: 15 | 16 | 17 | 18 | ```nim 19 | for planet in simpleElements: 20 | var step = planet.period / 360 21 | for i in 0..360: 22 | let pos = planet.posAt(step * float(i)) / AU 23 | ctx.lineTo(pos.x, pos.y) 24 | ctx.stroke() 25 | ``` 26 | 27 | You do this using `orbital elements`. The minimum number of orbital elements you need are: 28 | 29 | * o: Longitude of the ascending node 30 | * i: Inclination 31 | * w: Argument of periaps 32 | * a: Semi-major axis, or mean distance from Sun 33 | * e: Eccentricity (0=circle, 0-1=ellipse, 1=parabola) 34 | * m: Mean anomaly (0 at perihelion increases uniformly with time) 35 | * n: Mean motion 36 | 37 | If you have exact time, you can turn `orbital elements` into `orbital vectors` which are 38 | 39 | * pos: x, y, z -- position 40 | * vel: x, y, z -- velocity 41 | 42 | Then you can accutally plot `orbital vectors`. 43 | 44 | 45 | # Kernel Files Orbits 46 | 47 | NASA uses [.bsp SPICE SP-kernels](https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/info/intrdctn.html) files which accurately describe the orbits of planets with [chebyshev polynomials](https://en.wikipedia.org/wiki/Chebyshev_polynomials) after folks at JPL have collected all observation and ran them through supercomputer simulations. These orbits can be [downloaded](https://naif.jpl.nasa.gov/pub/naif/generic_kernels/spk/) from NASA and are pretty accurate. Unfortunately they only exist from about 1000 AD to 3000 AD and only the larger bodies of solar system. Smaller bodies that don't have spice kernels need to use orbital elements to extarpolate their simpler elliptical orbits. 48 | 49 | You can find SPK files here: https://naif.jpl.nasa.gov/pub/naif/generic_kernels/spk/ 50 | 51 | ```nim 52 | var spkFile = readSpk("tests/de435.bsp") 53 | for planet in complexElements: 54 | var step = planet.period / 360 55 | for i in 0..360: 56 | let time = step * float(i) 57 | let pos = spkFile.posAt(time, planet.id, 0) / AU 58 | ctx.lineTo(pos.x, pos.y) 59 | ctx.stroke() 60 | ``` 61 | 62 | 63 | 64 | Here you can see [Mercury precession](https://en.wikipedia.org/wiki/Tests_of_general_relativity#Perihelion_precession_of_Mercury). This is drawing 5 full orbit of merkury every 20 earth years. You can see the "wobble" of Merkury's orbit over 100 earth year time span. 65 | 66 | 67 | 68 | 69 | # JPL Horizon Orbits 70 | 71 | If getting data dumps from NASA is not enough you can also connect to NASA servers directly over [TELNET](https://en.wikipedia.org/wiki/Telnet) and query their databases in realtime. You can download a ton of interesting information from the [JPL Horizon System](https://ssd.jpl.nasa.gov/?horizons) you can’t get anywhere else. Thousands of positions of small bodies, asteroids, comets and select spacecraft throughout the solar system. 72 | 73 | 74 | 75 | ```nim 76 | var hz = newHorizonClient() 77 | for planet in simpleElements: 78 | let entries = hz.getOrbitalVectorsSeq( 79 | 0.0, 80 | planet.period, 81 | 360, 82 | planet.id, 0) 83 | for entry in entries: 84 | let pos = entry.pos / AU 85 | ctx.lineTo(pos.x, pos.y) 86 | ctx.closePath() 87 | ctx.stroke() 88 | hz.close() 89 | ``` 90 | 91 | You can get positions of many things, Including spacecraft: 92 | 93 | 94 | 95 | Position of SpaceX's launched Starman riding a Elon's Cherry Tesla with NASA's accuracy. 96 | 97 | 98 | 99 | 100 | # Simulations 101 | 102 | You can also use this to simulate the solar system: 103 | 104 | 105 | 106 | # Astrometry 107 | 108 | You can also draw star fields and compute star positions: 109 | 110 | 111 | 112 | Given an image of a star field you can find the brightest stars and using the stars you can find the patch of sky you are looking at. 113 | 114 | 115 | 116 | Ever though about navigating by the stars? 117 | 118 | 119 | 120 | Finding names of stars from a pictures: 121 | 122 | 123 | -------------------------------------------------------------------------------- /db/README.md: -------------------------------------------------------------------------------- 1 | # Body DB 2 | 3 | This folder contains different datbases for solar system objects. 4 | 5 | * bigBody.csv - major solar system bodies like planets and moons 6 | * smallBody.csv - astroids and commets. 7 | * feature.csv - surface feature names on moons and planets. 8 | * satellite.csv - satellite information from TLE db from NORAD. -------------------------------------------------------------------------------- /db/bigBody.csv: -------------------------------------------------------------------------------- 1 | id,parentId,name,massKg,meanRadiusM,albedo,gm,equatorialRadiusM,densityKgM3,siderealRotationPeriodS,orbitRotationPeriodS,equatorialGravity,escapeVelocityM 2 | 0,0,Solar System Barycenter (SSB),1.989e+030,0.0,0.0,1.327178151e+020,0.0,0.0,0.0,0.0,0.0,0.0, 3 | 10,0,Sun,1.989e+030,695700000.0,0.0,1.327178151e+020,695700000.0,0.0,2164320.0,0.0,0.0,0.0, 4 | 1,1,Mercury Barycenter,3.30114e+023,0.0,0.0,22027153752600.0,0.0,0.0,0.0,7600543.819920001,0.0,0.0, 5 | 2,2,Venus Barycenter,4.86747e+024,0.0,0.0,324786316473000.0,0.0,0.0,0.0,19414149.052176,0.0,0.0, 6 | 3,3,Earth-Moot Barycenter,5.97237e+024,0.0,0.0,398511763382999.9,0.0,0.0,0.0,31558149.10224,0.0,0.0, 7 | 4,4,Mars Barycenter,6.417119999999999e+023,0.0,0.0,42818810740799.99,0.0,0.0,0.0,59355036.22176,0.0,0.0, 8 | 5,5,Jupiter Barycenter,1.898187e+027,0.0,0.0,1.266582359433e+017,0.0,0.0,0.0,374355659.124,0.0,0.0, 9 | 6,6,Saturn Barycenter,5.68336e+026,0.0,0.0,3.79227311024e+016,0.0,0.0,0.0,929292362.8848001,0.0,0.0, 10 | 7,7,Uranus Barycenter,8.68127e+025,0.0,0.0,5792655538930000.0,0.0,0.0,0.0,2651370019.3296,0.0,0.0, 11 | 8,8,Neptune Barycenter,1.024126e+026,0.0,0.0,6833572906339999.0,0.0,0.0,0.0,5200418560.032001,0.0,0.0, 12 | 9,9,Pluto-Charon Barycenter,1.303e+022,0.0,0.0,869438477000.0,0.0,0.0,0.0,7823780704.44,0.0,0.0, 13 | 199,1,Mercury,3.30114e+023,2439400.0,0.106,22027153752600.0,2440530.0,5429.1,5067031.680000001,7600543.819920001,3.7,4250.0, 14 | 299,2,Venus,4.86747e+024,6051800.0,0.65,324786316473000.0,6051800.0,5243.0,-20996755.2,19414149.052176,8.869999999999999,10360.0, 15 | 399,3,Earth,5.97237e+024,6371008.399999999,0.367,398511763382999.9,6378136.6,5513.6,86164.10035200001,31558149.10224,9.800000000000001,11190.0, 16 | 499,4,Mars,6.417119999999999e+023,3389500.0,0.15,42818810740799.99,3396190.0,3934.1,88642.664064,59355036.22176,3.71,5030.0, 17 | 599,5,Jupiter,1.898187e+027,69911000.0,0.52,1.266582359433e+017,71492000.0,1326.2,35729.856,374355659.124,24.79,60200.0, 18 | 699,6,Saturn,5.68336e+026,58232000.0,0.47,3.79227311024e+016,60268000.0,687.1,38362.464,929292362.8848001,10.44,36090.0, 19 | 799,7,Uranus,8.68127e+025,25362000.0,0.51,5792655538930000.0,25559000.0,1270.0,-62063.71200000001,2651370019.3296,8.869999999999999,21380.0, 20 | 899,8,Neptune,1.024126e+026,24622000.0,0.41,6833572906339999.0,24764000.0,1638.0,57995.99999999999,5200418560.032001,11.15,23560.0, 21 | 999,9,Pluto,1.303e+022,1188300.0,0.3,869438477000.0,1188300.0,1890.0,-551854.08,7823780704.44,0.62,1210.0, 22 | 301,3,Moon,7.347673092457352e+022,1737500.0,0.12,4902801000000.0,0.0,3344.0,0.0,0.0,0.0,0.0, 23 | 401,4,Phobos,1.065852989618724e+016,11100.0,0.07099999999999999,711200.0,0.0,1872.0,0.0,0.0,0.0,0.0, 24 | 402,4,Deimos,1476188406600735.0,6200.0,0.06800000000000001,98500.0,0.0,1471.0,0.0,0.0,0.0,0.0, 25 | 501,5,Io,8.931937973110891e+022,1821600.0,0.63,5959916000000.0,0.0,3528.0,0.0,0.0,0.0,0.0, 26 | 502,5,Europa,4.799843838749272e+022,1560800.0,0.67,3202739000000.0,0.0,3013.0,0.0,0.0,0.0,0.0, 27 | 503,5,Ganymede,1.481858468750515e+023,2631200.0,0.43,9887834000000.0,0.0,1942.0,0.0,0.0,0.0,0.0, 28 | 504,5,Callisto,1.075937379638192e+023,2410300.0,0.17,7179289000000.0,0.0,1834.0,0.0,0.0,0.0,0.0, 29 | 505,5,Amalthea,2.068162437674127e+018,83450.0,0.09,138000000.0,0.0,849.0,0.0,0.0,0.0,0.0, 30 | 506,5,Himalia,6.74400794893737e+018,85000.0,0.04,450000000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 31 | 507,5,Elara,8.692276911963721e+017,43000.0,0.04,58000000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 32 | 508,5,Pasiphae,2.997336866194387e+017,30000.0,0.04,20000000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 33 | 509,5,Sinope,7.493342165485966e+016,19000.0,0.04,5000000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 34 | 510,5,Lysithea,6.294407419008211e+016,18000.0,0.04,4200000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 35 | 511,5,Carme,1.31882822112553e+017,23000.0,0.04,8800000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 36 | 512,5,Ananke,2.997336866194386e+016,14000.0,0.04,2000000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 37 | 513,5,Leda,1.094027956160951e+016,10000.0,0.04,730000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 38 | 514,5,Thebe,1.498668433097193e+018,49300.0,0.047,100000000.0,0.0,3000.0,0.0,0.0,0.0,0.0, 39 | 515,5,Adrastea,7493342165485966.0,8200.0,0.1,500000.0,0.0,3000.0,0.0,0.0,0.0,0.0, 40 | 516,5,Metis,1.198934746477755e+017,21500.0,0.061,8000000.0,0.0,3000.0,0.0,0.0,0.0,0.0, 41 | 517,5,Callirrhoe,869227691196372.0,4300.0,0.04,58000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 42 | 518,5,Themisto,689387479224708.9,4000.0,0.04,46000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 43 | 519,5,Megaclite,209813580633607.1,2700.0,0.04,14000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 44 | 520,5,Taygete,164853527640691.3,2500.0,0.04,11000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 45 | 521,5,Chaldene,74933421654859.66,1900.0,0.04,5000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 46 | 522,5,Harpalyke,119893474647775.5,2200.0,0.04,8000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 47 | 523,5,Kalyke,194826896302635.1,2600.0,0.04,13000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 48 | 524,5,Iocaste,194826896302635.1,2600.0,0.04,13000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 49 | 525,5,Erinome,44960052992915.8,1600.0,0.04,3000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 50 | 526,5,Isonoe,74933421654859.66,1900.0,0.04,5000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 51 | 527,5,Praxidike,434613845598186.0,3400.0,0.04,29000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 52 | 528,5,Autonoe,89920105985831.59,2000.0,0.04,6000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 53 | 529,5,Thyone,89920105985831.59,2000.0,0.04,6000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 54 | 530,5,Hermippe,89920105985831.59,2000.0,0.04,6000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 55 | 531,5,Aitne,44960052992915.8,1500.0,0.04,3000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 56 | 532,5,Eurydome,44960052992915.8,1500.0,0.04,3000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 57 | 533,5,Euanthe,44960052992915.8,1500.0,0.04,3000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 58 | 534,5,Euporie,14986684330971.93,1000.0,0.04,1000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 59 | 535,5,Orthosie,14986684330971.93,1000.0,0.04,1000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 60 | 536,5,Sponde,14986684330971.93,1000.0,0.04,1000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 61 | 537,5,Kale,14986684330971.93,1000.0,0.04,1000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 62 | 538,5,Pasithee,14986684330971.93,1000.0,0.04,1000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 63 | 539,5,Hegemone,44960052992915.8,1500.0,0.04,3000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 64 | 540,5,Mneme,14986684330971.93,1000.0,0.04,1000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 65 | 541,5,Aoede,89920105985831.59,2000.0,0.04,6000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 66 | 542,5,Thelxinoe,14986684330971.93,1000.0,0.04,1000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 67 | 543,5,Arche,44960052992915.8,1500.0,0.04,3000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 68 | 544,5,Kallichore,14986684330971.93,1000.0,0.04,1000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 69 | 545,5,Helike,89920105985831.59,2000.0,0.04,6000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 70 | 546,5,Carpo,44960052992915.8,1500.0,0.04,3000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 71 | 547,5,Eukelade,89920105985831.59,2000.0,0.04,6000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 72 | 548,5,Cyllene,14986684330971.93,1000.0,0.04,1000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 73 | 549,5,Kore,14986684330971.93,1000.0,0.04,1000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 74 | 550,5,Herse,14986684330971.93,1000.0,0.04,1000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 75 | 551,5,S/2000 J11,14986684330971.93,1000.0,0.04,1000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 76 | 552,5,S/2003 J2,14986684330971.93,1000.0,0.04,1000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 77 | 553,5,S/2003 J3,14986684330971.93,1000.0,0.04,1000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 78 | 554,5,S/2003 J4,14986684330971.93,1000.0,0.04,1000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 79 | 555,5,S/2003 J5,89920105985831.59,2000.0,0.04,6000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 80 | 556,5,S/2003 J9,1498668433097.193,500.0,0.04,100.0,0.0,2600.0,0.0,0.0,0.0,0.0, 81 | 557,5,S/2003 J10,14986684330971.93,1000.0,0.04,1000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 82 | 558,5,S/2003 J12,1498668433097.193,500.0,0.04,100.0,0.0,2600.0,0.0,0.0,0.0,0.0, 83 | 559,5,S/2003 J15,14986684330971.93,1000.0,0.04,1000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 84 | 560,5,S/2003 J16,14986684330971.93,1000.0,0.04,1000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 85 | 561,5,S/2003 J18,14986684330971.93,1000.0,0.04,1000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 86 | 562,5,S/2003 J19,14986684330971.93,1000.0,0.04,1000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 87 | 563,5,S/2003 J23,14986684330971.93,1000.0,0.04,1000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 88 | 564,5,S/2010 J1,14986684330971.93,1000.0,0.04,1000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 89 | 565,5,S/2010 J2,14986684330971.93,1000.0,0.04,1000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 90 | 566,5,S/2011 J1,14986684330971.93,1000.0,0.04,1000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 91 | 567,5,S/2011 J2,14986684330971.93,1000.0,0.04,1000.0,0.0,2600.0,0.0,0.0,0.0,0.0, 92 | 601,6,Mimas,3.750567620669036e+019,198200.0,0.962,2502600000.0,0.0,1150.0,0.0,0.0,0.0,0.0, 93 | 602,6,Enceladus,1.079445912306915e+020,252100.0,1.375,7202700000.0,0.0,1608.0,0.0,0.0,0.0,0.0, 94 | 603,6,Tethys,6.175518052210611e+020,533000.0,1.229,41206700000.0,0.0,973.0,0.0,0.0,0.0,0.0, 95 | 604,6,Dione,1.09574543018528e+021,561700.0,0.998,73114600000.0,0.0,1476.0,0.0,0.0,0.0,0.0, 96 | 605,6,Rhea,2.30708915128908e+021,764300.0,0.949,153942600000.0,0.0,1233.0,0.0,0.0,0.0,0.0, 97 | 606,6,Titan,1.345525230832405e+023,2574730.0,0.2,8978138200000.0,0.0,1882.0,0.0,0.0,0.0,0.0, 98 | 607,6,Hyperion,5.58553725015324e+018,135000.0,0.3,372700000.0,0.0,544.0,0.0,0.0,0.0,0.0, 99 | 608,6,Iapetus,1.805952411282576e+021,735600.0,0.6,120503800000.0,0.0,1083.0,0.0,0.0,0.0,0.0, 100 | 609,6,Phoebe,8.290633771893673e+018,106500.0,0.081,553200000.0,0.0,1638.0,0.0,0.0,0.0,0.0, 101 | 610,6,Janus,1.892818231001755e+018,89500.0,0.71,126300000.0,0.0,630.0,0.0,0.0,0.0,0.0, 102 | 611,6,Epimetheus,5.260326200171148e+017,58100.0,0.73,35100000.0,0.0,640.0,0.0,0.0,0.0,0.0, 103 | 612,6,Helene,1.138988009153867e+016,17600.0,1.67,760000.0,0.0,500.0,0.0,0.0,0.0,0.0, 104 | 613,6,Telesto,4046404769362422.0,12400.0,1.0,270000.0,0.0,500.0,0.0,0.0,0.0,0.0, 105 | 614,6,Calypso,2547736336265229.0,10700.0,1.34,170000.0,0.0,500.0,0.0,0.0,0.0,0.0, 106 | 615,6,Atlas,6594141105627650.0,15100.0,0.4,440000.0,0.0,460.0,0.0,0.0,0.0,0.0, 107 | 616,6,Prometheus,1.609569897146386e+017,43100.0,0.6,10740000.0,0.0,480.0,0.0,0.0,0.0,0.0, 108 | 617,6,Pandora,1.384769632181807e+017,40700.0,0.5,9240000.0,0.0,490.0,0.0,0.0,0.0,0.0, 109 | 618,6,Pan,4945605829220738.0,14100.0,0.5,330000.0,0.0,420.0,0.0,0.0,0.0,0.0, 110 | 619,6,Methone,8992010598583.16,1600.0,0.0,600.0,0.0,500.0,0.0,0.0,0.0,0.0, 111 | 620,6,Pallene,32970705528138.25,2500.0,0.0,2200.0,0.0,500.0,0.0,0.0,0.0,0.0, 112 | 621,6,Polydeuces,4496005299291.58,1300.0,0.0,300.0,0.0,500.0,0.0,0.0,0.0,0.0, 113 | 622,6,Daphnis,77930758521054.05,3800.0,0.0,5200.0,0.0,340.0,0.0,0.0,0.0,0.0, 114 | 623,6,Anthe,1498668433097.193,900.0,0.0,100.0,0.0,500.0,0.0,0.0,0.0,0.0, 115 | 624,6,Aegaeon,59946737323.88773,300.0,0.0,4.0,0.0,500.0,0.0,0.0,0.0,0.0, 116 | 625,6,Ymir,4945605829220738.0,9000.0,0.06,330000.0,0.0,2300.0,0.0,0.0,0.0,0.0, 117 | 626,6,Paaliaq,8242676382034563.0,11000.0,0.06,550000.0,0.0,2300.0,0.0,0.0,0.0,0.0, 118 | 627,6,Tarvos,2697603179574948.0,7500.0,0.06,180000.0,0.0,2300.0,0.0,0.0,0.0,0.0, 119 | 628,6,Ijiraq,1198934746477755.0,6000.0,0.06,80000.0,0.0,2300.0,0.0,0.0,0.0,0.0, 120 | 629,6,Suttungr,209813580633607.1,3500.0,0.06,14000.0,0.0,2300.0,0.0,0.0,0.0,0.0, 121 | 630,6,Kiviuq,3297070552813825.0,8000.0,0.06,220000.0,0.0,2300.0,0.0,0.0,0.0,0.0, 122 | 631,6,Mundilfari,209813580633607.1,3500.0,0.06,14000.0,0.0,2300.0,0.0,0.0,0.0,0.0, 123 | 632,6,Albiorix,2.09813580633607e+016,16000.0,0.06,1400000.0,0.0,2300.0,0.0,0.0,0.0,0.0, 124 | 633,6,Skathi,314720370950410.6,4000.0,0.06,21000.0,0.0,2300.0,0.0,0.0,0.0,0.0, 125 | 634,6,Erriapus,764320900879568.5,5000.0,0.06,51000.0,0.0,2300.0,0.0,0.0,0.0,0.0, 126 | 635,6,Siarnaq,3.896537926052702e+016,20000.0,0.06,2600000.0,0.0,2300.0,0.0,0.0,0.0,0.0, 127 | 636,6,Thrymr,209813580633607.1,3500.0,0.06,14000.0,0.0,2300.0,0.0,0.0,0.0,0.0, 128 | 637,6,Narvi,344693739612354.4,3500.0,0.04,23000.0,0.0,2300.0,0.0,0.0,0.0,0.0, 129 | 638,6,Aegir,0.0,3000.0,0.04,0.0,0.0,2300.0,0.0,0.0,0.0,0.0, 130 | 639,6,Bebhionn,0.0,3000.0,0.04,0.0,0.0,2300.0,0.0,0.0,0.0,0.0, 131 | 640,6,Bergelmir,0.0,3000.0,0.04,0.0,0.0,2300.0,0.0,0.0,0.0,0.0, 132 | 641,6,Bestla,0.0,3500.0,0.04,0.0,0.0,2300.0,0.0,0.0,0.0,0.0, 133 | 642,6,Farbauti,0.0,2500.0,0.04,0.0,0.0,2300.0,0.0,0.0,0.0,0.0, 134 | 643,6,Fenrir,0.0,2000.0,0.04,0.0,0.0,2300.0,0.0,0.0,0.0,0.0, 135 | 644,6,Fornjot,0.0,3000.0,0.04,0.0,0.0,2300.0,0.0,0.0,0.0,0.0, 136 | 645,6,Hati,0.0,3000.0,0.04,0.0,0.0,2300.0,0.0,0.0,0.0,0.0, 137 | 646,6,Hyrrokkin,0.0,3000.0,0.04,0.0,0.0,2300.0,0.0,0.0,0.0,0.0, 138 | 647,6,Kari,0.0,3000.0,0.04,0.0,0.0,2300.0,0.0,0.0,0.0,0.0, 139 | 648,6,Loge,0.0,3000.0,0.04,0.0,0.0,2300.0,0.0,0.0,0.0,0.0, 140 | 649,6,Skoll,0.0,3000.0,0.04,0.0,0.0,2300.0,0.0,0.0,0.0,0.0, 141 | 650,6,Surtur,0.0,3000.0,0.04,0.0,0.0,2300.0,0.0,0.0,0.0,0.0, 142 | 651,6,Jarnsaxa,0.0,3000.0,0.04,0.0,0.0,2300.0,0.0,0.0,0.0,0.0, 143 | 652,6,Greip,0.0,3000.0,0.04,0.0,0.0,2300.0,0.0,0.0,0.0,0.0, 144 | 653,6,Tarqeq,0.0,3000.0,0.04,0.0,0.0,2300.0,0.0,0.0,0.0,0.0, 145 | 654,6,S/2004 S7,0.0,3000.0,0.04,0.0,0.0,2300.0,0.0,0.0,0.0,0.0, 146 | 655,6,S/2004 S12,0.0,2500.0,0.04,0.0,0.0,2300.0,0.0,0.0,0.0,0.0, 147 | 656,6,S/2004 S13,0.0,3000.0,0.04,0.0,0.0,2300.0,0.0,0.0,0.0,0.0, 148 | 657,6,S/2004 S17,0.0,2000.0,0.04,0.0,0.0,2300.0,0.0,0.0,0.0,0.0, 149 | 658,6,S/2006 S1,0.0,3000.0,0.04,0.0,0.0,2300.0,0.0,0.0,0.0,0.0, 150 | 659,6,S/2006 S3,0.0,2500.0,0.04,0.0,0.0,2300.0,0.0,0.0,0.0,0.0, 151 | 660,6,S/2007 S2,0.0,3000.0,0.04,0.0,0.0,2300.0,0.0,0.0,0.0,0.0, 152 | 661,6,S/2007 S3,0.0,2000.0,0.04,0.0,0.0,2300.0,0.0,0.0,0.0,0.0, 153 | 701,7,Ariel,1.294849526195975e+021,578900.0,0.39,86400000000.0,0.0,1592.0,0.0,0.0,0.0,0.0, 154 | 702,7,Umbriel,1.221414772974212e+021,584700.0,0.21,81500000000.0,0.0,1459.0,0.0,0.0,0.0,0.0, 155 | 703,7,Titania,3.419961364327795e+021,788900.0,0.27,228200000000.0,0.0,1662.0,0.0,0.0,0.0,0.0, 156 | 704,7,Oberon,2.883438065279e+021,761400.0,0.23,192400000000.0,0.0,1559.0,0.0,0.0,0.0,0.0, 157 | 705,7,Miranda,6.594141105627651e+019,235800.0,0.32,4400000000.0,0.0,1214.0,0.0,0.0,0.0,0.0, 158 | 706,7,Cordelia,4.49600529929158e+016,20100.0,0.07000000000000001,3000000.0,0.0,1300.0,0.0,0.0,0.0,0.0, 159 | 707,7,Ophelia,5.395206359149895e+016,21400.0,0.07000000000000001,3600000.0,0.0,1300.0,0.0,0.0,0.0,0.0, 160 | 708,7,Bianca,9.291744285202598e+016,27000.0,0.065,6200000.0,0.0,1300.0,0.0,0.0,0.0,0.0, 161 | 709,7,Cressida,3.431950711792572e+017,41000.0,0.06900000000000001,22900000.0,0.0,1300.0,0.0,0.0,0.0,0.0, 162 | 710,7,Desdemona,1.78341543538566e+017,35000.0,0.08400000000000001,11900000.0,0.0,1300.0,0.0,0.0,0.0,0.0, 163 | 711,7,Juliet,5.575046571121559e+017,53000.0,0.075,37200000.0,0.0,1300.0,0.0,0.0,0.0,0.0, 164 | 712,7,Portia,1.681505981935051e+018,70000.0,0.06900000000000001,112200000.0,0.0,1300.0,0.0,0.0,0.0,0.0, 165 | 713,7,Rosalind,2.547736336265229e+017,36000.0,0.072,17000000.0,0.0,1300.0,0.0,0.0,0.0,0.0, 166 | 714,7,Belinda,3.56683087077132e+017,45000.0,0.067,23800000.0,0.0,1300.0,0.0,0.0,0.0,0.0, 167 | 715,7,Puck,2.89392874431068e+018,81000.0,0.104,193100000.0,0.0,1300.0,0.0,0.0,0.0,0.0, 168 | 716,7,Caliban,2.997336866194387e+017,36000.0,0.04,20000000.0,0.0,1500.0,0.0,0.0,0.0,0.0, 169 | 717,7,Sycorax,2.697603179574948e+018,75000.0,0.04,180000000.0,0.0,1500.0,0.0,0.0,0.0,0.0, 170 | 718,7,Prospero,9.891211658441475e+016,25000.0,0.04,6600000.0,0.0,1500.0,0.0,0.0,0.0,0.0, 171 | 719,7,Setebos,8.69227691196372e+016,24000.0,0.04,5800000.0,0.0,1500.0,0.0,0.0,0.0,0.0, 172 | 720,7,Stephano,2.547736336265228e+016,16000.0,0.04,1700000.0,0.0,1500.0,0.0,0.0,0.0,0.0, 173 | 721,7,Trinculo,4645872142601299.0,9000.0,0.04,310000.0,0.0,1500.0,0.0,0.0,0.0,0.0, 174 | 722,7,Francisco,8392543225344282.0,11000.0,0.04,560000.0,0.0,1500.0,0.0,0.0,0.0,0.0, 175 | 723,7,Margaret,6294407419008211.0,10000.0,0.04,420000.0,0.0,1500.0,0.0,0.0,0.0,0.0, 176 | 724,7,Ferdinand,6294407419008211.0,10000.0,0.04,420000.0,0.0,1500.0,0.0,0.0,0.0,0.0, 177 | 725,7,Perdita,1.798402119716632e+016,13000.0,0.07000000000000001,1200000.0,0.0,1300.0,0.0,0.0,0.0,0.0, 178 | 726,7,Mab,8992010598583159.0,12000.0,0.103,600000.0,0.0,1300.0,0.0,0.0,0.0,0.0, 179 | 727,7,Cupid,2997336866194387.0,9000.0,0.07000000000000001,200000.0,0.0,1300.0,0.0,0.0,0.0,0.0, 180 | 801,8,Triton,2.139499055089553e+022,1353400.0,0.719,1427600000000.0,0.0,2059.0,0.0,0.0,0.0,0.0, 181 | 802,8,Nereid,3.087256972180218e+019,170000.0,0.155,2060000000.0,0.0,1500.0,0.0,0.0,0.0,0.0, 182 | 803,8,Naiad,1.948268963026351e+017,33000.0,0.072,13000000.0,0.0,1300.0,0.0,0.0,0.0,0.0, 183 | 804,8,Thalassa,3.746671082742983e+017,41000.0,0.091,25000000.0,0.0,1300.0,0.0,0.0,0.0,0.0, 184 | 805,8,Despina,2.09813580633607e+018,75000.0,0.09,140000000.0,0.0,1300.0,0.0,0.0,0.0,0.0, 185 | 806,8,Galatea,3.746671082742983e+018,88000.0,0.079,250000000.0,0.0,1300.0,0.0,0.0,0.0,0.0, 186 | 807,8,Larissa,4.945605829220738e+018,97000.0,0.091,330000000.0,0.0,1300.0,0.0,0.0,0.0,0.0, 187 | 808,8,Proteus,5.03552593520657e+019,210000.0,0.096,3360000000.0,0.0,1300.0,0.0,0.0,0.0,0.0, 188 | 809,8,Halimede,1.798402119716632e+017,31000.0,0.04,12000000.0,0.0,1500.0,0.0,0.0,0.0,0.0, 189 | 810,8,Psamathe,4.945605829220738e+016,20000.0,0.04,3300000.0,0.0,1500.0,0.0,0.0,0.0,0.0, 190 | 811,8,Sao,6.74400794893737e+016,22000.0,0.04,4500000.0,0.0,1500.0,0.0,0.0,0.0,0.0, 191 | 812,8,Laomedeia,5.844806889079054e+016,21000.0,0.04,3900000.0,0.0,1500.0,0.0,0.0,0.0,0.0, 192 | 813,8,Neso,1.648535276406913e+017,30000.0,0.04,11000000.0,0.0,1500.0,0.0,0.0,0.0,0.0, 193 | 814,8,S/2004 N1,4496005299291580.0,9000.0,0.1,300000.0,0.0,1300.0,0.0,0.0,0.0,0.0, 194 | 901,9,Charon,1.533137807058429e+021,603600.0,0.372,102300000000.0,0.0,1664.0,0.0,0.0,0.0,0.0, 195 | 902,9,Nix,1.948268963026351e+016,23000.0,0.35,1300000.0,0.0,2100.0,0.0,0.0,0.0,0.0, 196 | 903,9,Hydra,9.741344815131755e+016,30500.0,0.35,6500000.0,0.0,800.0,0.0,0.0,0.0,0.0, 197 | 904,9,Kerberos,1.648535276406913e+016,14000.0,0.35,1100000.0,0.0,1400.0,0.0,0.0,0.0,0.0, 198 | 905,9,Styx,0.0,10000.0,0.35,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 199 | 2000001,0,Ceres,9.385920609538426e+020,469700.0,0.09,62628400000.0,0.0,0.0,0.0,0.0,0.0,0.0, 200 | 2000002,0,Pallas,2.143095859328986e+020,272500.0,0.101,14300000000.0,0.0,0.0,0.0,0.0,0.0,0.0, 201 | 2000003,0,Juno,0.0,123298.0,0.214,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 202 | 2000004,0,Vesta,2.667629810913004e+020,262700.0,0.4228,17800000000.0,0.0,0.0,0.0,0.0,0.0,0.0, 203 | 2000005,0,Astraea,0.0,53349.5,0.274,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 204 | 2000006,0,Hebe,0.0,92590.0,0.2679,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 205 | 2000007,0,Iris,0.0,99915.0,0.2766,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 206 | 2000008,0,Flora,0.0,73745.5,0.226,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 207 | 2000009,0,Metis,0.0,95000.0,0.118,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 208 | 2000010,0,Hygiea,1.049067903168035e+020,203560.0,0.0717,7000000000.0,0.0,0.0,0.0,0.0,0.0,0.0, 209 | 2000011,0,Parthenope,0.0,71443.5,0.191,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 210 | 2000012,0,Victoria,0.0,57543.5,0.163,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 211 | 2000013,0,Egeria,0.0,101318.0,0.07000000000000001,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 212 | 2000014,0,Irene,0.0,76000.0,0.159,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 213 | 2000015,0,Eunomia,0.0,115844.5,0.248,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 214 | 2000016,0,Psyche,2.292962702638706e+019,113000.0,0.1203,1530000000.0,0.0,0.0,0.0,0.0,0.0,0.0, 215 | 2000017,0,Thetis,0.0,42449.5,0.193,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 216 | 2000018,0,Melpomene,0.0,69797.0,0.181,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 217 | 2000019,0,Fortuna,0.0,100000.0,0.037,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 218 | 2000020,0,Massalia,0.0,67840.0,0.241,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 219 | 2000021,0,Lutetia,0.0,47880.0,0.2212,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 220 | 2000022,0,Kalliope,7.358462006507219e+018,83768.0,0.166,491000000.0,0.0,0.0,0.0,0.0,0.0,0.0, 221 | 2000023,0,Thalia,0.0,53765.0,0.2536,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 222 | 2000024,0,Themis,0.0,99000.0,0.067,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 223 | 2000025,0,Phocaea,0.0,30527.0,0.35,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 224 | 2000026,0,Proserpina,0.0,47400.0,0.1966,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 225 | 2000027,0,Euterpe,0.0,48000.0,0.215,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 226 | 2000028,0,Bellona,0.0,60450.0,0.1763,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 227 | 2000029,0,Amphitrite,0.0,94779.5,0.216,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 228 | 2000030,0,Urania,0.0,46393.5,0.192,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 229 | 2000031,0,Euphrosyne,0.0,133540.0,0.053,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 230 | 2000032,0,Pomona,0.0,40380.0,0.2564,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 231 | 2000033,0,Polyhymnia,0.0,26464.5,0.24,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 232 | 2000034,0,Circe,0.0,66496.0,0.052,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 233 | 2000035,0,Leukothea,0.0,51527.5,0.066,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 234 | 2000036,0,Atalante,0.0,66421.0,0.036,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 235 | 2000037,0,Fides,0.0,54175.0,0.1826,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 236 | 2000038,0,Leda,0.0,46127.5,0.055,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 237 | 2000039,0,Laetitia,0.0,89742.0,0.269,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 238 | 2000040,0,Harmonia,0.0,55625.5,0.22,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 239 | 2000041,0,Daphne,0.0,102747.5,0.059,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 240 | 2000042,0,Isis,0.0,55498.5,0.139,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 241 | 2000043,0,Ariadne,0.0,35670.0,0.234,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 242 | 2000044,0,Nysa,0.0,35320.0,0.482,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 243 | 2000045,0,Eugenia,0.0,101163.5,0.045,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 244 | 2000046,0,Hestia,0.0,65735.5,0.046,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 245 | 2000047,0,Aglaja,0.0,84087.0,0.082,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 246 | 2000048,0,Doris,0.0,108236.5,0.065,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 247 | 2000049,0,Pales,0.0,83126.0,0.048,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 248 | 2000050,0,Virginia,0.0,42037.0,0.05,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 249 | 2000051,0,Nemausa,0.0,69079.5,0.105,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 250 | 2000052,0,Europa,0.0,151959.0,0.057,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 251 | 2000053,0,Kalypso,0.0,48631.0,0.031,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 252 | 2000054,0,Alexandra,0.0,80060.0,0.041,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 253 | 2000055,0,Pandora,0.0,42397.0,0.204,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 254 | 2000056,0,Melete,0.0,60666.5,0.057,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 255 | 2000057,0,Mnemosyne,0.0,56295.0,0.2149,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 256 | 2000058,0,Concordia,0.0,53258.5,0.044,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 257 | 2000059,0,Elpis,0.0,82559.5,0.044,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 258 | 2000060,0,Echo,0.0,21609.0,0.373,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 259 | 2000061,0,Danae,0.0,42968.5,0.203,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 260 | 2000062,0,Erato,0.0,53460.5,0.048,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 261 | 2000063,0,Ausonia,0.0,58022.0,0.125,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 262 | 2000064,0,Angelina,0.0,29146.0,0.483,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 263 | 2000065,0,Cybele,0.0,118630.0,0.0706,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 264 | 2000066,0,Maja,0.0,35910.0,0.0618,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 265 | 2000067,0,Asia,0.0,28154.5,0.228,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 266 | 2000068,0,Leto,0.0,61254.5,0.228,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 267 | 2000069,0,Hesperia,0.0,69065.0,0.1402,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 268 | 2000070,0,Panopaea,0.0,63955.5,0.038,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 269 | 2000071,0,Niobe,0.0,41710.0,0.3052,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 270 | 2000072,0,Feronia,0.0,37483.0,0.083,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 271 | 2000073,0,Klytia,0.0,22295.0,0.223,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 272 | 2000074,0,Galatea,0.0,59355.0,0.0431,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 273 | 2000075,0,Eurydike,0.0,31188.5,0.117,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 274 | 2000076,0,Freia,0.0,72711.5,0.058,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 275 | 2000077,0,Frigga,0.0,30695.0,0.177,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 276 | 2000078,0,Diana,0.0,60300.0,0.0706,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 277 | 2000079,0,Eurynome,0.0,31739.5,0.287,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 278 | 2000080,0,Sappho,0.0,34281.5,0.206,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 279 | 2000081,0,Terpsichore,0.0,58863.5,0.045,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 280 | 2000082,0,Alkmene,0.0,28810.5,0.167,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 281 | 2000083,0,Beatrix,0.0,55251.5,0.05,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 282 | 2000084,0,Klio,0.0,39580.0,0.0527,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 283 | 2000085,0,Io,0.0,77395.0,0.06660000000000001,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 284 | 2000086,0,Semele,0.0,54964.5,0.056,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 285 | 2000087,0,Sylvia,0.0,126525.5,0.046,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 286 | 2000088,0,Thisbe,0.0,116000.0,0.06710000000000001,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 287 | 2000089,0,Julia,0.0,72741.5,0.189,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 288 | 2000090,0,Antiope,0.0,57987.0,0.058,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 289 | 2000091,0,Aegina,0.0,51701.0,0.048,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 290 | 2000092,0,Undina,0.0,63210.0,0.2509,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 291 | 2000093,0,Minerva,0.0,77077.5,0.056,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 292 | 2000094,0,Aurora,0.0,102445.0,0.0395,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 293 | 2000095,0,Arethusa,0.0,73984.5,0.058,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 294 | 2000096,0,Aegle,0.0,88887.0,0.048,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 295 | 2000097,0,Klotho,0.0,50358.5,0.128,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 296 | 2000098,0,Ianthe,0.0,66394.0,0.029,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 297 | 2000099,0,Dike,0.0,33677.0,0.065,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 298 | 2000100,0,Hekate,0.0,42867.0,0.205,0.0,0.0,0.0,0.0,0.0,0.0,0.0, 299 | -------------------------------------------------------------------------------- /demo/Anton-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treeform/orbits/b53ebe666d9f881fee26fd42bbfba5f17a3b0400/demo/Anton-Regular.ttf -------------------------------------------------------------------------------- /demo/horizonsys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treeform/orbits/b53ebe666d9f881fee26fd42bbfba5f17a3b0400/demo/horizonsys.png -------------------------------------------------------------------------------- /demo/orbitsDemo.nim: -------------------------------------------------------------------------------- 1 | import strformat, times 2 | import orbits/simple, orbits/spk, orbits/horizon 3 | import pixie/demo, vmath 4 | 5 | let now = epochTime() 6 | 7 | const 8 | planetScale = 0.25 9 | 10 | downloadSpk("de435.bsp") 11 | 12 | var 13 | spkFile = readSpk("de435.bsp") 14 | hz = newHorizonClient() 15 | slide = 0 16 | currentTime = 0.0 17 | keyWait = false 18 | scaleK = 10.0 19 | 20 | let 21 | spkfilesImage = readImage("demo/spkfiles.png") 22 | horizonsysImage = readImage("demo/horizonsys.png") 23 | 24 | hz.debug = false 25 | 26 | var font = readFont("demo/Anton-Regular.ttf") 27 | font.size = 20 28 | font.paint.color = color(1, 1, 1, 1).rgbx 29 | 30 | var font2 = readFont("demo/Anton-Regular.ttf") 31 | font2.size = 10 32 | font2.paint.color = color(1, 1, 1, 1).rgbx 33 | 34 | start("Orbits demo.") 35 | while true: 36 | 37 | scaleK += mouseWheelDelta * 0.2 38 | 39 | scaleK = clamp(scaleK, 1.0, 20.0) 40 | 41 | screen.fill(color(0.11, 0.14, 0.42, 1.0)) 42 | 43 | case slide 44 | of 0: 45 | let text = "Totally fake Solar System." 46 | screen.fillText(font.typeset(text), vec2(10, 10)) 47 | 48 | var mat = translate(vec2(400, 300)) * scale(vec2(scaleK, scaleK)) 49 | 50 | var path: Path 51 | path.circle(vec2(0, 0), 0.1) 52 | screen.fillPath(path, color(1,1,1,1), mat) 53 | 54 | for planet in simpleElements: 55 | var step = planet.period / 360 56 | var path: Path 57 | 58 | let dist = planet.posAt(0).length 59 | 60 | for i in 0..360: 61 | let th = float(i).toRadians() 62 | let pos = dvec3(sin(th), cos(th), 0) * dist / AU 63 | if i == 0: 64 | path.moveTo(pos.x.float32, pos.y.float32) 65 | else: 66 | path.lineTo(pos.x.float32, pos.y.float32) 67 | path.closePath() 68 | 69 | let th = (-currentTime*10 + planet.period)/ JULIAN_YEAR 70 | var currentPos = dvec3(sin(th), cos(th), 0) * dist / AU 71 | var pathBody: Path 72 | pathBody.circle(vec3(currentPos).xy, planetScale) 73 | screen.fillPath(pathBody, color(1,1,1,1), mat) 74 | 75 | screen.strokePath(path, color(1, 1, 1, 0.1), mat, strokeWidth = 1.5) 76 | 77 | of 1: 78 | let text = "Solar System with Ecliptical Model." 79 | screen.fillText(font.typeset(text), vec2(10, 10)) 80 | 81 | var mat = translate(vec2(400, 300)) * scale(vec2(scaleK, scaleK)) 82 | 83 | var path: Path 84 | path.circle(vec2(0, 0), 0.1) 85 | screen.fillPath(path, color(1,1,1,1), mat) 86 | 87 | for planet in simpleElements: 88 | var step = planet.period / 360 89 | var path: Path 90 | 91 | for i in 0..360: 92 | let time = step * float(i) 93 | let pos = planet.posAt(time) / AU 94 | if i == 0: 95 | path.moveTo(pos.x.float32, pos.y.float32) 96 | else: 97 | path.lineTo(pos.x.float32, pos.y.float32) 98 | path.closePath() 99 | 100 | var currentPos = planet.posAt(currentTime) / AU 101 | var pathBody: Path 102 | pathBody.circle(vec3(currentPos).xy, planetScale) 103 | screen.fillPath(pathBody, color(1,1,1,1), mat) 104 | 105 | screen.strokePath(path, color(1, 1, 1, 0.1), mat, strokeWidth = 0.5) 106 | 107 | of 2: 108 | screen.draw(spkfilesImage) 109 | 110 | of 3: 111 | let text = "Solar System with NASA's Chebyshev polynomials." 112 | screen.fillText(font.typeset(text), vec2(10, 10)) 113 | 114 | var mat = translate(vec2(400, 300)) * scale(vec2(scaleK, scaleK)) 115 | 116 | var path: Path 117 | path.circle(vec2(0, 0), 0.1) 118 | screen.fillPath(path, color(1,1,1,1), mat) 119 | 120 | for planet in simpleElements: 121 | var step = planet.period / 360 122 | var firstPos: DVec3 123 | var path: Path 124 | 125 | for i in 0..360: 126 | let time = step * float(i) 127 | let pos = spkFile.posAt(time, planet.id, 0) / AU 128 | if i == 0: 129 | path.moveTo(pos.x.float32, pos.y.float32) 130 | firstPos = pos 131 | else: 132 | path.lineTo(pos.x.float32, pos.y.float32) 133 | path.lineTo(firstPos.x.float32, firstPos.y.float32) 134 | path.closePath() 135 | 136 | var currentPos = spkFile.posAt(currentTime, planet.id, 0) / AU 137 | var pathBody: Path 138 | pathBody.circle(vec3(currentPos).xy, planetScale) 139 | screen.fillPath(pathBody, color(1,1,1,1), mat) 140 | 141 | screen.strokePath(path, color(1, 1, 1, 0.1), mat, strokeWidth = 0.25) 142 | 143 | of 4: 144 | screen.draw(horizonsysImage, mat=scale(vec2(0.8))) 145 | 146 | of 5: 147 | let text = "Solar System with NASA's Horizon System." 148 | screen.fillText(font.typeset(text), vec2(10, 10)) 149 | 150 | var mat = translate(vec2(400, 300)) * scale(vec2(scaleK, scaleK)) 151 | 152 | var path: Path 153 | path.circle(vec2(0, 0), 0.1) 154 | screen.fillPath(path, color(1,1,1,1), mat) 155 | 156 | proc drawOrbit(id: int, color: Color) = 157 | 158 | let elements = hz.getOrbitalElements( 159 | time = 0.0, 160 | targetId = id, 161 | observerId = 0 162 | ) 163 | 164 | let period = elements.period 165 | 166 | var firstPos: DVec3 167 | var path: Path 168 | 169 | let entries = hz.getOrbitalVectorsSeq( 170 | fromTime = 0.0, 171 | toTime = period, 172 | steps = 360, 173 | targetId = id, 174 | observerId = 0) 175 | for i, entry in entries: 176 | let pos = entry.pos / AU 177 | if i == 0: 178 | path.moveTo(pos.x.float32, pos.y.float32) 179 | firstPos = pos 180 | else: 181 | path.lineTo(pos.x.float32, pos.y.float32) 182 | path.lineTo(firstPos.x.float32, firstPos.y.float32) 183 | path.closePath() 184 | 185 | var currentPos = entries.posAt(currentTime mod period) / AU 186 | var pathBody: Path 187 | pathBody.circle(vec3(currentPos).xy, planetScale) 188 | screen.fillPath(pathBody, color(1,1,1,1), mat) 189 | 190 | screen.strokePath(path, color, mat, strokeWidth = 0.1) 191 | 192 | for planet in simpleElements: 193 | drawOrbit(planet.id, color(1, 1, 1, 0.1)) 194 | 195 | # for id in [2000001]: 196 | # drawOrbit(id, color(1, 0, 0, 0.1)) 197 | 198 | block: 199 | #plot voyager1 200 | let id = -31 201 | let entries = hz.getOrbitalVectorsSeq( 202 | 242290800+DAY, 203 | 1552284021, 204 | 1000, 205 | id, 206 | 0) 207 | var path: Path 208 | for entry in entries: 209 | let pos = entry.pos / AU 210 | path.lineTo(pos.x.float32, pos.y.float32) 211 | screen.strokePath(path, color(1,1,1,1), mat, strokeWidth = 0.1) 212 | let pos = entries[^1].pos / AU 213 | screen.fillText(font2, " Voyager 1", mat * vec2(pos.x.float32, pos.y.float32)) 214 | 215 | block: 216 | #plot voyager2 217 | let id = -32 218 | let entries = hz.getOrbitalVectorsSeq( 219 | 240908400+DAY, 220 | 1552284021, 221 | 1000, 222 | id, 223 | 0) 224 | var path: Path 225 | for entry in entries: 226 | let pos = entry.pos / AU 227 | path.lineTo(pos.x.float32, pos.y.float32) 228 | screen.strokePath(path, color(1,1,1,1), mat, strokeWidth = 0.1) 229 | let pos = entries[^1].pos / AU 230 | screen.fillText(font2, " Voyager 2", mat * vec2(pos.x.float32, pos.y.float32)) 231 | 232 | block: 233 | #plot voyager2 234 | let id = -98 235 | let entries = hz.getOrbitalVectorsSeq( 236 | 1137657600+DAY, 237 | 1552284021, 238 | 1000, 239 | id, 240 | 0) 241 | var path: Path 242 | for entry in entries: 243 | let pos = entry.pos / AU 244 | path.lineTo(pos.x.float32, pos.y.float32) 245 | screen.strokePath(path, color(1,1,1,1), mat, strokeWidth = 0.1) 246 | let pos = entries[^1].pos / AU 247 | screen.fillText(font2, " New Horizon", mat * vec2(pos.x.float32, pos.y.float32)) 248 | 249 | 250 | # for id in [136199]: 251 | # drawOrbit(id, color(1, 0, 0, 0.9)) 252 | 253 | else: 254 | discard 255 | 256 | if isKeyDown(KEY_RIGHT) or isKeyDown(KEY_SPACE): 257 | if not keyWait: 258 | keyWait = true 259 | slide = min(slide.ord + 1, 5) 260 | elif isKeyDown(KEY_LEFT): 261 | if not keyWait: 262 | keyWait = true 263 | slide = max(slide.ord - 1, 0) 264 | else: 265 | keyWait = false 266 | 267 | tick() 268 | 269 | currentTime += 60*60*25 * 1 # day per frame 270 | -------------------------------------------------------------------------------- /demo/spkfiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treeform/orbits/b53ebe666d9f881fee26fd42bbfba5f17a3b0400/demo/spkfiles.png -------------------------------------------------------------------------------- /nim.cfg: -------------------------------------------------------------------------------- 1 | --define:ssl 2 | -------------------------------------------------------------------------------- /orbits.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.1.0" 4 | author = "Andre von Houck" 5 | description = "Orbits - orbital mechanics library for nim." 6 | license = "MIT" 7 | srcDir = "src" 8 | 9 | # Dependencies 10 | 11 | requires "nim >= 1.0.0" 12 | requires "chroma >= 0.2.5" 13 | requires "pixie >= 4.3.0" 14 | requires "vmath >= 1.1.4" 15 | requires "print >= 1.0.2" 16 | requires "cairo >= 1.1.0" 17 | requires "puppy >= 2.0.3" 18 | -------------------------------------------------------------------------------- /src/orbits.nim: -------------------------------------------------------------------------------- 1 | import orbits/simple 2 | import orbits/spk 3 | import orbits/horizon 4 | 5 | export simple 6 | export spk 7 | export horizon -------------------------------------------------------------------------------- /src/orbits/astrometry.nim: -------------------------------------------------------------------------------- 1 | import flippy, vmath, random, print, chroma, json, strutils 2 | import cairo 3 | 4 | 5 | type Star = object 6 | id: int 7 | name: string 8 | otherName: string 9 | th: float 10 | ph: float 11 | luma: float 12 | pos: Vec3 13 | 14 | type StarTri = object 15 | aID, bID, cID: int 16 | ab, bc, ca: float 17 | 18 | var brightStars: seq[Star] 19 | var starTris: seq[StarTri] 20 | 21 | proc toRad(v: float): float = 22 | v / 180 * PI 23 | 24 | 25 | proc readBrightStars() = 26 | let brightStarsJson = parseJson(readFile("src/orbits/brightstars.json")) 27 | var id = 0 28 | for star in brightStarsJson: 29 | var star = Star( 30 | name: star[0].getStr().replace("\xC3\x8E\xC2", "\xCE"), 31 | otherName: star[1].getStr(), 32 | ph: star[2].getFloat(), 33 | th: star[3].getFloat() + 90, 34 | luma: star[4].getFloat() 35 | ) 36 | #if "U" notin star.name: continue 37 | #if star.luma > 5: continue 38 | star.id = id 39 | star.pos = -vec3( 40 | sin(star.th.toRad) * cos(star.ph.toRad), 41 | sin(star.th.toRad) * sin(star.ph.toRad), 42 | cos(star.th.toRad) 43 | ) 44 | brightStars.add star 45 | inc id 46 | 47 | 48 | 49 | proc normalize(tri: var StarTri) = 50 | let maxDist = max([tri.ab, tri.bc, tri.ca]) 51 | tri.ab /= maxDist 52 | tri.bc /= maxDist 53 | tri.ca /= maxDist 54 | # sort tri 55 | if tri.ca > tri.bc: 56 | (tri.bId, tri.cId) = (tri.cId, tri.bId) 57 | (tri.bc, tri.ca) = (tri.ca, tri.bc) 58 | if tri.bc > tri.ab: 59 | (tri.aId, tri.bId) = (tri.bId, tri.aId) 60 | (tri.ab, tri.bc) = (tri.bc, tri.ab) 61 | 62 | 63 | proc genStarTris() = 64 | const minDist = 0.2 65 | for a in brightStars: 66 | for b in brightStars: 67 | if a.id != b.id: 68 | var ab = dist(a.pos, b.pos) 69 | if ab < minDist: 70 | #print "star pair", a.id, b.id, ab 71 | for c in brightStars: 72 | if a.id != c.id and b.id != c.id: 73 | var bc = dist(b.pos, c.pos) 74 | if bc < minDist: 75 | #print " star tri", a.id, b.id, c.id, ab, bc 76 | var ca = dist(c.pos, a.pos) 77 | var tri = StarTri( 78 | aID: a.id, bID: b.id, cID: c.id, 79 | ab: ab, bc: bc, ca: ca 80 | ) 81 | tri.normalize() 82 | if brightStars[tri.aID].otherName == "Dubhe" and 83 | brightStars[tri.bID].otherName == "Megrez" and 84 | brightStars[tri.cID].otherName == "Merak": 85 | starTris.add tri 86 | 87 | print "genreated: ", starTris.len 88 | 89 | proc drawMilkyWay() = 90 | var 91 | surface = imageSurfaceCreate(FORMAT_ARGB32, 1000, 500) 92 | ctx = surface.create() 93 | ctx.selectFontFace("Sans", FONT_SLANT_NORMAL, FONT_WEIGHT_NORMAL) 94 | ctx.setFontSize(12.0) 95 | 96 | ctx.setSourceRGBA(0.11, 0.14, 0.42, 1) 97 | ctx.rectangle(0, 0, float surface.getWidth, float surface.getHeight) 98 | ctx.fill() 99 | 100 | for star in brightStars: 101 | var s = 2/(star.luma + 1.44 + 1) 102 | let 103 | x = (star.ph / 360) * 1000 104 | y = (star.th / 180) * 500 105 | ctx.setSourceRGBA(1, 1, 1, 1) 106 | ctx.newPath() 107 | ctx.arc( 108 | x, 109 | y, 110 | s, 111 | 0.0, 2.0*PI) 112 | ctx.fill() 113 | ctx.closePath() 114 | 115 | discard surface.writeToPng("tests/milkyway.png") 116 | 117 | 118 | proc drawStars3d() = 119 | 120 | # 3d 121 | var 122 | surface = imageSurfaceCreate(FORMAT_ARGB32, 1000, 1000) 123 | ctx = surface.create() 124 | ctx.setSourceRGBA(0.11, 0.14, 0.42, 0.75) 125 | ctx.rectangle(0, 0, float surface.getWidth, float surface.getHeight) 126 | ctx.fill() 127 | 128 | ctx.translate(500, 500) 129 | 130 | var lookAtPos: Vec3 131 | for star in brightStars: 132 | if star.otherName == "Polaris": 133 | lookAtPos = star.pos 134 | 135 | var proj = perspective(40, 1, 0.01, 20) 136 | var mat = lookAt(-lookAtPos, vec3(0,0,0), vec3(0,0,1)) 137 | #var mat = rotateX(0.toRad) 138 | for star in brightStars: 139 | var pos = proj * mat * star.pos 140 | pos.x /= pos.z 141 | pos.y /= pos.z 142 | if pos.z < 0: continue 143 | #if "UMi" notin star.name and "UMa" notin star.name: continue 144 | var s = 10/(star.luma + 1.44 + 1) 145 | ctx.setSourceRGBA(1, 1, 1, 1) 146 | 147 | ctx.newPath() 148 | ctx.arc( 149 | pos.x*500, 150 | pos.y*500, 151 | s, 152 | 0.0, 2.0*PI) 153 | ctx.fill() 154 | ctx.closePath() 155 | 156 | if star.otherName in ["Polaris", "Pherkad", "Kochab", "Yildun", "Kochab"]: #, "Dubhe", "Merak", "Megrez", "Phecda", "Alioth", "Alcor", "Mizar", "Alkaid"]: 157 | ctx.setSourceRGBA(1, 0, 0, 0.5) 158 | ctx.newPath() 159 | ctx.arc( 160 | pos.x*500, 161 | pos.y*500, 162 | 10, 163 | 0.0, 2.0*PI) 164 | ctx.stroke() 165 | ctx.closePath() 166 | 167 | if star.otherName in ["Dubhe", "Merak", "Megrez", "Phecda", "Alioth", "Alcor", "Mizar", "Alkaid"]: 168 | ctx.setSourceRGBA(0, 1, 0, 0.5) 169 | ctx.newPath() 170 | ctx.arc( 171 | pos.x*500, 172 | pos.y*500, 173 | 10, 174 | 0.0, 2.0*PI) 175 | ctx.stroke() 176 | ctx.closePath() 177 | 178 | discard surface.writeToPng("tests/brightstars3d.png") 179 | 180 | 181 | proc analyzeImage() = 182 | 183 | const keepBrightSpots = 50 184 | 185 | var img = loadImage("tests/bigdipper.png") 186 | var mipmaps: seq[Image] 187 | while img.width > 50 and img.height > 50: 188 | mipmaps.add img 189 | img = img.minifyBy2() 190 | print img.width 191 | echo "minified" 192 | 193 | var 194 | image = imageSurfaceCreateFromPng("tests/bigdipper.png") 195 | surface = imageSurfaceCreate(FORMAT_ARGB32, image.getWidth, image.getHeight) 196 | ctx = surface.create() 197 | 198 | ctx.setSource(image, 0, 0) 199 | ctx.paint() 200 | 201 | ctx.selectFontFace("Sans", FONT_SLANT_NORMAL, FONT_WEIGHT_NORMAL) 202 | ctx.setFontSize(12.0) 203 | 204 | ctx.setSourceRGBA(0.11, 0.14, 0.42, 0.75) 205 | ctx.rectangle(0, 0, float surface.getWidth, float surface.getHeight) 206 | ctx.fill() 207 | 208 | ctx.setSourceRGBA(1, 1, 1, 1) 209 | ctx.setLineWidth(1) 210 | 211 | proc luma(rgba: ColorRGBA): float = 212 | return (float(rgba.r)/255.0 + float(rgba.g)/255.0 + float(rgba.b)/255.0)/3.0 213 | 214 | type Spot = object 215 | x, y: int 216 | luma: float 217 | 218 | proc pos(spot: Spot): Vec2 = 219 | vec2(float spot.x, float spot.y) 220 | 221 | proc `pos=`(spot: var Spot, pos: Vec2) = 222 | spot.x = int pos.x 223 | spot.y = int pos.y 224 | 225 | # find the brighest spot the the lowest mip level 226 | var brightestN: seq[Spot] 227 | proc insertSort(brightest: var seq[Spot], spot: Spot, num: int) = 228 | for i, s in brightest: 229 | if spot.luma > s.luma: 230 | brightest.insert(spot, i) 231 | if brightest.len > num: 232 | brightest.setLen(num) 233 | return 234 | if brightest.len < num: 235 | brightest.add(spot) 236 | let mostBlurLevel = mipmaps.len - 1 237 | let mostBlur = mipmaps[^1] 238 | for x in 0 ..< mostBlur.width: 239 | for y in 0 ..< mostBlur.height: 240 | let spot = Spot(x:x, y:y, luma:luma(mostBlur.getRgba(x, y))) 241 | brightestN.insertSort(spot, keepBrightSpots) 242 | 243 | 244 | # adjust each spot in accuracy as we walk up the mip levels 245 | for spot in brightestN.mitems: 246 | for mipLevel in 0.. spotA.luma: 274 | spotA.luma = spotB.luma 275 | if merge: 276 | merged.add spotA 277 | 278 | brightestN = merged 279 | 280 | for spot in brightestN: 281 | 282 | ctx.newPath() 283 | ctx.setLineWidth(spot.luma*2) 284 | ctx.arc( 285 | float spot.x, 286 | float spot.y, 287 | 10, 288 | 0.0, 2.0*PI) 289 | ctx.stroke() 290 | ctx.closePath() 291 | 292 | # ctx.save() 293 | # ctx.moveTo(float spot.x + 15, float spot.y) 294 | # ctx.showText("Luma: " & $spot.luma) 295 | # ctx.restore() 296 | 297 | print brightestN.len 298 | discard surface.writeToPng("tests/bigdipper_stars.png") 299 | 300 | 301 | var 302 | thisTri: StarTri 303 | thisTri.aID = 0 304 | thisTri.bID = 1 305 | thisTri.cID = 2 306 | thisTri.ab = dist(brightestN[0].pos, brightestN[1].pos) 307 | thisTri.bc = dist(brightestN[1].pos, brightestN[2].pos) 308 | thisTri.ca = dist(brightestN[2].pos, brightestN[0].pos) 309 | thisTri.normalize() 310 | 311 | print thisTri 312 | var posTrans = -brightestN[thisTri.aID].pos + vec2(float surface.getWidth, float surface.getHeight) / 2.0 313 | print posTrans 314 | for b in brightestN.mitems: 315 | b.pos = (b.pos + posTrans) 316 | 317 | ctx.setSourceRGBA(0.11, 0.14, 0.42, 1) 318 | ctx.rectangle(0, 0, float surface.getWidth, float surface.getHeight) 319 | ctx.fill() 320 | 321 | ctx.save() 322 | ctx.setSource(image, 0, 0) 323 | ctx.paint() 324 | ctx.setSourceRGBA(0.11, 0.14, 0.42, 0.75) 325 | ctx.rectangle(0, 0, float surface.getWidth, float surface.getHeight) 326 | ctx.fill() 327 | ctx.restore() 328 | 329 | ctx.translate(-posTrans.x, -posTrans.y) 330 | 331 | ctx.setSourceRGBA(1, 1, 1, 1) 332 | ctx.setLineWidth(1) 333 | for i, id in [thisTri.aID, thisTri.bID, thisTri.cID]: 334 | let spot = brightestN[id] 335 | ctx.newPath() 336 | print spot 337 | ctx.arc( 338 | float spot.x, 339 | float spot.y, 340 | 10, 341 | 0.0, 2.0*PI) 342 | ctx.stroke() 343 | ctx.closePath() 344 | ctx.save() 345 | ctx.moveTo(float spot.x + 15, float spot.y) 346 | ctx.showText(" #" & $i) 347 | ctx.restore() 348 | 349 | for spot in brightestN: 350 | ctx.newPath() 351 | ctx.arc( 352 | float spot.x, 353 | float spot.y, 354 | 5, 355 | 0.0, 2.0*PI) 356 | ctx.stroke() 357 | ctx.closePath() 358 | 359 | var goodPixDist = float(max(surface.getWidth, surface.getHeight)) * 0.01 360 | var bestError = 1000.0 361 | var bestMatches = 0 362 | var bestMat: Mat4 363 | var bestTri: StarTri 364 | for tri in starTris: 365 | 366 | let minError = pow(thisTri.bc - tri.bc, 2) + pow(thisTri.ca - tri.ca, 2) 367 | print minError 368 | if true or minError < 0.8: 369 | bestError = minError 370 | # solve for matrix that: 371 | # brightestN[tri.aID].pos == mat * brightStars[tri.aID] 372 | # brightestN[tri.aID].pos == mat * brightStars[tri.aID] 373 | # brightestN[tri.aID].pos == mat * brightStars[tri.aID] 374 | 375 | # look at first star 376 | var mat = lookAt(brightStars[tri.aID].pos, vec3(0,0,0), vec3(0,0,1)) 377 | 378 | 379 | # make sure rotation between first and second screen spot 380 | # match the rotation between first and second star 381 | let 382 | posA = mat * brightStars[tri.aID].pos 383 | posB = mat * brightStars[tri.bID].pos 384 | starAngle = arctan2(posB.y - posA.y, posB.x - posA.x) 385 | spotA = brightestN[thisTri.aID].pos 386 | spotB = brightestN[thisTri.bID].pos 387 | spotAngle = arctan2(spotB.y - spotA.y, spotB.x - spotA.x) 388 | mat = rotateZ(starAngle - spotAngle) * mat 389 | 390 | # make sure that the distance between first and second spot 391 | # match the distance between frist and second star 392 | let zoom = dist(brightestN[thisTri.aID].pos, brightestN[thisTri.bID].pos) 393 | let zoom2 = dist(mat * brightStars[tri.aID].pos, mat * brightStars[tri.bID].pos) 394 | mat = scaleMat((zoom/zoom2)) * mat 395 | 396 | # translate the stars into screen center 397 | mat = translate(vec3(float(surface.getWidth)/2.0, float(surface.getHeight)/2.0, 0.0)) * mat 398 | 399 | # compute error of the thrid star and spot 400 | 401 | var error = dist((mat * brightStars[tri.cID].pos).xy, brightestN[thisTri.cID].pos) 402 | 403 | if error < goodPixDist: 404 | # test all stars against bright spots 405 | var matches = 0 406 | for star in brightStars: 407 | var pos = mat * star.pos 408 | for spot in brightestN: 409 | if dist(spot.pos, pos.xy) < goodPixDist: 410 | inc matches 411 | if matches > bestMatches: 412 | bestMatches = matches 413 | bestMat = mat 414 | bestTri = tri 415 | print bestMatches 416 | 417 | for i, id in [bestTri.aId, bestTri.bId, bestTri.cId]: 418 | let star = brightStars[id] 419 | var pos = bestMat * star.pos 420 | ctx.setSourceRGBA(0, 1, 0, 1) 421 | ctx.newPath() 422 | ctx.arc( 423 | pos.x, 424 | pos.y, 425 | goodPixDist/2, 426 | 0.0, 2.0*PI) 427 | ctx.stroke() 428 | ctx.closePath() 429 | ctx.save() 430 | ctx.moveTo(float pos.x, float pos.y - 20) 431 | ctx.showText($i) 432 | ctx.restore() 433 | 434 | for star in brightStars: 435 | var pos = bestMat * star.pos 436 | ctx.setSourceRGBA(1, 0, 0, 1) 437 | ctx.newPath() 438 | ctx.arc( 439 | pos.x, 440 | pos.y, 441 | goodPixDist, 442 | 0.0, 2.0*PI) 443 | ctx.stroke() 444 | ctx.closePath() 445 | if star.otherName != "": 446 | ctx.save() 447 | ctx.moveTo(float pos.x + 15, float pos.y) 448 | ctx.showText(" " & star.otherName) 449 | ctx.restore() 450 | 451 | discard surface.writeToPng("tests/matching.png") 452 | 453 | 454 | 455 | 456 | readBrightStars() 457 | drawMilkyWay() 458 | # drawStars3d() 459 | # genStarTris() 460 | # analyzeImage() -------------------------------------------------------------------------------- /src/orbits/elements.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "name": "mercury", 5 | "o": 48.10434602754433, 6 | "i": 7.003087466401921, 7 | "w": 27.78719571338938, 8 | "a": 0.4000255039788957, 9 | "e": 0.2201599657730014, 10 | "m": 334.788894542372, 11 | "n": 3.89819558905566 12 | }, 13 | { 14 | "id": 2, 15 | "name": "venus", 16 | "o": 76.97669439756598, 17 | "i": 3.393108149410717, 18 | "w": 58.35621153448815, 19 | "a": 0.7182208678326626, 20 | "e": 0.01382589661308556, 21 | "m": 131.0010390214528, 22 | "n": 1.620341792975639 23 | }, 24 | { 25 | "id": 3, 26 | "name": "earth-moon", 27 | "o": 289.3646992556555, 28 | "i": 0.0009582336890288729, 29 | "w": 187.9363761157968, 30 | "a": 1.000825172761707, 31 | "e": 0.01720051160494004, 32 | "m": 343.5828739041493, 33 | "n": 0.9850447188992955 34 | }, 35 | { 36 | "id": 4, 37 | "name": "mars", 38 | "o": 49.64825348948091, 39 | "i": 1.851665869965064, 40 | "w": 287.4131338858448, 41 | "a": 1.533553293577183, 42 | "e": 0.09573753010619797, 43 | "m": 36.28542603422069, 44 | "n": 0.5193339756606363 45 | }, 46 | { 47 | "id": 5, 48 | "name": "jupiter", 49 | "o": 100.4047724660621, 50 | "i": 1.305303896867871, 51 | "w": 273.6392934603785, 52 | "a": 5.19903163177084, 53 | "e": 0.0481019312121747, 54 | "m": 189.9816717886359, 55 | "n": 0.08307878022277117 56 | }, 57 | { 58 | "id": 6, 59 | "name": "saturn", 60 | "o": 113.7660922435914, 61 | "i": 2.485904803750773, 62 | "w": 338.5348887046594, 63 | "a": 9.543087681962577, 64 | "e": 0.05473491200083265, 65 | "m": 310.8086659014555, 66 | "n": 0.03344073922859368 67 | }, 68 | { 69 | "id": 7, 70 | "name": "uranus", 71 | "o": 74.01321376825706, 72 | "i": 0.7735080752889525, 73 | "w": 96.96320320192272, 74 | "a": 19.1872323556339, 75 | "e": 0.04707524224059415, 76 | "m": 13.72353476377218, 77 | "n": 0.01173406114563247 78 | }, 79 | { 80 | "id": 8, 81 | "name": "neptune", 82 | "o": 131.7910151155089, 83 | "i": 1.76979707258509, 84 | "w": 273.7475287397057, 85 | "a": 30.07248540697402, 86 | "e": 0.008462720353253175, 87 | "m": 193.7953393791112, 88 | "n": 0.005980090506368096 89 | }, 90 | { 91 | "id": 9, 92 | "name": "pluto-charon", 93 | "o": 110.3088877251087, 94 | "i": 17.14043880694397, 95 | "w": 113.7898310825914, 96 | "a": 39.48268090528783, 97 | "e": 0.2488884578150434, 98 | "m": 331.2789327899312, 99 | "n": 0.003975440882543604 100 | } 101 | ] -------------------------------------------------------------------------------- /src/orbits/featuredb.nim: -------------------------------------------------------------------------------- 1 | ## Downloads planet feature (such as craters) with names and location from planetarynames.wr.usgs.gov 2 | 3 | import strformat, strutils 4 | import utils 5 | 6 | var url = (&"""https://planetarynames.wr.usgs.gov/SearchResults? 7 | feature= 8 | &system= 9 | &target= 10 | &featureType= 11 | &northernLatitude= 12 | &southernLatitude= 13 | &westernLongitude= 14 | &easternLongitude= 15 | &wellKnownText= 16 | &minFeatureDiameter= 17 | &maxFeatureDiameter= 18 | &beginDate= 19 | &endDate= 20 | &approvalStatus= 21 | &continent= 22 | ðnicity= 23 | &reference= 24 | &sort_asc=false 25 | &sort_column=diameter 26 | &is_positive_east=true 27 | &is_0_360=true 28 | &is_planetographic=false 29 | &displayType=CSV 30 | &pageStart=0 31 | &resultsPerPage=50 32 | &featureIDColumn=true 33 | &featureNameColumn=true 34 | &cleanFeatureNameColumn=true 35 | &targetColumn=true 36 | &diameterColumn=true 37 | ¢erLatLonColumn=true 38 | &latLonColumn=true 39 | &coordSystemColumn=true 40 | &contEthColumn=true 41 | &featureTypeColumn=true 42 | &featureTypeCodeColumn=true 43 | &quadColumn=true 44 | &approvalStatusColumn=true 45 | &approvalDateColumn=true 46 | &referenceColumn=true 47 | &originColumn=true 48 | &additionalInfoColumn=true 49 | &lastUpdatedColumn=true 50 | """).replace(" ", "").replace("\n", "") 51 | 52 | downloadFileIfNotExists(url, "db/feature.csv") 53 | writeFile("db/feature.csv", readFile("db/feature.csv").strip()) 54 | -------------------------------------------------------------------------------- /src/orbits/horizon.nim: -------------------------------------------------------------------------------- 1 | import net, streams, strutils, os, strutils, strformat 2 | import vmath, print 3 | import simple 4 | 5 | 6 | type 7 | HorizonClient* = ref object 8 | socket: Socket 9 | debug*: bool # turn on debug spam 10 | 11 | proc cutBy(main, s1, s2:string): string = 12 | var starts = main.find(s1) + s1.len 13 | var ends = main.rfind(s2) 14 | if starts == -1: 15 | quit("can't find s1 " & s1) 16 | if ends == -1: 17 | quit("can't find s2 " & s2) 18 | return main[starts.. "): 82 | send($targetId) 83 | if "Select ..." in lastLine and promt(": "): 84 | send("E") 85 | if promt("Observe, Elements, Vectors [o,e,v,?] : "): 86 | send(format) 87 | 88 | if cordinates != "": 89 | # rotation 90 | if "Coordinate" in lastLine and "center" in lastLine and promt("] : "): 91 | send("c@" & $targetId) 92 | # tons of stuff don't have rotation information, just skip it then 93 | if backLines.len > 3: 94 | if "Cannot find station file" in backLines[^3] or "No rotational model for center body" in backLines[^3] or "Cannot find central body matching" in backLines[^3]: 95 | send "x" 96 | return "" 97 | if "Cylindrical or Geodetic input" in lastLine and promt("] : "): 98 | send("g") 99 | if "Specify geodetic" in lastLine and promt("} : "): 100 | send(cordinates) 101 | if "Confirm selected station" in lastLine and promt("--> "): 102 | send("y") 103 | else: 104 | # position 105 | if "Coordinate" in lastLine and "center" in lastLine and promt("] : "): 106 | send("@" & $observerId) 107 | 108 | if " Use previous center" in lastLine and promt("] : "): 109 | send("n") 110 | if promt("Reference plane [eclip, frame, body ] : "): 111 | #send("eclip") 112 | send(reference) 113 | 114 | if promt("] : ") and "Starting TDB [>=" in lastLine: 115 | send(fromTime) 116 | if promt("] : ") and "Ending TDB [<=" in lastLine: 117 | send(toTime) 118 | if promt("Output interval [ex: 10m, 1h, 1d, ? ] : "): 119 | send(duration) 120 | if promt("Accept default output [ cr=(y), n, ?] : "): 121 | send("y") 122 | if "Scroll & Page: space" in lastLine: 123 | hz.socket.send(" ") 124 | if promt(">>> Select... [A]gain, [N]ew-case, [F]tp, [M]ail, [R]edisplay, ? : "): 125 | var orbitText = backBuffer.cutBy( 126 | "$$SOE", 127 | "$$EOE") 128 | 129 | var text = orbitText.strip() 130 | 131 | for blk in backBuffer.split("*******************************************************************************"): 132 | if "Output units" in blk: 133 | text = blk & "*** START ***\n" & text 134 | 135 | var start = 0 136 | while true: 137 | var badBlock = text.find("\x1B[", start) 138 | if badBlock == -1: 139 | break 140 | var endBadBlock = text.find("\27[m\27[K\13\27[K", badBlock+2) 141 | start = endBadBlock + 3 142 | text.delete(badBlock, endBadBlock+10) 143 | 144 | writeFile(fileKey, text) 145 | if hz.debug: 146 | echo "cached data in:", fileKey 147 | result = text 148 | send("n") 149 | running = false 150 | 151 | proc parseOrbitalVectors*(data: string): seq[OrbitalVectors] = 152 | var unit = AU 153 | var header, text: string 154 | var parts = data.split("*** START ***\n") 155 | header = parts[0] 156 | assert parts.len == 2 157 | text = parts[1] 158 | if "Output units : KM-D" in header: 159 | unit = KM 160 | 161 | var orbitData = newSeq[OrbitalVectors]() 162 | var entry: OrbitalVectors 163 | 164 | var i = 0 165 | for line in text.split("\n"): 166 | if i mod 4 == 0: 167 | var oddTime : float64 = parseFloat(line[0..15]) 168 | var time = Y2000 + (oddTime - J2000) * DAY 169 | entry.time = time 170 | 171 | if line.startsWith(" X"): 172 | var p = dvec3( 173 | parseFloat(line[4..25].strip()), 174 | parseFloat(line[30..51].strip()), 175 | parseFloat(line[56..77].strip())) 176 | p = p * unit 177 | entry.pos = p 178 | 179 | if line.startsWith(" VX"): 180 | var v = dvec3( 181 | parseFloat(line[4..25].strip()), 182 | parseFloat(line[30..51].strip()), 183 | parseFloat(line[56..77].strip())) 184 | v = v * unit / DAY 185 | entry.vel = v 186 | orbitData.add(entry) 187 | 188 | if line.startsWith("VX"): 189 | var v = dvec3( 190 | parseFloat(line[3..24].strip()), 191 | parseFloat(line[29..50].strip()), 192 | parseFloat(line[55..76].strip())) 193 | v = v * unit / DAY 194 | orbitData.add(entry) 195 | 196 | inc i 197 | 198 | return orbitData 199 | 200 | 201 | proc close*(hz: HorizonClient) = 202 | if hz.socket != nil: 203 | hz.socket.close() 204 | 205 | 206 | proc getOrbitalVectorsSeq*( 207 | hz: HorizonClient, 208 | fromTime: float64, 209 | toTime: float64, 210 | steps: int, 211 | targetId: int, 212 | observerId: int 213 | ): seq[OrbitalVectors] = 214 | let data = hz.getOrbitalData( 215 | "v", 216 | "JD " & $toJulianDate(fromTime), 217 | "JD " & $toJulianDate(toTime), 218 | $steps, 219 | targetId, 220 | observerId, 221 | "" 222 | ) 223 | return parseOrbitalVectors(data) 224 | 225 | iterator pairwise[T](s: seq[T]): (T, T) = 226 | for i in 0 ..< s.len - 1: 227 | yield(s[i], s[i + 1]) 228 | 229 | proc posAt*(entries: seq[OrbitalVectors], time: float64): DVec3 = 230 | for a, b in entries.pairwise: 231 | if time >= a.time and time < b.time: 232 | let diff = (time - a.time) / (b.time - a.time) 233 | return lerp(a.pos, b.pos, diff) 234 | 235 | proc getOrbitalVectors*( 236 | hz: HorizonClient, 237 | time: float64, 238 | targetId: int, 239 | observerId: int 240 | ): OrbitalVectors = 241 | let data = hz.getOrbitalData( 242 | "v", 243 | "JD " & $toJulianDate(time), 244 | "JD " & $toJulianDate(time + DAY), 245 | "2", 246 | targetId, 247 | observerId, 248 | "" 249 | ) 250 | return parseOrbitalVectors(data)[0] 251 | 252 | 253 | proc parseOrbitalElements*(data: string): seq[OrbitalElements] = 254 | var unit = AU 255 | var header, text: string 256 | var parts = data.split("*** START ***\n") 257 | header = parts[0] 258 | assert parts.len == 2 259 | text = parts[1] 260 | if "Output units : KM-D" in header: 261 | unit = KM 262 | 263 | var orbitData = newSeq[OrbitalElements]() 264 | var entry: OrbitalElements 265 | 266 | var i = 0 267 | var 268 | EC, QR, IN, OM, W, Tp, N, MA, TA, A, AD, PR: float 269 | 270 | for line in text.split("\n"): 271 | if i mod 5 == 0: 272 | var oddTime : float64 = parseFloat(line[0..15]) 273 | var time = Y2000 + (oddTime - J2000) * DAY 274 | #entry.time = time 275 | 276 | if line.startsWith(" EC="): 277 | EC = parseFloat(line[4..25].strip()) 278 | QR = parseFloat(line[30..51].strip()) 279 | IN = parseFloat(line[56..77].strip()) 280 | 281 | if line.startsWith(" OM="): 282 | OM = parseFloat(line[4..25].strip()) 283 | W = parseFloat(line[30..51].strip()) 284 | Tp = parseFloat(line[56..77].strip()) 285 | 286 | if line.startsWith(" N ="): 287 | N = parseFloat(line[4..25].strip()) 288 | MA = parseFloat(line[30..51].strip()) 289 | TA = parseFloat(line[56..77].strip()) 290 | 291 | if line.startsWith(" A ="): 292 | A = parseFloat(line[4..25].strip()) 293 | AD = parseFloat(line[30..51].strip()) 294 | PR = parseFloat(line[56..77].strip()) 295 | 296 | entry.o = OM # longitude of the ascending node 297 | entry.i = IN # inclination to the ecliptic (plane of the Earth's orbit) 298 | entry.w = W # argument of perihelion 299 | entry.a = A # semi-major axis, or mean distance from Sun 300 | entry.e = EC # eccentricity (0=circle, 0-1=ellipse, 1=parabola) 301 | entry.m = MA # mean anomaly (0 at perihelion increases uniformly with time) 302 | entry.n = N # mean motion 303 | 304 | orbitData.add entry 305 | 306 | inc i 307 | 308 | return orbitData 309 | 310 | 311 | proc getOrbitalElementsSeq*( 312 | hz: HorizonClient, 313 | fromTime: float64, 314 | toTime: float64, 315 | steps: int, 316 | targetId: int, 317 | observerId: int 318 | ): seq[OrbitalElements] = 319 | ## Get a sequence of OrbitalElements 320 | let data = hz.getOrbitalData( 321 | "e", 322 | "JD " & $toJulianDate(fromTime), 323 | "JD " & $toJulianDate(toTime), 324 | $steps, 325 | targetId, 326 | observerId, 327 | "" 328 | ) 329 | return parseOrbitalElements(data) 330 | 331 | 332 | proc getOrbitalElements*( 333 | hz: HorizonClient, 334 | time: float64, 335 | targetId: int, 336 | observerId: int 337 | ): OrbitalElements = 338 | ## Get a single set of OrbitalElements at a given time 339 | let data = hz.getOrbitalData( 340 | "e", 341 | "JD " & $toJulianDate(time), 342 | "JD " & $toJulianDate(time+DAY), 343 | "2", 344 | targetId, 345 | observerId, 346 | "" 347 | ) 348 | return parseOrbitalElements(data)[0] 349 | 350 | 351 | proc getRadius*( 352 | hz: HorizonClient, 353 | time: float64, 354 | targetId: int 355 | ): float = 356 | ## Gets the radius of the body in meters 357 | let data = hz.getOrbitalData( 358 | "v", 359 | "JD " & $toJulianDate(time), 360 | "JD " & $toJulianDate(time+DAY), 361 | "2", 362 | targetId, 363 | targetId, 364 | "0,0,0" 365 | ) 366 | var ov = parseOrbitalVectors(data)[0] 367 | return ov.pos.length 368 | 369 | 370 | proc getRotationAxis*( 371 | hz: HorizonClient, 372 | time: float64, 373 | targetId: int 374 | ): DVec3 = 375 | ## Get a normalized vector that the body rotation around. 376 | let data = hz.getOrbitalData( 377 | "v", 378 | "JD " & $toJulianDate(time), 379 | "JD " & $toJulianDate(time+DAY), 380 | "2", 381 | targetId, 382 | targetId, 383 | "0,-90,0", 384 | "eclip" 385 | ) 386 | var ov = parseOrbitalVectors(data)[0] 387 | return ov.pos.normalize() 388 | 389 | 390 | proc getRotationAngularSpeed*( 391 | hz: HorizonClient, 392 | time: float64, 393 | targetId: int 394 | ): float = 395 | ## Get the rotation angular speed in rad/second 396 | let timeScale: float = 60 * 60 397 | let data = hz.getOrbitalData( 398 | "v", 399 | "JD " & $toJulianDate(time), 400 | "JD " & $toJulianDate(time + timeScale * 25), 401 | "1h", 402 | targetId, 403 | targetId, 404 | "0,0,0" 405 | ) 406 | for i in 0..<24: 407 | let 408 | vec1 = parseOrbitalVectors(data)[i].pos 409 | vec2 = parseOrbitalVectors(data)[i+1].pos 410 | result += vec1.angle(vec2) / timeScale 411 | result = result / 24 412 | -------------------------------------------------------------------------------- /src/orbits/simple.nim: -------------------------------------------------------------------------------- 1 | import math, tables, json 2 | import vmath 3 | 4 | type 5 | OrbitalElements* = object 6 | id*: int # JPL horizon ID 7 | name*: string # object name 8 | o*: float64 # longitude of the ascending node 9 | i*: float64 # inclination to the ecliptic (plane of the Earth's orbit) 10 | w*: float64 # argument of perihelion 11 | a*: float64 # semi-major axis, or mean distance from Sun 12 | e*: float64 # eccentricity (0=circle, 0-1=ellipse, 1=parabola) 13 | m*: float64 # mean anomaly (0 at perihelion increases uniformly with time) 14 | n*: float64 # Mean motion 15 | 16 | 17 | OrbitalVectors* = object 18 | pos*: DVec3 # position 19 | vel*: DVec3 # velocity 20 | time*: float64 # time 21 | 22 | 23 | const Y2000* = 946684800.0 24 | const J2000* = 2451544.5 25 | const AU* = 149597870700.0 26 | const KM* = 1000.0 27 | const DAY* = 60*60*24 28 | const JULIAN_YEAR* = 365.25 * DAY 29 | const JULIAN_CENTURY* = 36525 * DAY 30 | const C* = 299792458 31 | const G* = 6.67259E-11 32 | 33 | proc toJulianDate*(time: float64): float64 = 34 | (time - Y2000) / DAY + J2000 35 | 36 | const elementsData = staticRead("elements.json") 37 | var simpleElements* = parseJson(elementsData).to(seq[OrbitalElements]) 38 | 39 | proc period*(oe: OrbitalElements): float64 = 40 | return 365.2568984 * pow(oe.a, 1.5) * 24*60*60 41 | 42 | proc rev*(x: float64): float64 = 43 | var rv = x - round(x / 360.0) * 360.0 44 | if rv < 0.0: 45 | rv = rv + 360.0 46 | return rv 47 | 48 | proc toRadians*(deg: float): float = 49 | return PI * deg / 180.0 50 | 51 | proc toDegrees*(rad: float): float = 52 | return rev(180.0 * rad / PI) 53 | 54 | proc posAt*(orbitalElements: OrbitalElements, time: float64): DVec3 = 55 | var d = time / (24*60*60) 56 | 57 | var N = orbitalElements.o # (Long asc. node) 58 | var i = orbitalElements.i # (Inclination) 59 | var w = orbitalElements.w # (Arg. of perigee) 60 | var a = orbitalElements.a # (Mean distance) 61 | var e = orbitalElements.e # (Eccentricity) 62 | var M = orbitalElements.m # (Mean anomaly) 63 | M += orbitalElements.n * d # (Mean motion) 64 | 65 | # Normalize angles 66 | N = rev(N) 67 | i = rev(i) 68 | w = rev(w) 69 | M = rev(M) 70 | 71 | # Compute the eccentric anomaly E from the mean anomaly M and from the eccentricity e (E and M in degrees): 72 | var E = M + (180/PI) * e * sin(toRadians(M)) * (1.0 + e * cos(toRadians(M))) 73 | var error = 1.0 74 | while error > 0.005: 75 | var E1 = E - (E - (180/PI) * e * sin(toRadians(E)) - M) / (1 - e * cos(toRadians(E))) 76 | error = abs(E - E1) 77 | E = E1 78 | 79 | # Then compute the body's distance r and its true anomaly v from: 80 | var x = a * (cos(toRadians(E)) - e) 81 | var y = a * (sqrt(1.0 - e*e) * sin(toRadians(E))) 82 | 83 | # Then we convert this to distance and true anonaly: 84 | var r = sqrt(x*x + y*y) 85 | var v = toDegrees(arctan2(y, x)) 86 | 87 | var n_rad = toRadians(N) 88 | var xw_rad = toRadians(v + w) 89 | var i_rad = toRadians(i) 90 | 91 | # To compute the position in ecliptic coordinates, we apply these formulae: 92 | var xeclip = r * ( cos(n_rad) * cos(xw_rad) - sin(n_rad) * sin(xw_rad) * cos(i_rad) ) 93 | var yeclip = r * ( sin(n_rad) * cos(xw_rad) + cos(n_rad) * sin(xw_rad) * cos(i_rad) ) 94 | var zeclip = r * sin(xw_rad) * sin(i_rad) 95 | 96 | var RA = toDegrees(arctan2(yeclip, xeclip)) 97 | var Decl = toDegrees(arcsin(zeclip / r)) 98 | 99 | return dvec3(xeclip, yeclip, zeclip) * AU 100 | -------------------------------------------------------------------------------- /src/orbits/simulate.nim: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treeform/orbits/b53ebe666d9f881fee26fd42bbfba5f17a3b0400/src/orbits/simulate.nim -------------------------------------------------------------------------------- /src/orbits/solarbodydb.nim: -------------------------------------------------------------------------------- 1 | ## Solor body downloads differnet databases from JPL Propulsion LIbratory 2 | ## And combines them into one. 3 | 4 | 5 | import httpclient, json, os, re, strutils, htmlparser, xmltree, sequtils 6 | import simple, utils 7 | import print 8 | 9 | type 10 | Body = object 11 | id: int 12 | parentId: int 13 | name: string 14 | meanRadiusM: float 15 | massKg: float 16 | albedo: float 17 | 18 | gm: float # G * mass 19 | equatorialRadiusM: float 20 | densityKgM3: float 21 | siderealRotationPeriodS: float 22 | orbitRotationPeriodS: float 23 | equatorialGravity: float 24 | escapeVelocityM: float 25 | 26 | 27 | var bodies: seq[Body] 28 | 29 | 30 | proc constantsParser*() = 31 | downloadFileIfNotExists("https://ssd.jpl.nasa.gov/?constants","cache/constants.html") 32 | var html = readFile("cache/constants.html").replace(" ", " ").replace("±","+-").replace("-11 kg-1 m3 s-2 ", "") 33 | var values = html.findAll(re"""(.*)""") 34 | proc fix(a: string): string = a.replace("""""", "").replace("", "").strip() 35 | for i, v in values: 36 | if i + 1 >= values.len: break 37 | let 38 | name = fix(v) 39 | value = fix(values[i+1]) 40 | if name in [ 41 | "speed of light", 42 | "Julian day", 43 | "Julian year", 44 | "Julian century", 45 | "astronomical unit", 46 | "mean sidereal day", 47 | "sidereal year", 48 | "gravitational constant", 49 | ]: 50 | echo name, " = ", value 51 | 52 | 53 | proc planetsParser*() = 54 | downloadFileIfNotExists("https://ssd.jpl.nasa.gov/?planet_phys_par","cache/planets.html") 55 | var html = readFile("cache/planets.html") 56 | var table = html.cutBy(""" (km s-1) 57 | """, 58 | """

References

""") 59 | var text = table.replacef(re"<(\w*)[^>]*>", "") 60 | text = text.replace(" ", " ").replace("±","+-") 61 | text = text.replace(re"\s+", " ") 62 | text = text.replacef(re"([A-Z][a-z]+)", "\n$1") 63 | 64 | proc p(num: string): float = 65 | parseFloat(num.split("+-")[0]) 66 | 67 | var body = Body() 68 | body.id = 0 69 | body.parentId = 0 70 | body.name = "Solar System Barycenter (SSB)" 71 | body.massKg = 1.989E30 72 | body.gm = body.massKg * G 73 | bodies.add(body) 74 | 75 | body = Body() 76 | body.id = 10 77 | body.parentId = 0 78 | body.name = "Sun" 79 | body.massKg = 1.989E30 80 | body.gm = body.massKg * G 81 | body.equatorialRadiusM = 6.957E8 82 | body.meanRadiusM = 6.957E8 83 | body.siderealRotationPeriodS = 25.05*24*60*60 84 | bodies.add(body) 85 | 86 | var planetId = 1 87 | for line in text.strip().splitLines(): 88 | var args = line.split(" ") 89 | # barycenter 90 | var body = Body() 91 | body.id = planetId 92 | body.parentId = planetId 93 | body.name = args[0] & " Barycenter" 94 | if planetId == 3: body.name = "Earth-Moot Barycenter" 95 | if planetId == 9: body.name = "Pluto-Charon Barycenter" 96 | body.massKg = p(args[5]) * 1E+24 97 | body.gm = body.massKg * G 98 | body.orbitRotationPeriodS = p(args[11])*24*60*60*365.25 99 | bodies.add(body) 100 | inc planetId 101 | 102 | # body itself 103 | planetId = 1 104 | for line in text.strip().splitLines(): 105 | var args = line.split(" ") 106 | var body = Body() 107 | body.id = planetId * 100 + 99 108 | body.parentId = planetId 109 | body.name = args[0] 110 | body.equatorialRadiusM = p(args[1])*1000 111 | body.meanRadiusM = p(args[3])*1000 112 | body.massKg = p(args[5]) * 1E+24 113 | body.gm = body.massKg * G 114 | body.densityKgM3 = p(args[7]) * 1000 115 | body.siderealRotationPeriodS = p(args[9])*24*60*60 116 | body.orbitRotationPeriodS = p(args[11])*24*60*60*365.25 117 | body.albedo = p(args[15]) 118 | body.equatorialGravity = p(args[17]) 119 | body.escapeVelocityM = p(args[19]) * 1000 120 | bodies.add(body) 121 | 122 | inc planetId 123 | 124 | 125 | proc satellitesParser*() = 126 | downloadFileIfNotExists("https://ssd.jpl.nasa.gov/?sat_phys_par","cache/satellites.html") 127 | var html = readFile("cache/satellites.html") 128 | var table = html.cutBy("""JPL lunar constants and models technical document 129 | is also available. 130 |

""", 131 | """

Table Column Headings

""") 132 | table = table.replace("", "name:") 133 | var text = table.replacef(re"<(\w*)[^>]*>", "") 134 | text = text.replace(" ", " ").replace(" ", " ").replace("±","+-") 135 | text = text.replace(re"\s+", " ") 136 | text = text.replace("name:", "\n") 137 | text = text.replacef(re"\]([^\]]*GM \(km3/sec2\))", "]\n---- $1") 138 | text = "---- " & text 139 | #text = text.replacef(re"([A-Z][a-z//\d]+)", "\n$1") 140 | #var moons = text.split("GeometricAlbedo") 141 | echo text 142 | 143 | var 144 | planetId = 2 145 | moonId = 1 146 | 147 | for line in text.strip().splitLines(): 148 | if line.startsWith("----"): 149 | inc planetId 150 | moonId = 1 151 | continue 152 | var args = line.split(" ") 153 | if args[0].startsWith("S/20"): 154 | args[0].add " " & args[1] 155 | args.delete(1) 156 | 157 | proc p(num: string): float = 158 | if num == "?": return 0 159 | parseFloat(num.split(re"(\+\-|\[)")[0]) 160 | var body = Body() 161 | body.id = planetId * 100 + moonId 162 | body.parentId = planetId 163 | body.name = args[0] 164 | body.gm = p(args[1]) * 1000000000.0 165 | body.massKg = body.gm / G 166 | body.meanRadiusM = p(args[2])*1000 167 | body.densityKgM3 = p(args[3])*1000 168 | body.albedo = p(args[5]) 169 | bodies.add(body) 170 | inc moonId 171 | 172 | 173 | proc smallBodyParser*() = 174 | var filePath = "db/smallBody.csv" 175 | if not existsFile(filePath): 176 | var client = newHttpClient() 177 | let url = "https://ssd.jpl.nasa.gov/sbdb_query.cgi" 178 | var data = newMultipartData() 179 | data["obj_group"] = "all" 180 | data["obj_kind"] = "all" 181 | data["obj_numbered"] = "all" 182 | data["OBJ_field"] = "0" 183 | data["OBJ_op"] = "0" 184 | data["OBJ_value"] = "" 185 | data["ORB_field"] = "0" 186 | data["ORB_op"] = "0" 187 | data["ORB_value"] = "" 188 | data["c_fields"] = "AbAeAtApArBgBhBiBjBkBlBmBnBoBr" 189 | data["table_format"] = "CSV" 190 | data["max_rows"] = "500" 191 | data["format_option"] = "full" 192 | data["query"] = "Generate Table" 193 | data[".cgifields"] = "format_option" 194 | data[".cgifields"] = "field_list" 195 | data[".cgifields"] = "obj_kind" 196 | data[".cgifields"] = "obj_group" 197 | data[".cgifields"] = "obj_numbered" 198 | data[".cgifields"] = "ast_orbit_class" 199 | data[".cgifields"] = "table_format" 200 | data[".cgifields"] = "OBJ_field_set" 201 | data[".cgifields"] = "ORB_field_set" 202 | data[".cgifields"] = "preset_field_set" 203 | data[".cgifields"] = "com_orbit_class" 204 | var csv = client.postContent(url, multipart=data) 205 | writeFile(filePath, csv) 206 | var csv = readFile(filePath) 207 | var i = 0 208 | for line in csv.splitLines()[0..100]: 209 | if i == 0: 210 | inc i 211 | continue 212 | var args = line.split(",") 213 | #echo args 214 | proc p(num: string): float = 215 | if num == "": return 0 216 | let num2 = num.split(re"[^\d.+-eE]")[0] 217 | if num2 == "": return 0 218 | parseFloat(num2) 219 | 220 | var body = Body() 221 | body.id = parseInt(args[0]) 222 | body.name = args[1] 223 | body.gm = p(args[2]) * 1000000000.0 224 | body.massKg = body.gm / G 225 | body.meanRadiusM = p(args[3])/2*1000 226 | body.albedo = p(args[4]) 227 | bodies.add(body) 228 | 229 | inc i 230 | 231 | 232 | proc writeBodyDb() = 233 | var s = "id,parentId,name,massKg,meanRadiusM,albedo,gm,equatorialRadiusM,densityKgM3,siderealRotationPeriodS,orbitRotationPeriodS,equatorialGravity,escapeVelocityM\n" 234 | for body in bodies: 235 | s.add $body.id & "," 236 | s.add $body.parentId & "," 237 | s.add body.name & "," 238 | s.add $body.massKg & "," 239 | s.add $body.meanRadiusM & "," 240 | s.add $body.albedo & "," 241 | s.add $body.gm & "," 242 | s.add $body.equatorialRadiusM & "," 243 | s.add $body.densityKgM3 & "," 244 | s.add $body.siderealRotationPeriodS & "," 245 | s.add $body.orbitRotationPeriodS & "," 246 | s.add $body.equatorialGravity & "," 247 | s.add $body.escapeVelocityM & "," 248 | s.add "\n" 249 | writeFile("db/bigBody.csv", s) 250 | 251 | #constantsParser() 252 | planetsParser() 253 | satellitesParser() 254 | smallBodyParser() 255 | 256 | writeBodyDb() -------------------------------------------------------------------------------- /src/orbits/solvers.nim: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | func computeMachineEpsilon(): float = 4 | ## Return a number you can add to 1.0 and still get 1.0 5 | result = 1.0 6 | while true: 7 | result *= 0.5 8 | if (1.0 + result) == 1.0: 9 | break 10 | 11 | const 12 | GOLDEN_RATIO* = (1 + sqrt(5.0)) / 2 13 | MACHINE_EPSILON* = computeMachineEpsilon() 14 | 15 | 16 | proc isNaN*(x: float): bool = x.classify == fcNaN 17 | proc isInf*(x: float): bool = x.classify == fcInf 18 | 19 | 20 | proc newtonsRoot*(x0: float, f, df: proc(x: float): float): float = 21 | ## Finds the root of f(x) near x0 given df(x) = f'(x) 22 | var 23 | x0 = x0 24 | x = 0.0 25 | while true: 26 | x = x0 - f(x0) / df(x0) 27 | if isNaN(x) or abs(x - x0) < 1e-6: # Close enough 28 | return x 29 | x0 = x 30 | 31 | 32 | proc goldenSectionSearch*(x1, x2: float, f: proc(x: float): float, epsilon=MACHINE_EPSILON): float = 33 | # Finds the minimum of f(x) between x1 and x2. Returns x. 34 | # See: http://en.wikipedia.org/wiki/Golden_section_search 35 | 36 | let 37 | k = 2 - GOLDEN_RATIO 38 | var 39 | x3 = x2 40 | x2 = x1 + k * (x3 - x1) 41 | x1 = 0.0 42 | x = 0.0 43 | 44 | y2 = f(x2) 45 | y = 0.0 46 | 47 | while true: 48 | if (x3 - x2) > (x2 - x1): 49 | x = x2 + k * (x3 - x2) 50 | else: 51 | x = x2 - k * (x2 - x1) 52 | 53 | if (x3 - x1) <= (epsilon * (x2 + x)): # Close enough 54 | return (x3 + x1) / 2 55 | 56 | y = f(x) 57 | if y < y2: 58 | if (x3 - x2) > (x2 - x1): 59 | x1 = x2 60 | else: 61 | x3 = x2 62 | x2 = x 63 | y2 = y 64 | else: 65 | if (x3 - x2) > (x2 - x1): 66 | x3 = x 67 | else: 68 | x1 = x -------------------------------------------------------------------------------- /src/orbits/spk.nim: -------------------------------------------------------------------------------- 1 | ## Reads SPICE SPK DAF files. SPK files have .bsp extention. 2 | ## You can download many of the SPK files here: https://naif.jpl.nasa.gov/pub/naif/ 3 | ## 4 | ## Based on the work by arrieta: 5 | ## https://gist.githubusercontent.com/arrieta/c2b56f1e2277a6fede6d1afbc85095fb/raw/70c43dd28804be9ba9f7c604fe3ad2742f8fa753/spk.py 6 | ## 7 | 8 | 9 | import streams, print, puppy, strutils, os 10 | import vmath 11 | import simple 12 | 13 | type 14 | StateVecs = object 15 | pos: DVec3 16 | vel: DVec3 17 | 18 | 19 | iterator range(start, stop, step: int): int = 20 | var at = start 21 | if step < 0: 22 | while at > stop: 23 | yield at 24 | at += step 25 | else: 26 | while at < stop: 27 | yield at 28 | at += step 29 | 30 | 31 | proc chebyshev(order: int, x: float64, data: seq[float64]): float64 = 32 | # Evaluate a Chebyshev polynomial 33 | var 34 | two_x = 2 * x 35 | bkp2 = data[order] 36 | bkp1 = two_x * bkp2 + data[order - 1] 37 | for n in range(order - 2, 0, -1): 38 | let bk = data[n] + two_x * bkp1 - bkp2 39 | bkp2 = bkp1 40 | bkp1 = bk 41 | return data[0] + x * bkp1 - bkp2 42 | 43 | 44 | proc der_chebyshev(order: int, x: float64, data: seq[float64]): float64 = 45 | # Evaluate the derivative of a Chebyshev polynomial 46 | var 47 | two_x = 2 * x 48 | bkp2 = float64(order) * data[order] 49 | bkp1 = two_x * bkp2 + float64(order - 1) * data[order - 1] 50 | for n in range(order - 2, 1, -1): 51 | let bk = float64(n) * data[n] + two_x * bkp1 - bkp2 52 | bkp2 = bkp1 53 | bkp1 = bk 54 | return data[1] + two_x * bkp1 - bkp2 55 | 56 | 57 | type 58 | Spk = ref object 59 | records*: seq[Record] 60 | stream: Stream 61 | 62 | Record* = object 63 | etbeg*: float64 64 | etend*: float64 65 | target*: uint32 66 | observer*: uint32 67 | frame*: uint32 68 | rtype*: uint32 69 | rbeg*: uint32 70 | rend*: uint32 71 | 72 | proc readSpk*(filename: string): Spk = 73 | 74 | var spk = Spk() 75 | 76 | var f = openFileStream(filename) 77 | spk.stream = f 78 | # make sure this is in the right file format 79 | if f.readStr(7) != "DAF/SPK": 80 | quit("invalid SPK signature") 81 | f.setPosition(88) 82 | # make sure this is little endian 83 | if f.readStr(8) != "LTL-IEEE": 84 | quit("SPK file is not little endian") 85 | f.setPosition(8) 86 | # make sure number of float64 per record and number of uint32 per record match 87 | if f.readInt32() != 2: 88 | quit("SPK file float64 per record does not match") 89 | if f.readInt32() != 6: 90 | quit("SPK file number of uint32 per record match") 91 | 92 | f.setPosition(76) 93 | let 94 | fward = f.readInt32() 95 | bward = f.readInt32() 96 | 97 | # extract summary records 98 | let 99 | summaryOffset = (fward - 1) * 1024 100 | summarySize = 2 + (6 + 1) div 2 # integer division 101 | f.setPosition(summaryOffset) 102 | 103 | while true: 104 | # read record chunks 105 | let 106 | nxt = f.readFloat64() 107 | prv = f.readFloat64() 108 | nsum = f.readFloat64() 109 | for n in 0..") 137 | if tleFile.endsWith(".txt"): 138 | let tleFileUrl = "https://www.celestrak.com/NORAD/elements/" & tleFile 139 | echo tleFileUrl 140 | downloadFileIfNotExists(tleFileUrl, cacheDir / "" & tleFile) 141 | readTLE( cacheDir / "" & tleFile) 142 | 143 | 144 | proc heavensAbove(id: int) = 145 | downloadFileIfNotExists(&"https://www.heavens-above.com/satinfo.aspx?satid={id}", cacheDir / &"satinfo_{id}.html") 146 | var html = readFile(cacheDir / &"satinfo_{id}.html") 147 | var satInfo = SatInfo() 148 | for line in html.split("\n"): 149 | 150 | let satID = line.cutBy("", "") 151 | if satID.len > 0: 152 | #print satID 153 | satInfo.id = parseInt(satID) 154 | let cosparId = line.cutBy("", "").strip() 155 | if cosparId.len > 0: 156 | #print cosparId 157 | satInfo.cosparId = cosparId 158 | let name = line.cutBy("", "") 159 | if name.len > 0: 160 | #print name 161 | satInfo.name = name 162 | var orbit = line.cutBy("", "") 163 | if orbit.len > 0: 164 | orbit = orbit.replace(" ", " ") 165 | #print orbit 166 | satInfo.orbit = orbit 167 | if orbit.startsWith("decayed"): 168 | var orbitDecayedDate = orbit[8 .. ^1] 169 | #print orbitDecayedDate 170 | satInfo.orbitDecayedDate = orbitDecayedDate 171 | if orbit.startsWith("landed"): 172 | var landedDate = orbit[7 .. ^1] 173 | #print landedDate 174 | satInfo.landedDate = landedDate 175 | if " x " in orbit: 176 | let orbitParts = orbit.split(" ") 177 | var periapsis = orbitParts[2] 178 | var apoapsis = orbitParts[0] 179 | var inclination = orbitParts[4].replace("\xC2\xB0", "") 180 | #print periapsis, apoapsis, inclination 181 | satInfo.periapsis = parseFloat periapsis.replace(",", "") 182 | satInfo.apoapsis = parseFloat apoapsis.replace(",", "") 183 | satInfo.inclination = parseFloat inclination 184 | 185 | let category = line.cutBy("Category ", "").strip() 186 | if category.len > 0: 187 | #print category 188 | satInfo.category = category 189 | let country = line.cutBy("Country/organisation of origin ", "") 190 | if country.len > 0: 191 | #print country 192 | satInfo.country = country 193 | let launchDate = line.cutBy("", "") 194 | if launchDate.len > 0: 195 | #print launchDate 196 | satInfo.launchDate = launchDate 197 | var launchSite = line.cutBy("Launch site", "") 198 | if launchSite.len > 0: 199 | launchSite = launchSite.replace("
", "") 200 | #print launchSite 201 | satInfo.launchSite = launchSite 202 | let launchVehicle = line.cutBy("", "") 203 | if launchVehicle.len > 0: 204 | #print launchVehicle 205 | satInfo.launchVehicle = launchVehicle 206 | let description = line.cutBy("Description
", "
") 207 | if description.len > 0: 208 | #print description 209 | satInfo.description = description 210 | let mass = line.cutBy("Mass ", "") 211 | if mass.len > 0: 212 | #print mass 213 | satInfo.mass = parseInt mass.split(" ")[0] 214 | var image = line.cutBy(" 0: 216 | image = "https://www.heavens-above.com/" & image 217 | #print image 218 | satInfo.image = image 219 | var intrinsicBrightness = line.cutBy("Intrinsic brightness (Magnitude) ", "") 220 | if intrinsicBrightness.len > 0: 221 | if intrinsicBrightness == "?": 222 | intrinsicBrightness = "" 223 | #print intrinsicBrightness 224 | satInfo.intrinsicBrightness = intrinsicBrightness 225 | var maxBrightness = line.cutBy("Maximum brightness (Magnitude)", "") 226 | if maxBrightness.len > 0: 227 | #print maxBrightness 228 | satInfo.maxBrightness = maxBrightness 229 | 230 | print satInfo.id, satInfo.name 231 | 232 | satInfos[satInfo.id] = satInfo 233 | 234 | 235 | proc q(s: string): string = 236 | if "," in s or " " in s: 237 | return "\"" & s & "\"" 238 | 239 | proc q(s: SomeNumber): string = 240 | $s 241 | 242 | proc writeSatelliteHistorical() = 243 | var data = "id,name,cosparId,orbit,orbitDecayedDate,landedDate,periapsis,apoapsis,inclination,category,country,launchDate,launchSite,launchVehicle,description,mass,image,intrinsicBrightness,maxBrightness\n" 244 | for id in toSeq(satInfos.keys).sorted: 245 | let satInfo = satInfos[id] 246 | data.add q satInfo.id 247 | data.add "," 248 | data.add q satInfo.name 249 | data.add "," 250 | data.add q satInfo.cosparId 251 | data.add "," 252 | data.add q satInfo.orbit 253 | data.add "," 254 | data.add q satInfo.orbitDecayedDate 255 | data.add "," 256 | data.add q satInfo.landedDate 257 | data.add "," 258 | data.add q satInfo.periapsis 259 | data.add "," 260 | data.add q satInfo.apoapsis 261 | data.add "," 262 | data.add q satInfo.inclination 263 | data.add "," 264 | data.add q satInfo.category 265 | data.add "," 266 | data.add q satInfo.country 267 | data.add "," 268 | data.add q satInfo.launchDate 269 | data.add "," 270 | data.add q satInfo.launchSite 271 | data.add "," 272 | data.add satInfo.launchVehicle 273 | data.add "," 274 | data.add q satInfo.description 275 | data.add "," 276 | data.add q satInfo.mass 277 | data.add "," 278 | data.add q satInfo.image 279 | data.add "," 280 | data.add q satInfo.intrinsicBrightness 281 | data.add "," 282 | data.add q satInfo.maxBrightness 283 | data.add "\n" 284 | 285 | writeFile("db/satelliteHistorical.csv", data) 286 | 287 | 288 | proc writeTLE() = 289 | var data = "satnum,name,group,classification,intldesg,year,epochdays,ndot,nddot,bstar,ephtype,elnum,inclo,nodeo,ecco,argpo,mo,no_kozai,revnum\n" 290 | for name in toSeq(tles.keys).sorted: 291 | let tle = tles[name] 292 | data.add $tle.satnum 293 | data.add "," 294 | data.add tle.name 295 | data.add "," 296 | data.add tle.group 297 | data.add "," 298 | data.add tle.classification 299 | data.add "," 300 | data.add tle.intldesg 301 | data.add "," 302 | data.add $tle.year 303 | data.add "," 304 | data.add $tle.epochdays 305 | data.add "," 306 | data.add $tle.ndot 307 | data.add "," 308 | data.add $tle.nddot 309 | data.add "," 310 | data.add $tle.bstar 311 | data.add "," 312 | data.add tle.ephtype 313 | data.add "," 314 | data.add $tle.elnum 315 | data.add "," 316 | data.add $tle.inclo 317 | data.add "," 318 | data.add $tle.nodeo 319 | data.add "," 320 | data.add $tle.ecco 321 | data.add "," 322 | data.add $tle.argpo 323 | data.add "," 324 | data.add $tle.mo 325 | data.add "," 326 | data.add $tle.no_kozai 327 | data.add "," 328 | data.add $tle.revnum 329 | data.add "," 330 | data.add $tle.ibexp 331 | data.add "," 332 | data.add $tle.revnum 333 | data.add "\n" 334 | 335 | writeFile("db/satellite.csv", data) 336 | 337 | data = "satnum,name,group,classification,intldesg\n" 338 | for name in toSeq(tles.keys).sorted: 339 | let tle = tles[name] 340 | data.add $tle.satnum 341 | data.add "," 342 | data.add tle.name 343 | data.add "," 344 | data.add tle.group 345 | data.add "," 346 | data.add tle.classification 347 | data.add "," 348 | data.add tle.intldesg 349 | data.add "\n" 350 | writeFile("db/satelliteNames.csv", data) 351 | 352 | # downloadTLEIndex() 353 | # writeTLE() 354 | 355 | for id in 1 .. 18723: 356 | #print "-------------" 357 | heavensAbove(id) 358 | writeSatelliteHistorical() -------------------------------------------------------------------------------- /src/orbits/utils.nim: -------------------------------------------------------------------------------- 1 | import os, strutils, puppy 2 | 3 | proc cutBy*(text, startStr, endStr: string): string = 4 | let a = text.find(startStr) 5 | if a == -1: 6 | return 7 | let b = text.find(endStr, start=a + startStr.len) 8 | if b == -1: 9 | return 10 | return text[a + startStr.len ..< b] 11 | 12 | 13 | proc downloadFileIfNotExists*(url, filePath: string) = 14 | let dir = filePath.splitPath.head 15 | if not existsDir(dir): 16 | createDir(dir) 17 | if existsFile(filePath): 18 | return 19 | writeFile(puppy.fetch(url), filePath) 20 | -------------------------------------------------------------------------------- /tests/all.nim: -------------------------------------------------------------------------------- 1 | import generateOrbitalElements 2 | import generateStats 3 | import orbitsHorizon 4 | import orbitsSimple 5 | import orbitsSpk 6 | import spacecraft 7 | #import starman -------------------------------------------------------------------------------- /tests/bigdipper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treeform/orbits/b53ebe666d9f881fee26fd42bbfba5f17a3b0400/tests/bigdipper.png -------------------------------------------------------------------------------- /tests/bigdipper_stars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treeform/orbits/b53ebe666d9f881fee26fd42bbfba5f17a3b0400/tests/bigdipper_stars.png -------------------------------------------------------------------------------- /tests/brightstars3d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treeform/orbits/b53ebe666d9f881fee26fd42bbfba5f17a3b0400/tests/brightstars3d.png -------------------------------------------------------------------------------- /tests/earthToMarsSim.nim: -------------------------------------------------------------------------------- 1 | import strformat 2 | import orbits/simple, orbits/spk, orbits/horizon 3 | import cairo, orbits/vmath64, random, math 4 | import print 5 | 6 | 7 | var 8 | surface = imageSurfaceCreate(FORMAT_ARGB32, 1000, 1000) 9 | ctx = surface.create() 10 | 11 | ctx.setSourceRGBA(0.11, 0.14, 0.42, 1.0) 12 | ctx.rectangle(0, 0, float surface.getWidth, float surface.getHeight) 13 | ctx.fill() 14 | 15 | let scale = 250.0 16 | 17 | ctx.translate(500, 500) 18 | ctx.scale(scale, scale) 19 | ctx.setLineWidth(1/scale) 20 | 21 | ctx.setSourceRGBA(1, 1, 1, 1) 22 | ctx.arc(0, 0, 0.00464, 0.0, 2.0*PI) 23 | ctx.fill() 24 | 25 | type Body = object 26 | id: int 27 | pos: Vec3 28 | vel: Vec3 29 | mass: float 30 | var bodies: seq[Body] 31 | 32 | # sun 33 | var body = Body() 34 | body.id = 0 35 | bodies.add body 36 | 37 | downloadSpk("de435.bsp") 38 | ctx.setSourceRGBA(1, 1, 1, 0.15) 39 | var spkFile = readSpk("de435.bsp") 40 | for planet in simpleElements: 41 | var step = planet.period / 360 42 | for i in 0..360: 43 | let time = step * float(i) 44 | let pos = spkFile.posAt(time, planet.id, 0) / AU 45 | ctx.lineTo(pos.x, pos.y) 46 | ctx.closePath() 47 | ctx.stroke() 48 | 49 | var body = Body() 50 | body.id = planet.id 51 | bodies.add body 52 | 53 | const HOUR = 60 * 60 54 | const MINUTE = 60 55 | bodies[0].mass = 1.98847E+30 56 | bodies[1].mass = 3.30E+23 57 | bodies[2].mass = 4.87E+24 58 | bodies[3].mass = 5.97E+24 59 | bodies[4].mass = 6.42E+23 60 | bodies[5].mass = 1.90E+27 61 | bodies[6].mass = 5.68E+26 62 | bodies[7].mass = 8.68E+25 63 | bodies[8].mass = 1.02E+26 64 | bodies[9].mass = 1.46E+22 65 | 66 | 67 | 68 | 69 | proc minDistance(startTime: float, kick: Vec3, stepTime: float, draw: bool): (float, float) = 70 | 71 | 72 | 73 | for planet in simpleElements: 74 | bodies[planet.id].pos = spkFile.posAt(startTime, planet.id, 0) 75 | bodies[planet.id].vel = spkFile.velAt(startTime, planet.id, 0) 76 | 77 | var spaceCraft = Body() 78 | spaceCraft.pos = bodies[3].pos 79 | var prevPos = spaceCraft.pos 80 | spaceCraft.vel = bodies[3].vel + kick 81 | spaceCraft.mass = 110 * 1000 # The Martian: Hermes mass: 110 tons 82 | 83 | if draw: 84 | ctx.setSourceRGBA(1, 1, 1, 1) 85 | ctx.arc(spaceCraft.pos.x/AU, spaceCraft.pos.y/AU, 3/scale, 0.0, 2.0*PI) 86 | ctx.fill() 87 | 88 | var minMarsDist = 1E100 89 | 90 | 91 | let maxStep = int(JULIAN_YEAR*2/stepTime) 92 | #let stepTime = 100000.0 93 | var time = startTime 94 | var minTime = 0.0 95 | var minPos = vec3(0,0,0) 96 | for step in 0..maxStep: 97 | 98 | spaceCraft.pos = spaceCraft.pos + spaceCraft.vel * stepTime 99 | 100 | for other in bodies[0..0]: 101 | #if other.id != 0: 102 | # other.pos = spkFile.posAt(time, other.id, 0) 103 | let offset = other.pos - spaceCraft.pos 104 | let dv = G * other.mass / pow(offset.length, 2) 105 | let direction = offset.normalize() 106 | spaceCraft.vel = spaceCraft.vel + direction * dv * stepTime 107 | 108 | let 109 | marsPos = spkFile.posAt(time, 4, 0) 110 | marsVel = spkFile.velAt(time, 4, 0) 111 | let (minMars, minSpaceCraftPos, dist) = computeSegToSeg(marsPos, marsPos + marsVel, spaceCraft.pos, prevPos) 112 | #let dist = distPToS(, spaceCraft.pos, prevPos) 113 | 114 | #let dist = (marsPos - spaceCraft.pos).length 115 | #print dist, "vs", dist2, abs(dist - dist2) / dist2 116 | 117 | if minMarsDist > dist: 118 | minMarsDist = dist 119 | minTime = time 120 | minPos = minSpaceCraftPos #pointPToS(spkFile.posAt(time, 4, 0), spaceCraft.pos, prevPos) 121 | 122 | 123 | prevPos = spaceCraft.pos 124 | time += stepTime 125 | 126 | if draw: 127 | #echo dist 128 | #echo spaceCraft.pos 129 | ctx.lineTo(spaceCraft.pos.x/AU, spaceCraft.pos.y/AU) 130 | 131 | if draw: 132 | ctx.stroke() 133 | 134 | let marsPos = spkFile.posAt(minTime, 4, 0) 135 | ctx.setSourceRGBA(1, 0, 0, 1) 136 | ctx.arc(marsPos.x/AU, marsPos.y/AU, 5/scale, 0.0, 2.0*PI) 137 | ctx.fill() 138 | 139 | ctx.setSourceRGBA(1, 1, 1, 1) 140 | ctx.arc(minPos.x/AU, minPos.y/AU, 3/scale, 0.0, 2.0*PI) 141 | ctx.fill() 142 | 143 | return (minMarsDist, minTime) 144 | 145 | 146 | 147 | ctx.setSourceRGBA(1,1,1,1) 148 | 149 | var 150 | bestTime = 1552696987.0 151 | bestKick = vec3(0, 0, 0) 152 | bestMinDist = 1E30 153 | curTime = bestTime 154 | curKick = bestKick 155 | curMinDist = 1E30 156 | 157 | 158 | 159 | 160 | # let dayScan = 365 161 | 162 | # let startTime = 1552715293.0 163 | # randomize() 164 | # var minTime: float 165 | 166 | # bestKick = vec3(0, 0, 0) 167 | # bestMinDist = 1E30 168 | # for i in 0..dayScan: 169 | # curTime = startTime + float(i) * DAY*2 170 | 171 | # let dv = 3700.0 172 | # curKick = spkFile.velAt(curTime, 3, 0).normalize() * dv 173 | # (curMinDist, minTime) = minDistance(curTime, curKick, 10000, false) 174 | 175 | # if curMinDist < bestMinDist: 176 | # bestMinDist = curMinDist 177 | # bestTime = curTime 178 | # bestKick = curKick 179 | # let dv = bestKick.length() 180 | # print "best scan day: ", bestMinDist, bestTime, dv, bestKick 181 | 182 | 183 | bestMinDist=3670923806.137688 184 | bestTime=1594878493.0 185 | bestKick=vec3(3383.63238149, 1497.00765049, 0.03649411) 186 | 187 | 188 | # var 189 | # vecTime: float 190 | # vecKick: Vec3 191 | # for accuracy in [20000]: 192 | # print accuracy 193 | # let maxSearch = accuracy 194 | # var a = 1.0 195 | # var g = 0.10 196 | # for i in 0..maxSearch: 197 | # # random search 198 | # let 199 | # vecTimeD = DAY * rand(-1.0..1.0) 200 | # vecKickD = vec3(rand(-1.0..1.0), rand(-1.0..1.0), rand(-0.1..0.1)) * rand(0.0..100.0) 201 | 202 | # vecTime = vecTime * (1-a) + vecTimeD * a 203 | # vecKick = vecKick * (1-a) + vecKickD * a 204 | 205 | # curTime = bestTime + vecTime * g 206 | # curKick = bestKick + vecKick * g 207 | 208 | # (curMinDist, minTime) = minDistance(curTime, curKick, float(accuracy), false) 209 | 210 | # if curMinDist < bestMinDist: 211 | # bestMinDist = curMinDist 212 | # bestTime = curTime 213 | # bestKick = curKick 214 | 215 | # vecTime *= -0.2 216 | # vecKick *= -0.2 217 | 218 | # a = 0.0 219 | # let dv = bestKick.length() 220 | # print "best search: ", accuracy, i, g, bestMinDist, bestTime, dv, bestKick 221 | 222 | # a = min(1.0, a + 0.01) 223 | 224 | 225 | 226 | 227 | 228 | # var 229 | # adj: array[4, float] 230 | 231 | # let accuracy = 20000 232 | 233 | # for i in 0..1000: 234 | # adj[0] = 0 235 | # adj[1] = 0 236 | # adj[2] = 0 237 | # adj[3] = 0 238 | 239 | # for pick in 0..<4: 240 | # adj[pick] = rand(-1.0 .. +1.0) 241 | 242 | # curTime = bestTime + adj[0] 243 | # curKick.x = bestKick.x + adj[1] 244 | # curKick.y = bestKick.y + adj[2] 245 | # curKick.z = bestKick.z + adj[3] 246 | # (curMinDist, minTime) = minDistance(curTime, curKick, float(accuracy), false) 247 | 248 | # if curMinDist < bestMinDist: 249 | # print "adjusting +1", pick 250 | 251 | # while curMinDist < bestMinDist: 252 | # bestMinDist = curMinDist 253 | # bestTime = curTime 254 | # bestKick = curKick 255 | # let dv = bestKick.length() 256 | # print " best search: ", accuracy, bestMinDist, bestTime, dv, bestKick 257 | 258 | # adj[0] *= 1.2 259 | # adj[1] *= 1.2 260 | # adj[2] *= 1.2 261 | # adj[3] *= 1.2 262 | 263 | # curTime = bestTime + adj[0] 264 | # curKick.x = bestKick.x + adj[1] 265 | # curKick.y = bestKick.y + adj[2] 266 | # curKick.z = bestKick.z + adj[3] 267 | # (curMinDist, minTime) = minDistance(curTime, curKick, float(accuracy), false) 268 | 269 | # adj[0] *= 0.5 270 | # adj[1] *= 0.5 271 | # adj[2] *= 0.5 272 | # adj[3] *= 0.5 273 | # print "end" 274 | 275 | # # flip 276 | # adj[0] *= -1 277 | # adj[1] *= -1 278 | # adj[2] *= -1 279 | # adj[3] *= -1 280 | 281 | # curTime = bestTime + adj[0] 282 | # curKick.x = bestKick.x + adj[1] 283 | # curKick.y = bestKick.y + adj[2] 284 | # curKick.z = bestKick.z + adj[3] 285 | # (curMinDist, minTime) = minDistance(curTime, curKick, float(accuracy), false) 286 | 287 | # if curMinDist < bestMinDist: 288 | # print "adjusting -1", pick 289 | 290 | # while curMinDist < bestMinDist: 291 | # bestMinDist = curMinDist 292 | # bestTime = curTime 293 | # bestKick = curKick 294 | # let dv = bestKick.length() 295 | # print " best search: ", accuracy, bestMinDist, bestTime, dv, bestKick 296 | 297 | # adj[0] *= 1.2 298 | # adj[1] *= 1.2 299 | # adj[2] *= 1.2 300 | # adj[3] *= 1.2 301 | 302 | # curTime = bestTime + adj[0] 303 | # curKick.x = bestKick.x + adj[1] 304 | # curKick.y = bestKick.y + adj[2] 305 | # curKick.z = bestKick.z + adj[3] 306 | # (curMinDist, minTime) = minDistance(curTime, curKick, float(accuracy), false) 307 | 308 | # print "end" 309 | 310 | 311 | 312 | 313 | # let accuracy = 10000 314 | # print accuracy 315 | # let maxSearch = 1000 316 | # var a = 1.0 317 | 318 | # for i in 0..maxSearch: 319 | # # random search 320 | # let 321 | # #vecTime = DAY * rand(-1.0..1.0) 322 | # vecKick = vec3(rand(-1.0..1.0), rand(-1.0..1.0), rand(-0.1..0.1)) * rand(0.0..10.0) 323 | 324 | # let a = 1.0 - pow(i/maxSearch, 1) 325 | 326 | # #curTime = bestTime + vecTime * a 327 | # curKick = bestKick + vecKick * a 328 | 329 | # (curMinDist, minTime) = minDistance(curTime, curKick, float(accuracy), false) 330 | 331 | # if curMinDist < bestMinDist: 332 | # bestMinDist = curMinDist 333 | # bestTime = curTime 334 | # bestKick = curKick 335 | # let dv = bestKick.length() 336 | # print "best search: ", accuracy, bestMinDist, bestTime, dv, bestKick, i, a 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | # best vector search 346 | for accuracy in [10000, 1000, 100, 10, 1]: 347 | bestMinDist = minDistance(bestTime, bestKick, float(accuracy), false)[0] 348 | print accuracy, bestMinDist 349 | 350 | for k in 0..1000: 351 | let a = 100 / pow(float(k+1), 2) 352 | if a < 10: break 353 | 354 | for i in 0..100: 355 | let adjs = @[ 356 | vec3( 1, 0, 0) * a, 357 | vec3( 0, 1, 0) * a, 358 | vec3( 0, 0, 1) * a, 359 | vec3(-1, 0, 0) * a, 360 | vec3( 0, -1, 0) * a, 361 | vec3( 0, 0, -1) * a 362 | ] 363 | var dists = @[0.0,0,0,0,0,0] 364 | 365 | for i in 0..<6: 366 | dists[i] = minDistance(bestTime, bestKick + adjs[i], float(accuracy), false)[0] 367 | #print dists[i], "vs", bestMinDist 368 | 369 | var 370 | minDist = bestMinDist 371 | minI = -1 372 | for i in 0..<6: 373 | if dists[i] < minDist: 374 | minI = i 375 | minDist = dists[i] 376 | if minI == -1: 377 | echo "<==" 378 | break 379 | 380 | bestKick = bestKick + adjs[minI] 381 | bestMinDist = dists[minI] 382 | echo "better!", bestMinDist, adjs[minI] 383 | 384 | 385 | 386 | 387 | discard minDistance(bestTime, bestKick, 10000, true) 388 | 389 | discard surface.writeToPng("tests/earthToMarsSim.png") 390 | 391 | -------------------------------------------------------------------------------- /tests/earthToMarsSim.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treeform/orbits/b53ebe666d9f881fee26fd42bbfba5f17a3b0400/tests/earthToMarsSim.png -------------------------------------------------------------------------------- /tests/generateOrbitalElements.nim: -------------------------------------------------------------------------------- 1 | import strformat 2 | import orbits/simple, orbits/spk, orbits/horizon 3 | import cairo, orbits/vmath64 4 | 5 | var 6 | surface = imageSurfaceCreate(FORMAT_ARGB32, 1000, 1000) 7 | ctx = surface.create() 8 | 9 | ctx.setSourceRGBA(0.11, 0.14, 0.42, 1.0) 10 | ctx.rectangle(0, 0, float surface.getWidth, float surface.getHeight) 11 | ctx.fill() 12 | 13 | let scale = 10.0 14 | 15 | ctx.translate(500, 500) 16 | ctx.scale(scale, scale) 17 | ctx.setLineWidth(1/scale) 18 | 19 | ctx.arc(0, 0, 0.00464, 0.0, 2.0*PI) 20 | ctx.fill() 21 | 22 | var hz = newHorizonClient() 23 | import json 24 | ctx.setSourceRGBA(1, 1, 1, 1) 25 | var newElements = newSeq[OrbitalElements]() 26 | for planet in simpleElements: 27 | let elements = hz.getOrbitalElements( 28 | 0.0, planet.id, 0) 29 | echo planet.name 30 | var planet2 = elements 31 | planet2.id = planet.id 32 | planet2.name = planet.name 33 | newElements.add(planet2) 34 | var step = planet2.period / 360 35 | for i in 0..360: 36 | let time = step * float(i) 37 | let pos = planet2.posAt(time) 38 | ctx.lineTo(pos.x, pos.y) 39 | ctx.stroke() 40 | 41 | echo $(%newElements) 42 | 43 | hz.close() 44 | 45 | discard surface.writeToPng("tests/generatedOE.png") -------------------------------------------------------------------------------- /tests/generateStats.nim: -------------------------------------------------------------------------------- 1 | import strformat 2 | import orbits/simple, orbits/spk, orbits/horizon 3 | import orbits/vmath64 4 | 5 | var hz = newHorizonClient() 6 | 7 | # mercury 58.6462 days (sidereal) 8 | # venus 243.018 days (sidereal, retrograde) 9 | # earth 0.99726968 days 10 | # mars 1.02595676 days 11 | # jupiter 0.41354 days 12 | 13 | 14 | # mercury 0.01 15 | # venus 177.36 16 | # earth 23.45 17 | # mars 25.19 18 | # jupiter 3.13 19 | # saturn 26.73 20 | # uranus 97.77 21 | # neptue 28.32 22 | # pluto 122.53 23 | 24 | 25 | hz.debug = false 26 | 27 | for planetId in [199, 299, 399, 499, 599, 699, 799, 899, 999]: 28 | echo planetId 29 | echo " radius:", hz.getRadius(Y2000, planetId), " m" 30 | echo " axis:", hz.getRotationAxis(Y2000, planetId), " unit vector ", hz.getRotationAxis(Y2000, planetId).angleBetween(vec3(0,0,1))/PI*180, " deg" 31 | var speed = hz.getRotationAngularSpeed(Y2000, planetId) 32 | echo " angular speed:", speed, " rad/s ", (2*PI/speed)/DAY, " days" 33 | 34 | 35 | hz.close() 36 | -------------------------------------------------------------------------------- /tests/generatedOE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treeform/orbits/b53ebe666d9f881fee26fd42bbfba5f17a3b0400/tests/generatedOE.png -------------------------------------------------------------------------------- /tests/matching.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treeform/orbits/b53ebe666d9f881fee26fd42bbfba5f17a3b0400/tests/matching.png -------------------------------------------------------------------------------- /tests/merkuryPrecession.nim: -------------------------------------------------------------------------------- 1 | # import orbits/simple, orbits/complex 2 | # import cairo, math 3 | 4 | # var 5 | # surface = imageSurfaceCreate(FORMAT_ARGB32, 1000, 1000) 6 | # ctx = surface.create() 7 | 8 | # ctx.setSourceRGBA(0.11, 0.14, 0.42, 1.0) 9 | # ctx.rectangle(0, 0, float surface.getWidth, float surface.getHeight) 10 | # ctx.fill() 11 | 12 | # ctx.translate(500, 500) 13 | # ctx.scale(1000, 1000) 14 | 15 | # ctx.setSourceRGBA(1, 1, 1, 1) 16 | # ctx.setLineWidth(0.001) 17 | 18 | # # draw orbit of merkury 10 times showing shift over 500y 19 | # let shiftBy = 500.0 * (60*60*24*365) 20 | # for j in 0..10: 21 | # ctx.setSourceRGBA(1, 1, 1, float(j) / 10 * 0.8 + 0.2) 22 | # let planet = complexElements[0] # merkury 23 | # var step = planet.period / 360 24 | # for i in 0..360: 25 | # let pos = planet.posAt(step * float(i) + float(j) * shiftBy) 26 | # ctx.lineTo(pos.x, pos.y) 27 | # ctx.stroke() 28 | 29 | # ctx.arc(0, 0, 0.00464, 0.0, 2.0*PI) 30 | # ctx.fill() 31 | 32 | # discard surface.writeToPng("tests/merkuryPrecession.png") 33 | 34 | 35 | import orbits/simple, orbits/spk 36 | import cairo, orbits/vmath64 37 | 38 | var 39 | surface = imageSurfaceCreate(FORMAT_ARGB32, 1000, 1000) 40 | ctx = surface.create() 41 | 42 | ctx.setSourceRGBA(0.11, 0.14, 0.42, 1.0) 43 | ctx.rectangle(0, 0, float surface.getWidth, float surface.getHeight) 44 | ctx.fill() 45 | 46 | ctx.translate(500, 500) 47 | ctx.scale(1000, 1000) 48 | 49 | ctx.setSourceRGBA(1, 1, 1, 1) 50 | ctx.setLineWidth(0.001) 51 | 52 | ctx.setSourceRGBA(1, 1, 1, 1) 53 | var spkFile = readSpk("de435.bsp") 54 | 55 | # draw orbit of merkury 5 times showing shift over 10 earth years 56 | let shiftBy = 10.0 * (60*60*24*365) 57 | for j in 0..5: 58 | ctx.setSourceRGBA(1, 1, 1, float(j) / 5 * 0.8 + 0.2) 59 | let planetId = 1 # merkury 60 | var step = simpleElements[0].period / 360 61 | for i in 0..360: 62 | let time = step * float(i) + shiftBy * float(j) 63 | let pos = spkFile.posAt(time, planetId, 0) / AU 64 | ctx.lineTo(pos.x, pos.y) 65 | ctx.stroke() 66 | 67 | ctx.arc(0, 0, 0.00464, 0.0, 2.0*PI) 68 | ctx.fill() 69 | 70 | discard surface.writeToPng("tests/merkuryPrecession.png") -------------------------------------------------------------------------------- /tests/merkuryPrecession.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treeform/orbits/b53ebe666d9f881fee26fd42bbfba5f17a3b0400/tests/merkuryPrecession.png -------------------------------------------------------------------------------- /tests/milkyway.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treeform/orbits/b53ebe666d9f881fee26fd42bbfba5f17a3b0400/tests/milkyway.png -------------------------------------------------------------------------------- /tests/nim.cfg: -------------------------------------------------------------------------------- 1 | --path:"../src/" 2 | -------------------------------------------------------------------------------- /tests/orbitsHorizon.nim: -------------------------------------------------------------------------------- 1 | import strformat 2 | import orbits/simple, orbits/spk, orbits/horizon 3 | import cairo, orbits/vmath64 4 | 5 | var 6 | surface = imageSurfaceCreate(FORMAT_ARGB32, 1000, 1000) 7 | ctx = surface.create() 8 | 9 | ctx.setSourceRGBA(0.11, 0.14, 0.42, 1.0) 10 | ctx.rectangle(0, 0, float surface.getWidth, float surface.getHeight) 11 | ctx.fill() 12 | 13 | let scale = 10.0 14 | 15 | ctx.translate(500, 500) 16 | ctx.scale(scale, scale) 17 | ctx.setLineWidth(1/scale) 18 | 19 | ctx.setSourceRGBA(1, 1, 1, 1) 20 | ctx.arc(0, 0, 0.00464, 0.0, 2.0*PI) 21 | ctx.fill() 22 | 23 | var hz = newHorizonClient() 24 | 25 | # simple orbits in red 26 | ctx.setSourceRGBA(1, 0, 0, 1) 27 | for planet in simpleElements: 28 | var step = planet.period / 360 29 | for i in 0..360: 30 | let time = step * float(i) 31 | let pos = planet.posAt(time) / AU 32 | ctx.lineTo(pos.x, pos.y) 33 | ctx.closePath() 34 | ctx.stroke() 35 | 36 | #spk orbits in blue 37 | downloadSpk("de435.bsp") 38 | ctx.setSourceRGBA(0, 1, 0, 1) 39 | var spkFile = readSpk("de435.bsp") 40 | for planet in simpleElements: 41 | var step = planet.period / 360 42 | for i in 0..360: 43 | let time = step * float(i) 44 | let pos = spkFile.posAt(time, planet.id, 0) / AU 45 | ctx.lineTo(pos.x, pos.y) 46 | ctx.closePath() 47 | ctx.stroke() 48 | 49 | #horizon orbits in white 50 | ctx.setSourceRGBA(1, 1, 1, 1) 51 | for planet in simpleElements: 52 | let entries = hz.getOrbitalVectorsSeq( 53 | fromTime = 0.0, 54 | toTime = planet.period, 55 | steps = 360, 56 | targetId = planet.id, 57 | observerId = 0) 58 | for entry in entries: 59 | let pos = entry.pos / AU 60 | ctx.lineTo(pos.x, pos.y) 61 | ctx.closePath() 62 | ctx.stroke() 63 | hz.close() 64 | 65 | discard surface.writeToPng("tests/orbitsHorizon.png") -------------------------------------------------------------------------------- /tests/orbitsHorizon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treeform/orbits/b53ebe666d9f881fee26fd42bbfba5f17a3b0400/tests/orbitsHorizon.png -------------------------------------------------------------------------------- /tests/orbitsSimple.nim: -------------------------------------------------------------------------------- 1 | import orbits/simple, orbits/vmath64, cairo 2 | 3 | var 4 | surface = imageSurfaceCreate(FORMAT_ARGB32, 1000, 1000) 5 | ctx = surface.create() 6 | 7 | ctx.setSourceRGB(0.11, 0.14, 0.42) 8 | ctx.rectangle(0, 0, float surface.getWidth, float surface.getHeight) 9 | ctx.fill() 10 | 11 | 12 | let scale = 10.0 13 | 14 | ctx.translate(500, 500) 15 | ctx.scale(scale, scale) 16 | ctx.setLineWidth(1/scale) 17 | 18 | ctx.setSourceRGBA(1, 1, 1, 1) 19 | ctx.arc(0, 0, 0.00464, 0.0, 2.0*PI) 20 | ctx.fill() 21 | 22 | 23 | for planet in simpleElements: 24 | var step = planet.period / 360 25 | for i in 0..360: 26 | let pos = planet.posAt(step * float(i)) / AU 27 | ctx.lineTo(pos.x, pos.y) 28 | ctx.stroke() 29 | 30 | discard surface.writeToPng("tests/orbitsSimple.png") -------------------------------------------------------------------------------- /tests/orbitsSimple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treeform/orbits/b53ebe666d9f881fee26fd42bbfba5f17a3b0400/tests/orbitsSimple.png -------------------------------------------------------------------------------- /tests/orbitsSimulated.nim: -------------------------------------------------------------------------------- 1 | import strformat 2 | import orbits/simple, orbits/spk, orbits/horizon 3 | import cairo, orbits/vmath64 4 | 5 | var 6 | surface = imageSurfaceCreate(FORMAT_ARGB32, 1000, 1000) 7 | ctx = surface.create() 8 | 9 | ctx.setSourceRGBA(0.11, 0.14, 0.42, 1.0) 10 | ctx.rectangle(0, 0, float surface.getWidth, float surface.getHeight) 11 | ctx.fill() 12 | 13 | let scale = 250.0 14 | 15 | ctx.translate(500, 500) 16 | ctx.scale(scale, scale) 17 | ctx.setLineWidth(1/scale) 18 | 19 | ctx.setSourceRGBA(1, 1, 1, 1) 20 | ctx.arc(0, 0, 0.00464, 0.0, 2.0*PI) 21 | ctx.fill() 22 | 23 | var hz = newHorizonClient() 24 | 25 | type Body = object 26 | id: int 27 | pos: Vec3 28 | vel: Vec3 29 | mass: float 30 | var bodies: seq[Body] 31 | 32 | # sun 33 | var body = Body() 34 | body.id = 0 35 | bodies.add body 36 | 37 | # get inital obits from horizon 38 | ctx.setSourceRGBA(1, 1, 1, 0.1) 39 | for planet in simpleElements: 40 | let entries = hz.getOrbitalVectorsSeq( 41 | fromTime = 0.0, 42 | toTime = planet.period, 43 | steps = 360, 44 | targetId = planet.id, 45 | observerId = 0) 46 | var body = Body() 47 | body.id = planet.id 48 | body.pos = entries[0].pos 49 | body.vel = entries[0].vel 50 | bodies.add body 51 | 52 | for entry in entries: 53 | let pos = entry.pos / AU 54 | ctx.lineTo(pos.x, pos.y) 55 | ctx.closePath() 56 | ctx.stroke() 57 | 58 | const G = 6.674E-11 59 | const HOUR = 60 * 60 60 | const MINUTE = 60 61 | bodies[0].mass = 1.98847E+30 62 | bodies[1].mass = 3.30E+23 63 | bodies[2].mass = 4.87E+24 64 | bodies[3].mass = 5.97E+24 65 | bodies[4].mass = 6.42E+23 66 | bodies[5].mass = 1.90E+27 67 | bodies[6].mass = 5.68E+26 68 | bodies[7].mass = 8.68E+25 69 | bodies[8].mass = 1.02E+26 70 | bodies[9].mass = 1.46E+22 71 | 72 | let maxStep = 1000000 73 | let stepTime = 10.0 74 | for step in 0..maxStep: 75 | 76 | for body in bodies.mitems: 77 | body.pos = body.pos + body.vel * stepTime 78 | 79 | for other in bodies: 80 | if body.id == other.id: continue 81 | let offset = other.pos - body.pos 82 | let dv = G * other.mass / pow(offset.length, 2) 83 | let direction = offset.normalize() 84 | body.vel = body.vel + direction * dv * stepTime 85 | 86 | 87 | if step mod (maxStep div 50) == 0: 88 | 89 | for body in bodies: 90 | ctx.setSourceRGBA(1, 1, 1, step/maxStep + 0.1) 91 | ctx.arc(body.pos.x/AU, body.pos.y/AU, 3/scale, 0.0, 2.0*PI) 92 | ctx.fill() 93 | 94 | 95 | hz.close() 96 | 97 | 98 | discard surface.writeToPng("tests/orbitsSimulated.png") -------------------------------------------------------------------------------- /tests/orbitsSimulated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treeform/orbits/b53ebe666d9f881fee26fd42bbfba5f17a3b0400/tests/orbitsSimulated.png -------------------------------------------------------------------------------- /tests/orbitsSpk.nim: -------------------------------------------------------------------------------- 1 | import orbits/simple, orbits/spk 2 | import cairo, orbits/vmath64 3 | 4 | var 5 | surface = imageSurfaceCreate(FORMAT_ARGB32, 1000, 1000) 6 | ctx = surface.create() 7 | 8 | ctx.setSourceRGBA(0.11, 0.14, 0.42, 1.0) 9 | ctx.rectangle(0, 0, float surface.getWidth, float surface.getHeight) 10 | ctx.fill() 11 | 12 | let scale = 10.0 13 | 14 | ctx.translate(500, 500) 15 | ctx.scale(scale, scale) 16 | ctx.setLineWidth(1/scale) 17 | 18 | ctx.setSourceRGBA(1, 0, 0, 1) 19 | for planet in simpleElements: 20 | var step = planet.period / 360 21 | for i in 0..360: 22 | let time = step * float(i) 23 | let pos = planet.posAt(time) / AU 24 | ctx.lineTo(pos.x, pos.y) 25 | ctx.stroke() 26 | 27 | ctx.setSourceRGBA(1, 1, 1, 1) 28 | 29 | downloadSpk("de435.bsp") 30 | 31 | var spkFile = readSpk("de435.bsp") 32 | for planet in simpleElements: 33 | var step = planet.period / 360 34 | for i in 0..360: 35 | let time = step * float(i) 36 | let pos = spkFile.posAt(time, planet.id, 0) / AU 37 | ctx.lineTo(pos.x, pos.y) 38 | ctx.stroke() 39 | 40 | 41 | ctx.arc(0, 0, 0.00464, 0.0, 2.0*PI) 42 | ctx.fill() 43 | 44 | discard surface.writeToPng("tests/orbitsSpk.png") -------------------------------------------------------------------------------- /tests/orbitsSpk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treeform/orbits/b53ebe666d9f881fee26fd42bbfba5f17a3b0400/tests/orbitsSpk.png -------------------------------------------------------------------------------- /tests/solarsystemSpk.nim: -------------------------------------------------------------------------------- 1 | import strformat, times 2 | import orbits/simple, orbits/spk#, orbits/vmath64 3 | import pixie/demo, vmath 4 | 5 | let now = epochTime() 6 | 7 | 8 | downloadSpk("de435.bsp") 9 | 10 | var spkFile = readSpk("de435.bsp") 11 | 12 | var currentTime = 0.0 13 | 14 | start("Solar System from NASA's SPK files that use Chebyshev polynomials") 15 | while true: 16 | screen.fill(color(0.11, 0.14, 0.42, 1.0)) 17 | 18 | let scaleK = 10.0 19 | var mat = translate(vec2(400, 300)) * scale(vec2(scaleK, scaleK)) 20 | 21 | var path: Path 22 | path.circle(vec2(0, 0), 0.1) 23 | screen.fillPath(path, color(1,1,1,1), mat) 24 | 25 | for planet in simpleElements: 26 | var step = planet.period / 360 27 | var path: Path 28 | 29 | for i in 0..360: 30 | let time = step * float(i) 31 | let pos = spkFile.posAt(time, planet.id, 0) / AU 32 | if i == 0: 33 | path.moveTo(pos.x.float32, pos.y.float32) 34 | else: 35 | path.lineTo(pos.x.float32, pos.y.float32) 36 | path.closePath() 37 | 38 | 39 | var currentPos = spkFile.posAt(currentTime, planet.id, 0) / AU 40 | var pathBody: Path 41 | pathBody.circle(vec3(currentPos).xy, 0.1) 42 | screen.fillPath(pathBody, color(1,1,1,1), mat) 43 | 44 | screen.strokePath(path, color(1, 1, 1, 0.1), mat, strokeWidth = 0.1) 45 | 46 | tick() 47 | 48 | currentTime += 60*60*25 * 7 # week per frame 49 | -------------------------------------------------------------------------------- /tests/solvertest.nim: -------------------------------------------------------------------------------- 1 | import orbits/solvers, print 2 | 3 | 4 | print MACHINE_EPSILON 5 | 6 | block: 7 | func f1(x: float): float = x * x + x - 1 8 | for i in 0 .. 10: 9 | let x = float(i)/10.0 10 | print x, f1(x) 11 | echo goldenSectionSearch(0, 1, f1) 12 | 13 | func f2(x: float): float = - x * x + x 14 | for i in 0 .. 10: 15 | let x = float(i)/10.0 16 | print x, f2(x) 17 | echo goldenSectionSearch(0.1, 1, f2) 18 | 19 | 20 | func f3(x: float): float = x * x - x 21 | for i in 0 .. 10: 22 | let x = float(i)/10.0 23 | print x, f3(x) 24 | echo goldenSectionSearch(0.0, 1, f3) 25 | 26 | block: 27 | proc f1(x: float): float = x * x + x 28 | proc f2(x: float): float = 2 * x + 1 29 | for i in 0 .. 10: 30 | let x = float(i)/10.0 31 | print x, f1(x), f2(x) 32 | echo newtonsRoot(0.1, f1, f2) -------------------------------------------------------------------------------- /tests/spacecraft.nim: -------------------------------------------------------------------------------- 1 | import strformat, times 2 | import orbits/simple, orbits/spk, orbits/horizon 3 | import cairo, orbits/vmath64 4 | 5 | var 6 | surface = imageSurfaceCreate(FORMAT_ARGB32, 1000, 1000) 7 | ctx = surface.create() 8 | 9 | ctx.setSourceRGBA(0.11, 0.14, 0.42, 1.0) 10 | ctx.rectangle(0, 0, float surface.getWidth, float surface.getHeight) 11 | ctx.fill() 12 | 13 | let scale = 3.5 14 | 15 | ctx.translate(500, 500) 16 | ctx.scale(scale, scale) 17 | 18 | 19 | ctx.selectFontFace("Sans", FONT_SLANT_NORMAL, FONT_WEIGHT_NORMAL) 20 | ctx.setFontSize(12.0/scale) 21 | 22 | ctx.setSourceRGBA(1, 1, 1, 1) 23 | ctx.arc(0, 0, 0.00464, 0.0, 2.0*PI) 24 | ctx.fill() 25 | 26 | var hz = newHorizonClient() 27 | 28 | # simple orbits in red 29 | ctx.setSourceRGBA(1, 1, 1, 0.1) 30 | ctx.setLineWidth(2/scale) 31 | for planet in simpleElements: 32 | var step = planet.period / 360 33 | for i in 0..360: 34 | let time = step * float(i) 35 | let pos = planet.posAt(time) / AU 36 | ctx.lineTo(pos.x, pos.y) 37 | ctx.closePath() 38 | ctx.stroke() 39 | 40 | ctx.setLineWidth(1/scale) 41 | 42 | block: 43 | #plot voyager1 44 | ctx.setSourceRGBA(1, 1, 1, 1) 45 | let id = -31 46 | let entries = hz.getOrbitalVectorsSeq( 47 | 242290800+DAY, 48 | 1552284021, 49 | 1000, 50 | id, 51 | 0) 52 | ctx.newPath() 53 | for entry in entries: 54 | let pos = entry.pos / AU 55 | ctx.lineTo(pos.x, pos.y) 56 | ctx.stroke() 57 | 58 | let pos = entries[^1].pos / AU 59 | ctx.lineTo(pos.x, pos.y) 60 | ctx.showText(" Voyager 1") 61 | 62 | 63 | block: 64 | #plot voyager2 65 | ctx.setSourceRGBA(1, 1, 1, 1) 66 | let id = -32 67 | let entries = hz.getOrbitalVectorsSeq( 68 | 240908400+DAY, 69 | 1552284021, 70 | 1000, 71 | id, 72 | 0) 73 | ctx.newPath() 74 | for entry in entries: 75 | let pos = entry.pos / AU 76 | ctx.lineTo(pos.x, pos.y) 77 | ctx.stroke() 78 | 79 | let pos = entries[^1].pos / AU 80 | ctx.lineTo(pos.x, pos.y) 81 | ctx.showText(" Voyager 2") 82 | 83 | block: 84 | #plot new horizons 85 | ctx.setSourceRGBA(1, 1, 1, 1) 86 | let id = -98 87 | let entries = hz.getOrbitalVectorsSeq( 88 | 1137657600+DAY, 89 | 1552284021, 90 | 1000, 91 | id, 92 | 0) 93 | ctx.newPath() 94 | for entry in entries: 95 | let pos = entry.pos / AU 96 | ctx.lineTo(pos.x, pos.y) 97 | ctx.stroke() 98 | 99 | let pos = entries[^1].pos / AU 100 | ctx.lineTo(pos.x, pos.y) 101 | ctx.showText(" New Horizon") 102 | 103 | 104 | # block: 105 | # #plot Dawn 106 | # ctx.setSourceRGBA(1, 1, 1, 1) 107 | # let voyager2Id = -203 108 | # let entries = hz.getOrbitalVectorsSeq( 109 | # 1190876400 + DAY, 110 | # 1552284021, 111 | # 1000, 112 | # voyager2Id, 113 | # 0) 114 | # ctx.newPath() 115 | # for entry in entries: 116 | # let pos = entry.pos / AU 117 | # ctx.lineTo(pos.x, pos.y) 118 | # ctx.stroke() 119 | 120 | # let pos = entries[^1].pos / AU 121 | # ctx.lineTo(pos.x, pos.y) 122 | # ctx.showText(" Dawn") 123 | 124 | 125 | hz.close() 126 | 127 | discard surface.writeToPng("tests/spacecraft.png") -------------------------------------------------------------------------------- /tests/spacecraft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treeform/orbits/b53ebe666d9f881fee26fd42bbfba5f17a3b0400/tests/spacecraft.png -------------------------------------------------------------------------------- /tests/starman.nim: -------------------------------------------------------------------------------- 1 | import strformat, times 2 | import orbits/simple, orbits/spk, orbits/horizon 3 | import cairo, orbits/vmath64 4 | 5 | let now = epochTime() 6 | 7 | var 8 | surface = imageSurfaceCreate(FORMAT_ARGB32, 1000, 1000) 9 | ctx = surface.create() 10 | 11 | ctx.setSourceRGBA(0.11, 0.14, 0.42, 1.0) 12 | ctx.rectangle(0, 0, float surface.getWidth, float surface.getHeight) 13 | ctx.fill() 14 | 15 | let scale = 250.0 16 | 17 | ctx.translate(500, 500) 18 | ctx.scale(scale, scale) 19 | 20 | 21 | ctx.selectFontFace("Sans", FONT_SLANT_NORMAL, FONT_WEIGHT_NORMAL) 22 | ctx.setFontSize(12.0/scale) 23 | 24 | ctx.setSourceRGBA(1, 1, 1, 1) 25 | ctx.arc(0, 0, 0.00464, 0.0, 2.0*PI) 26 | ctx.fill() 27 | 28 | var hz = newHorizonClient() 29 | 30 | # simple orbits in red 31 | ctx.setSourceRGBA(1, 1, 1, 0.1) 32 | ctx.setLineWidth(2/scale) 33 | for planet in simpleElements: 34 | var step = planet.period / 360 35 | for i in 0..360: 36 | let time = step * float(i) 37 | let pos = planet.posAt(time) / AU 38 | ctx.lineTo(pos.x, pos.y) 39 | ctx.closePath() 40 | ctx.stroke() 41 | 42 | ctx.setLineWidth(1/scale) 43 | 44 | 45 | block: 46 | ctx.setSourceRGBA(1, 1, 1, 1) 47 | let id = -143205 48 | let entries = hz.getOrbitalVectorsSeq( 49 | 1517904000+DAY, 50 | now, 51 | 1000, 52 | id, 53 | 0) 54 | ctx.newPath() 55 | for entry in entries: 56 | let pos = entry.pos / AU 57 | ctx.lineTo(pos.x, pos.y) 58 | ctx.stroke() 59 | 60 | let pos = entries[^1].pos / AU 61 | ctx.lineTo(pos.x, pos.y) 62 | 63 | 64 | echo entries[^1].pos.length / AU 65 | echo entries[^1].vel.length 66 | 67 | let earthOv = hz.getOrbitalVectors( 68 | now, 69 | 399, 70 | 0) 71 | 72 | let 73 | vel = int(entries[^1].vel.length) 74 | dist = int((earthOv.pos - entries[^1].pos).length / 1000 - 6378.1) 75 | 76 | ctx.save() 77 | ctx.moveTo(pos.x, pos.y) 78 | ctx.showText(" Starman") 79 | ctx.restore() 80 | ctx.save() 81 | ctx.moveTo(pos.x, pos.y+20/scale) 82 | ctx.showText(&" {$vel}m/s") 83 | ctx.restore() 84 | ctx.save() 85 | ctx.moveTo(pos.x, pos.y+40/scale) 86 | ctx.showText(&" {$dist}km") 87 | ctx.restore() 88 | 89 | hz.close() 90 | 91 | discard surface.writeToPng("tests/starman.png") -------------------------------------------------------------------------------- /tests/starman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treeform/orbits/b53ebe666d9f881fee26fd42bbfba5f17a3b0400/tests/starman.png -------------------------------------------------------------------------------- /tests/starman2.nim: -------------------------------------------------------------------------------- 1 | import strformat, times 2 | import orbits/simple, orbits/spk, orbits/horizon 3 | import pixie/demo, vmath 4 | 5 | downloadSpk("de435.bsp") 6 | 7 | var spkFile = readSpk("de435.bsp") 8 | 9 | var fromTime = 1517904000.float64 + DAY # starman launch date 10 | let toTime = epochTime() 11 | let id = -143205 # starman ID 12 | 13 | var currentTime = fromTime 14 | 15 | var hz = newHorizonClient() 16 | let hzStarManVecs = hz.getOrbitalVectorsSeq( 17 | fromTime, 18 | toTime, 19 | 1000, 20 | id, 21 | 0 22 | ) 23 | 24 | proc computePos(entries: seq[OrbitalVectors], time: float64): DVec3 = 25 | for i, entry in entries: 26 | if entry.time < time: 27 | continue 28 | assert entries[i-1].time < time and entries[i].time > time 29 | let 30 | a = entries[i-1] 31 | b = entries[i] 32 | timeDelta = a.time - b.time 33 | return lerp(b.pos, a.pos, (time - a.time)/timeDelta) 34 | 35 | start("Where is SpaceX's Starman?") 36 | while true: 37 | screen.fill(color(0.11, 0.14, 0.42, 1.0)) 38 | 39 | let scaleK = 100.0 40 | var mat = translate(vec2(400, 300)) * scale(vec2(scaleK, scaleK)) 41 | 42 | var path: Path 43 | path.circle(vec2(0, 0), 0.1) 44 | screen.fillPath(path, color(1,1,1,1), mat) 45 | 46 | for planet in simpleElements: 47 | var step = planet.period / 360 48 | var path: Path 49 | 50 | for i in 0..360: 51 | let time = step * float(i) 52 | let pos = spkFile.posAt(time, planet.id, 0) / AU 53 | if i == 0: 54 | path.moveTo(pos.x.float32, pos.y.float32) 55 | else: 56 | path.lineTo(pos.x.float32, pos.y.float32) 57 | path.closePath() 58 | screen.strokePath(path, color(1, 1, 1, 0.1), mat, strokeWidth = 0.01) 59 | 60 | var currentPos = spkFile.posAt(currentTime, planet.id, 0) / AU 61 | var pathBody: Path 62 | pathBody.circle(vec3(currentPos).xy, 0.01) 63 | screen.fillPath(pathBody, color(1,1,1,1), mat) 64 | 65 | # drawing the starman 66 | block: 67 | var path: Path 68 | for i, entry in hzStarManVecs: 69 | let pos = entry.pos / AU 70 | if i == 0: 71 | path.moveTo(pos.x.float32, pos.y.float32) 72 | else: 73 | path.lineTo(pos.x.float32, pos.y.float32) 74 | path.closePath() 75 | screen.strokePath(path, color(1, 1, 1, 0.1), mat, strokeWidth = 0.02) 76 | 77 | var currentPos = computePos(hzStarManVecs, currentTime) / AU 78 | var pathBody: Path 79 | pathBody.circle(vec3(currentPos).xy, 0.02) 80 | screen.fillPath(pathBody, color(1,1,1,1), mat) 81 | 82 | tick() 83 | 84 | currentTime += 60*60*25 # day per frame 85 | 86 | currentTime = min(currentTime, toTime) 87 | if isKeyDown(KEY_SPACE): 88 | currentTime = fromTime 89 | -------------------------------------------------------------------------------- /tests/test.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | typedef struct Result_distance { 6 | double* pA; 7 | double* pB; 8 | double d; 9 | } Result_distance; 10 | 11 | double determinante3(double* a, double* v1, double* v2){ 12 | return a[0] * (v1[1] * v2[2] - v1[2] * v2[1]) + a[1] * (v1[2] * v2[0] - v1[0] * v2[2]) + a[2] * (v1[0] * v2[1] - v1[1] * v2[0]); 13 | } 14 | 15 | double* cross3(double* v1, double* v2){ 16 | double* v = (double*)malloc(3 * sizeof(double)); 17 | v[0] = v1[1] * v2[2] - v1[2] * v2[1]; 18 | v[1] = v1[2] * v2[0] - v1[0] * v2[2]; 19 | v[2] = v1[0] * v2[1] - v1[1] * v2[0]; 20 | return v; 21 | } 22 | 23 | double dot3(double* v1, double* v2){ 24 | return v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2]; 25 | } 26 | 27 | double norma3(double* v1){ 28 | double soma = 0; 29 | for (int i = 0; i < 3; i++) { 30 | soma += pow(v1[i], 2); 31 | } 32 | return sqrt(soma); 33 | } 34 | 35 | double* multiplica3(double* v1, double v){ 36 | double* v2 = (double*)malloc(3 * sizeof(double)); 37 | for (int i = 0; i < 3; i++) { 38 | v2[i] = v1[i] * v; 39 | } 40 | return v2; 41 | } 42 | 43 | double* soma3(double* v1, double* v2, int sinal){ 44 | double* v = (double*)malloc(3 * sizeof(double)); 45 | for (int i = 0; i < 3; i++) { 46 | v[i] = v1[i] + sinal * v2[i]; 47 | } 48 | return v; 49 | } 50 | 51 | Result_distance* closestDistanceBetweenLines(double* a0, double* a1, double* b0, double* b1, int clampAll, int clampA0, int clampA1, int clampB0, int clampB1){ 52 | double denom, det0, det1, t0, t1, d; 53 | double *A, *B, *_A, *_B, *cross, *t, *pA, *pB; 54 | Result_distance *rd = (Result_distance *)malloc(sizeof(Result_distance)); 55 | 56 | if (clampAll){ 57 | clampA0 = 1; 58 | clampA1 = 1; 59 | clampB0 = 1; 60 | clampB1 = 1; 61 | } 62 | 63 | A = soma3(a1, a0, -1); 64 | B = soma3(b1, b0, -1); 65 | _A = multiplica3(A, 1 / norma3(A)); 66 | _B = multiplica3(B, 1 / norma3(B)); 67 | cross = cross3(_A, _B); 68 | denom = pow(norma3(cross), 2); 69 | 70 | printf("denom = %f\n", denom); 71 | 72 | if (denom == 0){ 73 | double d0 = dot3(_A, soma3(b0, a0, -1)); 74 | d = norma3(soma3(soma3(multiplica3(_A, d0), a0, 1), b0, -1)); 75 | printf("d = %f\n", d); 76 | if (clampA0 || clampA1 || clampB0 || clampB1){ 77 | double d1 = dot3(_A, soma3(b1, a0, -1)); 78 | if (d0 <= 0 && 0 >= d1){ 79 | if (clampA0 && clampB1){ 80 | if (abs(d0) < abs(d1)){ 81 | rd->pA = b0; 82 | rd->pB = a0; 83 | rd->d = norma3(soma3(b0, a0, -1)); 84 | } 85 | else{ 86 | rd->pA = b1; 87 | rd->pB = a0; 88 | rd->d = norma3(soma3(b1, a0, -1)); 89 | } 90 | } 91 | } 92 | else if (d0 >= norma3(A) && norma3(A) <= d1){ 93 | if (clampA1 && clampB0){ 94 | if (abs(d0) pA = b0; 96 | rd->pB = a1; 97 | rd->d = norma3(soma3(b0, a1, -1)); 98 | } 99 | else{ 100 | rd->pA = b1; 101 | rd->pB = a1; 102 | rd->d = norma3(soma3(b1, a1, -1)); 103 | } 104 | } 105 | } 106 | } 107 | else{ 108 | rd->pA = NULL; 109 | rd->pB = NULL; 110 | rd->d = d; 111 | } 112 | } 113 | else{ 114 | t = soma3(b0, a0, -1); 115 | det0 = determinante3(t, _B, cross); 116 | det1 = determinante3(t, _A, cross); 117 | t0 = det0 / denom; 118 | t1 = det1 / denom; 119 | pA = soma3(a0, multiplica3(_A, t0), 1); 120 | pB = soma3(b0, multiplica3(_B, t1), 1); 121 | 122 | if (clampA0 || clampA1 || clampB0 || clampB1){ 123 | if (t0 < 0 && clampA0) 124 | pA = a0; 125 | else if (t0 > norma3(A) && clampA1) 126 | pA = a1; 127 | if (t1 < 0 && clampB0) 128 | pB = b0; 129 | else if (t1 > norma3(B) && clampB1) 130 | pB = b1; 131 | } 132 | 133 | d = norma3(soma3(pA, pB, -1)); 134 | 135 | rd->pA = pA; 136 | rd->pB = pB; 137 | rd->d = d; 138 | } 139 | 140 | free(A); 141 | free(B); 142 | free(cross); 143 | free(t); 144 | return rd; 145 | } 146 | 147 | int main(void){ 148 | //example 149 | double a1[] = { 13.43, 21.77, 46.81 }; 150 | double a0[] = { 27.83, 31.74, -26.60 }; 151 | double b0[] = { 77.54, 7.53, 6.22 }; 152 | double b1[] = { 26.99, 12.39, 11.18 }; 153 | 154 | Result_distance* rd = closestDistanceBetweenLines(a0, a1, b0, b1, 1, 0, 0, 0, 0); 155 | printf("pA = [%f, %f, %f]\n", rd->pA[0], rd->pA[1], rd->pA[2]); 156 | printf("pB = [%f, %f, %f]\n", rd->pB[0], rd->pB[1], rd->pB[2]); 157 | printf("d = %f\n", rd->d); 158 | return 0; 159 | } -------------------------------------------------------------------------------- /tests/test.nim: -------------------------------------------------------------------------------- 1 | import print, vmath 2 | 3 | proc det(a, v1, v2: Vec3): float = 4 | return 5 | a.x * (v1.y * v2.z - v1.z * v2.y) + 6 | a.y * (v1.z * v2.x - v1.x * v2.z) + 7 | a.z * (v1.x * v2.y - v1.y * v2.x) 8 | 9 | 10 | proc computeLineToLine( 11 | a0, a1, b0, b1: Vec3, 12 | clampA0, clampA1, clampB0, clampB1: bool 13 | ): (Vec3, Vec3, float32) = 14 | 15 | let 16 | a = a1 - a0 17 | b = b1 - b0 18 | aNorm = a.normalize() 19 | bNorm = b.normalize() 20 | cross = cross(aNorm, bNorm) 21 | denom = pow(cross.length, 2) 22 | 23 | if denom == 0: 24 | let 25 | d0 = dot(aNorm, b0 - a0) 26 | if clampA0 or clampA1 or clampB0 or clampB1: 27 | let d1 = dot(aNorm, b1 - a0) 28 | if d0 <= 0 and 0 >= d1: 29 | if clampA0 and clampB1: 30 | if abs(d0) < abs(d1): 31 | return (b0, a0, (b0 - a0).length) 32 | else: 33 | return (b1, a0, (b1 - a0).length) 34 | elif d0 >= a.length and a.length <= d1: 35 | if clampA1 and clampB0: 36 | if abs(d0) < abs(d1): 37 | return (b0, a1, (b0 - a1).length) 38 | else: 39 | return (b1, a1, (b1 - a1).length) 40 | else: 41 | let d = (aNorm * d0 + a0 - b0).length 42 | return (vec3(0,0,0), vec3(0,0,0), d) 43 | else: 44 | let 45 | t = b0 - a0 46 | det0 = det(t, bNorm, cross) 47 | det1 = det(t, aNorm, cross) 48 | t0 = det0 / denom 49 | t1 = det1 / denom 50 | var 51 | pA = a0 + aNorm * t0 52 | pB = b0 + bNorm * t1 53 | 54 | if clampA0 or clampA1 or clampB0 or clampB1: 55 | if t0 < 0 and clampA0: 56 | pA = a0 57 | elif t0 > a.length and clampA1: 58 | pA = a1 59 | if t1 < 0 and clampB0: 60 | pB = b0; 61 | elif t1 > b.length and clampB1: 62 | pB = b1; 63 | 64 | var d = (pA - pB).length 65 | 66 | return (pA, pB, d) 67 | 68 | 69 | proc computeSegToSeg(a0, a1, b0, b1: Vec3): (Vec3, Vec3, float32) = 70 | computeLineToLine(a0, a1, b0, b1, true, true, true, true) 71 | 72 | 73 | 74 | when isMainModule: 75 | proc almostEquals(a, b: float): bool = abs(a - b) < 0.00001 76 | 77 | block: 78 | let 79 | a1 = vec3(0, 0, 0) 80 | a0 = vec3(0, 1, 0) 81 | b0 = vec3(1, 0, 0) 82 | b1 = vec3(1, 1, 0) 83 | let r = computeLineToLine(a0, a1, b0, b1, false, false, false, false) 84 | assert r[0] == vec3(0, 0, 0) 85 | assert r[1] == vec3(0, 0, 0) 86 | assert r[2] == 1.0 87 | 88 | 89 | block: 90 | let 91 | a1 = vec3(0, 0, 0) 92 | a0 = vec3(0, 1, 0) 93 | b0 = vec3(100, 0, 0) 94 | b1 = vec3(100, 1, 0) 95 | let r = computeLineToLine(a0, a1, b0, b1, false, false, false, false) 96 | assert r[0] == vec3(0, 0, 0) 97 | assert r[1] == vec3(0, 0, 0) 98 | assert r[2] == 100.0 99 | 100 | 101 | block: 102 | let 103 | a1 = vec3(0, -7, 0) 104 | a0 = vec3(0, -13, 0) 105 | b0 = vec3(0, 25, 0) 106 | b1 = vec3(0, 26, 0) 107 | let r = computeLineToLine(a0, a1, b0, b1, false, false, false, false) 108 | assert r[0] == vec3(0, 0, 0) 109 | assert r[1] == vec3(0, 0, 0) 110 | assert r[2] == 0 111 | 112 | let r2 = computeLineToLine(a0, a1, b0, b1, true, true, true, true) 113 | assert r2[0] == vec3(0, 25, 0) 114 | assert r2[1] == vec3(0, -7, 0) 115 | assert r2[2] == 32 116 | 117 | block: 118 | let 119 | a1 = vec3(0, 0, 0) 120 | a0 = vec3(1, 1, 1) 121 | b0 = vec3(-1, -1, -1) 122 | b1 = vec3(-2, -2, -2) 123 | let r = computeLineToLine(a0, a1, b0, b1, true, true, true, true) 124 | assert r[0] == vec3(-1, -1, -1) 125 | assert r[1] == vec3(0, 0, 0) 126 | assert almostEquals(r[2], 1.732050807568877) 127 | 128 | block: 129 | let 130 | a1 = vec3(13.43, 21.77, 46.81) 131 | a0 = vec3(27.83, 31.74, -26.60) 132 | b0 = vec3(77.54, 7.53, 6.22) 133 | b1 = vec3(26.99, 12.39, 11.18) 134 | let dist = computeLineToLine(a0, a1, b0, b1, true, true, true, true) 135 | #let dist = computeLineToLine(a0, a1, b0, b1, false, false, false, false) 136 | #assert $dist == "((19.85163563, 26.21609078, 14.07303667), (26.99000000, 12.39000000, 11.18000000), 15.82677141213224)" 137 | # pA = [19.851636, 26.216091, 14.073037] 138 | # pB = [26.990000, 12.390000, 11.180000] 139 | # d = 15.826771 140 | --------------------------------------------------------------------------------