├── .babelrc ├── .eslintrc.json ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── dist ├── astrologyjs-es6.js ├── astrologyjs.d.ts ├── astrologyjs.js ├── astrologyjs.js.map └── astrologyjs.min.js ├── gulpfile.babel.js ├── jasmine-runner.js ├── jasmine.json ├── package.json ├── rollup.config.js ├── src ├── aspect.spec.ts ├── aspect.ts ├── astrologyjs.ts ├── chart-factory.spec.ts ├── chart-factory.ts ├── chart.spec.ts ├── chart.ts ├── person.spec.ts ├── person.ts ├── planet.spec.ts ├── planet.ts ├── rp.spec.ts └── rp.ts ├── tsconfig.def.json ├── tsconfig.json └── tsconfig.test.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015-rollup"], 3 | "plugins": ["transform-runtime"] 4 | } 5 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "parserOptions": { 8 | "sourceType": "module" 9 | }, 10 | "rules": { 11 | "linebreak-style": [ 12 | "error", 13 | "unix" 14 | ], 15 | "semi": [ 16 | "error", 17 | "always" 18 | ], 19 | "no-console": [ 20 | "error", 21 | { 22 | "allow": ["error"] 23 | } 24 | ] 25 | } 26 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | *.log 3 | npm-debug.log* 4 | node_modules 5 | typings 6 | spec 7 | coverage 8 | .DS_store 9 | .github_changelog_generator 10 | src/js -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | logs 2 | *.log 3 | *.tgz 4 | npm-debug.log* 5 | node_modules 6 | typings 7 | spec 8 | src/**/*.spec.ts 9 | src/js 10 | coverage 11 | .DS_store 12 | .github_changelog_generator 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '6' 4 | before_install: 5 | - npm install -g typescript istanbul jasmine-core coveralls 6 | script: 7 | - npm test 8 | after_script: 9 | - cat ./coverage/coverage-remapped.lcov | coveralls -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [v1.3.0](https://github.com/morphatic/astrologyjs/tree/v1.3.0) (2016-11-18) 4 | [Full Changelog](https://github.com/morphatic/astrologyjs/compare/v1.2.0...v1.3.0) 5 | 6 | ## [v1.2.0](https://github.com/morphatic/astrologyjs/tree/v1.2.0) (2016-06-26) 7 | [Full Changelog](https://github.com/morphatic/astrologyjs/compare/v1.1.0...v1.2.0) 8 | 9 | ## [v1.1.0](https://github.com/morphatic/astrologyjs/tree/v1.1.0) (2016-06-20) 10 | [Full Changelog](https://github.com/morphatic/astrologyjs/compare/v1.0.0...v1.1.0) 11 | 12 | ## [v1.0.0](https://github.com/morphatic/astrologyjs/tree/v1.0.0) (2016-06-13) 13 | 14 | 15 | \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Morgan Benton 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Documento Monetized](https://img.shields.io/badge/documento-monetized-brightgreen?style=for-the-badge)](https://github-monetize.web.app/view/5ecd8b8f09cbbd0017edb875) 2 | #### Documentation can be found at: 3 | ## [documento](https://github-monetize.web.app/view/5ecd8b8f09cbbd0017edb875) 4 | Document is web monetized. You would need a [Coil](https://coil.com/) membership to view it. -------------------------------------------------------------------------------- /dist/astrologyjs.d.ts: -------------------------------------------------------------------------------- 1 | declare module "planet" { 2 | /** 3 | * One of the planets, asteroids, the sun or moon 4 | */ 5 | export class Planet { 6 | /** 7 | * The planet name, e.g. Mercury 8 | * @type {string} 9 | */ 10 | name: string; 11 | /** 12 | * A planet's longitude identifies what sign 13 | * it is in 14 | * @type {number} 15 | */ 16 | longitude: number; 17 | /** 18 | * A planet's latitude describes it's distance 19 | * from the ecliptic, and can be used to 20 | * determine if it is out of bounds 21 | * @type {number} 22 | */ 23 | latitude: number; 24 | /** 25 | * A planet's speed allows us to know if it is 26 | * retrograde, and to calculate whether an 27 | * aspect is applying or separating 28 | * @type {number} 29 | */ 30 | speed: number; 31 | /** 32 | * The symbol for this planet as represented in 33 | * the Kairon Semiserif font 34 | * @type {string} 35 | */ 36 | symbol: string; 37 | /** 38 | * Dictionary of symbols for the planets for 39 | * use with the Kairon Semiserif font 40 | * @type {Object} 41 | */ 42 | private symbols; 43 | /** 44 | * Instantiate a new planet object. 45 | * @param {string} name The planet's name 46 | * @param {number} lon The planet's longitude 47 | * @param {number} lat The planet's latitude 48 | * @param {number} spd The planet's speed relative to earth 49 | */ 50 | constructor(name: string, lon: number, lat: number, spd: number); 51 | /** 52 | * A planet is retrograde when it's speed relative 53 | * to earth is less than zero 54 | * @return {boolean} Whether or not the planet is retrograde 55 | */ 56 | isRetrograde(): boolean; 57 | /** 58 | * Is this one of the major planets typically included in a chart? 59 | * @return {boolean} Returns true if it is a major planet 60 | */ 61 | isMajor(): boolean; 62 | } 63 | } 64 | declare module "rp" { 65 | export interface RequestPromiseOptions { 66 | uri: string; 67 | qs: { 68 | [name: string]: string | number | boolean; 69 | }; 70 | } 71 | const rp: (options: RequestPromiseOptions) => Promise; 72 | export default rp; 73 | } 74 | declare module "person" { 75 | export interface GoogleLocation { 76 | lat: number; 77 | lng: number; 78 | } 79 | export type Point = GoogleLocation; 80 | /** 81 | * Represents a person or event for whom a chart will be created 82 | */ 83 | export class Person { 84 | name: string; 85 | date: string; 86 | location: Point; 87 | /** 88 | * Google API key 89 | * @type {string} 90 | */ 91 | private static _key; 92 | /** 93 | * Creates a Person object 94 | * @param {string} public name Name of the person or event 95 | * @param {string} public date UTC date in ISO 8601 format, i.e. YYYY-MM-DDTHH:mmZ (caller must convert to UTC) 96 | * @param {Point} location The [lat: number, lon: number] of the event or person's birthplace 97 | */ 98 | constructor(name: string, date: string, location: Point); 99 | /** 100 | * Asynchronous factory function for creating people or events 101 | * @param {string} name Name of the person or event 102 | * @param {Date | string} date Exact datetime for the chart, preferably UTC date in ISO 8601 format, i.e. YYYY-MM-DDTHH:mmZ (caller must convert to UTC) 103 | * @param {Point | string} location Either an address or a lat/lng combination 104 | * @return {Promise} The Person object that was created 105 | */ 106 | static create(name: string, date: Date | string, location: Point | string): Promise; 107 | /** 108 | * Gets a timezone given a latitude and longitude 109 | * @param {Point} p Contains the latitude and longitude in decimal format 110 | */ 111 | static getTimezone(p: Point): Promise; 112 | /** 113 | * Get a latitude and longitude given an address 114 | * @param {string} address The address of the desired lat/lon 115 | */ 116 | static getLatLon(address: string): Promise; 117 | } 118 | } 119 | declare module "aspect" { 120 | import { Planet } from "planet"; 121 | /** 122 | * Represents an aspect between two planets 123 | */ 124 | export class Aspect { 125 | p1: Planet; 126 | p2: Planet; 127 | /** 128 | * A label naming the aspect type, e.g. trine 129 | * @type {string} 130 | */ 131 | private _type; 132 | /** 133 | * Number of degrees away from being perfectly in aspect 134 | * @type {number} 135 | */ 136 | private _orb; 137 | /** 138 | * Is the aspect applying or separating 139 | * @type {boolean} 140 | */ 141 | private _applying; 142 | /** 143 | * Catalog of all of the aspect types available in our system 144 | * @type {AspectTypeArray} 145 | */ 146 | private _types; 147 | /** 148 | * Creates a new Aspect or throws an error if no aspect exists 149 | * between the planets 150 | * @param {Planet} public p1 First planet in the relationship 151 | * @param {Planet} public p2 Second planet in the relationship 152 | */ 153 | constructor(p1: Planet, p2: Planet); 154 | /** 155 | * Get the type assigned to this aspect 156 | * @return {string} One of the aspect type names 157 | */ 158 | readonly type: string; 159 | /** 160 | * Get the number of degrees away from being in perfect aspect 161 | * @return {number} The number of degrees (absolute value) 162 | */ 163 | readonly orb: number; 164 | /** 165 | * Get the character that will produce the correct symbol for 166 | * this aspect in the Kairon Semiserif font 167 | * @return {string} A character representing a symbol 168 | */ 169 | readonly symbol: string; 170 | /** 171 | * Is the aspect applying or separating? 172 | * @return {boolean} True if the aspect is applying 173 | */ 174 | isApplying(): boolean; 175 | /** 176 | * Is this a "major" aspect? i.e. one of those you usually 177 | * hear about in astrological forecasts 178 | * @return {boolean} True if this is a "major" aspect 179 | */ 180 | isMajor(): boolean; 181 | } 182 | } 183 | declare module "chart" { 184 | import { Person, Point } from "person"; 185 | import { Planet } from "planet"; 186 | import { Aspect } from "aspect"; 187 | export enum ChartType { 188 | Basic = 0, 189 | Transits = 1, 190 | Synastry = 2, 191 | Combined = 3, 192 | Davison = 4, 193 | CombinedTransits = 5, 194 | DavisonTransits = 6, 195 | } 196 | export interface PlanetData { 197 | name: string; 198 | lon: number; 199 | lat: number; 200 | spd: number; 201 | r: number; 202 | } 203 | export interface PlanetDataArray { 204 | [name: string]: PlanetData; 205 | } 206 | export interface ChartData { 207 | planets: PlanetDataArray; 208 | houses: Array; 209 | ascendant: number; 210 | mc: number; 211 | } 212 | export interface ChartDataArray { 213 | [index: number]: ChartData; 214 | } 215 | export class Chart { 216 | name: string; 217 | p1: Person; 218 | p2: Person; 219 | type: ChartType; 220 | _planets1: Array; 221 | _planets2: Array; 222 | _aspects: Array; 223 | _ascendant: number; 224 | _houses: Array; 225 | _debug: boolean; 226 | _signs: { 227 | name: string; 228 | symbol: string; 229 | v: number; 230 | }[]; 231 | constructor(name: string, p1: Person, cdata: ChartDataArray, p2?: Person, type?: ChartType); 232 | /** 233 | * Extracts planet data from ChartData and creates Planet objects for each one 234 | * @param {ChartData} cdata JSON data returned from morphemeris REST API 235 | * @return {Array} An array of Planet objects 236 | */ 237 | getPlanets(cdata: ChartData): Array; 238 | /** 239 | * Calculates the aspects between planets in the chart 240 | */ 241 | calculateAspects(): void; 242 | /** 243 | * Calculates longitudes for a combined chart 244 | * @param {ChartData} p1 Planet data from person one 245 | * @param {ChartData} p2 Planet data from person two 246 | */ 247 | calculateCombinedPlanets(cdata: ChartDataArray): ChartData; 248 | /** 249 | * Finds the midpoint between two planets on the "short" side 250 | * @param {number} l1 Longitude of planet one 251 | * @param {number} l2 Longitude of planet two 252 | * @return {number} Longitude of the midpoint 253 | */ 254 | getLonMidpoint(l1: number, l2: number): number; 255 | /** 256 | * Gets chart data from the online ephemeris 257 | * @param {string} date A UTC datetime string in ISO 8601 format 258 | * @param {Point} p An object with numeric lat and lng properties 259 | * @return {Promise} A JSON object with the data needed to implement a chart 260 | */ 261 | static getChartData(date: string, p: Point): Promise; 262 | /** 263 | * Refresh or set the transits to a new time 264 | * @param {string} date (Optional) Target datetime for transits in ISO 8601 format; defaults to now() 265 | */ 266 | refreshTransits(date?: string): Promise; 267 | readonly houses: Array; 268 | readonly aspects: Array; 269 | readonly ascendant: number; 270 | readonly innerPlanets: Array; 271 | readonly outerPlanets: Array; 272 | } 273 | } 274 | declare module "chart-factory" { 275 | import { Person, Point } from "person"; 276 | import { Chart, ChartType } from "chart"; 277 | /** 278 | * Usage: let chart: Chart = ChartFactory.create("my chart", person); 279 | */ 280 | export class ChartFactory { 281 | static create(name: string, p1: Person, p2?: Person, type?: ChartType): Promise; 282 | /** 283 | * Calculates the lat/lon of the geographic midpoint between two lat/lon pairs 284 | * @param {Point} p1 Latitude/longitude of first location 285 | * @param {Point} p2 Latitude/longitude of second location 286 | * @return {Point} The latitude/longitude of the geographic midpoint 287 | */ 288 | static getGeoMidpoint(p1: Point, p2: Point): Point; 289 | /** 290 | * Finds the exact midpoint between two dates 291 | * @param {string} date1 The first date 292 | * @param {string} date2 The second date 293 | * @return {string} The midpoint date as an ISO 8601 string 294 | */ 295 | static getDatetimeMidpoint(date1: string, date2: string): string; 296 | /** 297 | * Converts decimal degrees to radians 298 | * @param {number} degrees Decimal representation of degrees to be converted 299 | * @return {number} Returns radians 300 | */ 301 | static toRadians: (degrees: number) => number; 302 | /** 303 | * Converts radians to decimal degrees 304 | * @param {number} radians Radians to be converted 305 | * @return {number} Returns decimal degrees 306 | */ 307 | static toDegrees: (radians: number) => number; 308 | } 309 | } 310 | declare module "astrologyjs" { 311 | export { Planet } from "planet"; 312 | export { Person, Point } from "person"; 313 | export { Aspect } from "aspect"; 314 | export { Chart, ChartType } from "chart"; 315 | export { ChartFactory } from "chart-factory"; 316 | } 317 | -------------------------------------------------------------------------------- /dist/astrologyjs.min.js: -------------------------------------------------------------------------------- 1 | !function(t,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports,require("https"),require("http")):"function"==typeof define&&define.amd?define(["exports","https","http"],n):n(t.astrologyjs=t.astrologyjs||{},t.https,t.http)}(this,function(t,n,e){"use strict";function r(t){return t&&t.__esModule?t.default:t}function o(t,n){return n={exports:{}},t(n,n.exports),n.exports}n="default"in n?n.default:n,e="default"in e?e.default:e;var i="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},a=o(function(t,n){n.__esModule=!0,n.default=function(t,n){if(!(t instanceof n))throw new TypeError("Cannot call a class as a function")}}),u=r(a),s=o(function(t){var n=t.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=n)}),l=o(function(t){var n=t.exports={version:"2.4.0"};"number"==typeof __e&&(__e=n)}),c=function(t){if("function"!=typeof t)throw TypeError(t+" is not a function!");return t},f=c,h=function(t,n,e){if(f(t),void 0===n)return t;switch(e){case 1:return function(e){return t.call(n,e)};case 2:return function(e,r){return t.call(n,e,r)};case 3:return function(e,r,o){return t.call(n,e,r,o)}}return function(){return t.apply(n,arguments)}},p=function(t){return"object"==typeof t?null!==t:"function"==typeof t},d=p,y=function(t){if(!d(t))throw TypeError(t+" is not an object!");return t},v=function(t){try{return!!t()}catch(t){return!0}},m=!v(function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a}),g=p,b=s.document,w=g(b)&&g(b.createElement),_=function(t){return w?b.createElement(t):{}},j=!m&&!v(function(){return 7!=Object.defineProperty(_("div"),"a",{get:function(){return 7}}).a}),x=p,C=function(t,n){if(!x(t))return t;var e,r;if(n&&"function"==typeof(e=t.toString)&&!x(r=e.call(t)))return r;if("function"==typeof(e=t.valueOf)&&!x(r=e.call(t)))return r;if(!n&&"function"==typeof(e=t.toString)&&!x(r=e.call(t)))return r;throw TypeError("Can't convert object to primitive value")},k=y,M=j,O=C,P=Object.defineProperty,T=m?Object.defineProperty:function(t,n,e){if(k(t),n=O(n,!0),k(e),M)try{return P(t,n,e)}catch(t){}if("get"in e||"set"in e)throw TypeError("Accessors not supported!");return"value"in e&&(t[n]=e.value),t},S={f:T},E=function(t,n){return{enumerable:!(1&t),configurable:!(2&t),writable:!(4&t),value:n}},L=S,D=E,R=m?function(t,n,e){return L.f(t,n,D(1,e))}:function(t,n,e){return t[n]=e,t},A=s,F=l,I=h,N=R,q="prototype",G=function(t,n,e){var r,o,i,a=t&G.F,u=t&G.G,s=t&G.S,l=t&G.P,c=t&G.B,f=t&G.W,h=u?F:F[n]||(F[n]={}),p=h[q],d=u?A:s?A[n]:(A[n]||{})[q];u&&(e=n);for(r in e)o=!a&&d&&void 0!==d[r],o&&r in h||(i=o?d[r]:e[r],h[r]=u&&"function"!=typeof d[r]?e[r]:c&&o?I(i,A):f&&d[r]==i?function(t){var n=function(n,e,r){if(this instanceof t){switch(arguments.length){case 0:return new t;case 1:return new t(n);case 2:return new t(n,e)}return new t(n,e,r)}return t.apply(this,arguments)};return n[q]=t[q],n}(i):l&&"function"==typeof i?I(Function.call,i):i,l&&((h.virtual||(h.virtual={}))[r]=i,t&G.R&&p&&!p[r]&&N(p,r,i)))};G.F=1,G.G=2,G.S=4,G.P=8,G.B=16,G.W=32,G.U=64,G.R=128;var Y=G,B=Y;B(B.S+B.F*!m,"Object",{defineProperty:S.f});var W=l.Object,z=function(t,n,e){return W.defineProperty(t,n,e)},U=o(function(t){t.exports={default:z,__esModule:!0}}),H=o(function(t,n){function e(t){return t&&t.__esModule?t:{default:t}}n.__esModule=!0;var r=U,o=e(r);n.default=function(){function t(t,n){for(var e=0;e-1}}]),t}(),Z=X,K={Planet:Z},J=o(function(t){!function(n){function e(t,n,e,r){var i=n&&n.prototype instanceof o?n:o,a=Object.create(i.prototype),u=new p(r||[]);return a._invoke=c(t,e,u),a}function r(t,n,e){try{return{type:"normal",arg:t.call(n,e)}}catch(t){return{type:"throw",arg:t}}}function o(){}function i(){}function a(){}function u(t){["next","throw","return"].forEach(function(n){t[n]=function(t){return this._invoke(n,t)}})}function s(t){this.arg=t}function l(t){function n(e,o,i,a){var u=r(t[e],t,o);if("throw"!==u.type){var l=u.arg,c=l.value;return c instanceof s?Promise.resolve(c.arg).then(function(t){n("next",t,i,a)},function(t){n("throw",t,i,a)}):Promise.resolve(c).then(function(t){l.value=t,i(l)},a)}a(u.arg)}function e(t,e){function r(){return new Promise(function(r,o){n(t,e,r,o)})}return o=o?o.then(r,r):r()}"object"==typeof process&&process.domain&&(n=process.domain.bind(n));var o;this._invoke=e}function c(t,n,e){var o=x;return function(i,a){if(o===k)throw new Error("Generator is already running");if(o===M){if("throw"===i)throw a;return y()}for(;;){var u=e.delegate;if(u){if("return"===i||"throw"===i&&u.iterator[i]===v){e.delegate=null;var s=u.iterator.return;if(s){var l=r(s,u.iterator,a);if("throw"===l.type){i="throw",a=l.arg;continue}}if("return"===i)continue}var l=r(u.iterator[i],u.iterator,a);if("throw"===l.type){e.delegate=null,i="throw",a=l.arg;continue}i="next",a=v;var c=l.arg;if(!c.done)return o=C,c;e[u.resultName]=c.value,e.next=u.nextLoc,e.delegate=null}if("next"===i)e.sent=e._sent=a;else if("throw"===i){if(o===x)throw o=M,a;e.dispatchException(a)&&(i="next",a=v)}else"return"===i&&e.abrupt("return",a);o=k;var l=r(t,n,e);if("normal"===l.type){o=e.done?M:C;var c={value:l.arg,done:e.done};if(l.arg!==O)return c;e.delegate&&"next"===i&&(a=v)}else"throw"===l.type&&(o=M,i="throw",a=l.arg)}}}function f(t){var n={tryLoc:t[0]};1 in t&&(n.catchLoc=t[1]),2 in t&&(n.finallyLoc=t[2],n.afterLoc=t[3]),this.tryEntries.push(n)}function h(t){var n=t.completion||{};n.type="normal",delete n.arg,t.completion=n}function p(t){this.tryEntries=[{tryLoc:"root"}],t.forEach(f,this),this.reset(!0)}function d(t){if(t){var n=t[b];if(n)return n.call(t);if("function"==typeof t.next)return t;if(!isNaN(t.length)){var e=-1,r=function n(){for(;++e=0;--r){var o=this.tryEntries[r],i=o.completion;if("root"===o.tryLoc)return n("end");if(o.tryLoc<=this.prev){var a=m.call(o,"catchLoc"),u=m.call(o,"finallyLoc");if(a&&u){if(this.prev=0;--e){var r=this.tryEntries[e];if(r.tryLoc<=this.prev&&m.call(r,"finallyLoc")&&this.prev=0;--n){var e=this.tryEntries[n];if(e.finallyLoc===t)return this.complete(e.completion,e.afterLoc),h(e),O}},catch:function(t){for(var n=this.tryEntries.length-1;n>=0;--n){var e=this.tryEntries[n];if(e.tryLoc===t){var r=e.completion;if("throw"===r.type){var o=r.arg;h(e)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(t,n,e){return this.delegate={iterator:d(t),resultName:n,nextLoc:e},O}}}("object"==typeof i?i:"object"==typeof window?window:"object"==typeof self?self:i)}),V="object"==typeof i?i:"object"==typeof window?window:"object"==typeof self?self:i,$=V.regeneratorRuntime&&Object.getOwnPropertyNames(V).indexOf("regeneratorRuntime")>=0,tt=$&&V.regeneratorRuntime;V.regeneratorRuntime=void 0;var nt=J;if($)V.regeneratorRuntime=tt;else try{delete V.regeneratorRuntime}catch(t){V.regeneratorRuntime=void 0}var et=nt,rt=Math.ceil,ot=Math.floor,it=function(t){return isNaN(t=+t)?0:(t>0?ot:rt)(t)},at=function(t){if(void 0==t)throw TypeError("Can't call method on "+t);return t},ut=it,st=at,lt=function(t){return function(n,e){var r,o,i=String(st(n)),a=ut(e),u=i.length;return a<0||a>=u?t?"":void 0:(r=i.charCodeAt(a),r<55296||r>56319||a+1===u||(o=i.charCodeAt(a+1))<56320||o>57343?t?i.charAt(a):r:t?i.slice(a,a+2):(r-55296<<10)+(o-56320)+65536)}},ct=!0,ft=R,ht={}.hasOwnProperty,pt=function(t,n){return ht.call(t,n)},dt={},yt={}.toString,vt=function(t){return yt.call(t).slice(8,-1)},mt=vt,gt=Object("z").propertyIsEnumerable(0)?Object:function(t){return"String"==mt(t)?t.split(""):Object(t)},bt=gt,wt=at,_t=function(t){return bt(wt(t))},jt=it,xt=Math.min,Ct=function(t){return t>0?xt(jt(t),9007199254740991):0},kt=it,Mt=Math.max,Ot=Math.min,Pt=function(t,n){return t=kt(t),t<0?Mt(t+n,0):Ot(t,n)},Tt=_t,St=Ct,Et=Pt,Lt=function(t){return function(n,e,r){var o,i=Tt(n),a=St(i.length),u=Et(r,a);if(t&&e!=e){for(;a>u;)if(o=i[u++],o!=o)return!0}else for(;a>u;u++)if((t||u in i)&&i[u]===e)return t||u||0;return!t&&-1}},Dt=s,Rt="__core-js_shared__",At=Dt[Rt]||(Dt[Rt]={}),Ft=function(t){return At[t]||(At[t]={})},It=0,Nt=Math.random(),qt=function(t){return"Symbol(".concat(void 0===t?"":t,")_",(++It+Nt).toString(36))},Gt=Ft("keys"),Yt=qt,Bt=function(t){return Gt[t]||(Gt[t]=Yt(t))},Wt=pt,zt=_t,Ut=Lt(!1),Ht=Bt("IE_PROTO"),Qt=function(t,n){var e,r=zt(t),o=0,i=[];for(e in r)e!=Ht&&Wt(r,e)&&i.push(e);for(;n.length>o;)Wt(r,e=n[o++])&&(~Ut(i,e)||i.push(e));return i},Xt="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(","),Zt=Qt,Kt=Xt,Jt=Object.keys||function(t){return Zt(t,Kt)},Vt=S,$t=y,tn=Jt,nn=m?Object.defineProperties:function(t,n){$t(t);for(var e,r=tn(n),o=r.length,i=0;o>i;)Vt.f(t,e=r[i++],n[e]);return t},en=s.document&&document.documentElement,rn=y,on=nn,an=Xt,un=Bt("IE_PROTO"),sn=function(){},ln="prototype",cn=function(){var t,n=_("iframe"),e=an.length,r="<",o=">";for(n.style.display="none",en.appendChild(n),n.src="javascript:",t=n.contentWindow.document,t.open(),t.write(r+"script"+o+"document.F=Object"+r+"/script"+o),t.close(),cn=t.F;e--;)delete cn[ln][an[e]];return cn()},fn=Object.create||function(t,n){var e;return null!==t?(sn[ln]=rn(t),e=new sn,sn[ln]=null,e[un]=t):e=cn(),void 0===n?e:on(e,n)},hn=o(function(t){var n=Ft("wks"),e=qt,r=s.Symbol,o="function"==typeof r,i=t.exports=function(t){return n[t]||(n[t]=o&&r[t]||(o?r:e)("Symbol."+t))};i.store=n}),pn=S.f,dn=pt,yn=hn("toStringTag"),vn=function(t,n,e){t&&!dn(t=e?t:t.prototype,yn)&&pn(t,yn,{configurable:!0,value:n})},mn=fn,gn=E,bn=vn,wn={};R(wn,hn("iterator"),function(){return this});var _n=function(t,n,e){t.prototype=mn(wn,{next:gn(1,e)}),bn(t,n+" Iterator")},jn=at,xn=function(t){return Object(jn(t))},Cn=pt,kn=xn,Mn=Bt("IE_PROTO"),On=Object.prototype,Pn=Object.getPrototypeOf||function(t){return t=kn(t),Cn(t,Mn)?t[Mn]:"function"==typeof t.constructor&&t instanceof t.constructor?t.constructor.prototype:t instanceof Object?On:null},Tn=ct,Sn=Y,En=ft,Ln=R,Dn=pt,Rn=dt,An=_n,Fn=vn,In=Pn,Nn=hn("iterator"),qn=!([].keys&&"next"in[].keys()),Gn="@@iterator",Yn="keys",Bn="values",Wn=function(){return this},zn=function(t,n,e,r,o,i,a){An(e,n,r);var u,s,l,c=function(t){if(!qn&&t in d)return d[t];switch(t){case Yn:return function(){return new e(this,t)};case Bn:return function(){return new e(this,t)}}return function(){return new e(this,t)}},f=n+" Iterator",h=o==Bn,p=!1,d=t.prototype,y=d[Nn]||d[Gn]||o&&d[o],v=y||c(o),m=o?h?c("entries"):v:void 0,g="Array"==n?d.entries||y:y;if(g&&(l=In(g.call(new t)),l!==Object.prototype&&(Fn(l,f,!0),Tn||Dn(l,Nn)||Ln(l,Nn,Wn))),h&&y&&y.name!==Bn&&(p=!0,v=function(){return y.call(this)}),Tn&&!a||!qn&&!p&&d[Nn]||Ln(d,Nn,v),Rn[n]=v,Rn[f]=Wn,o)if(u={values:h?v:c(Bn),keys:i?v:c(Yn),entries:m},a)for(s in u)s in d||En(d,s,u[s]);else Sn(Sn.P+Sn.F*(qn||p),n,u);return u},Un=lt(!0);zn(String,"String",function(t){this._t=String(t),this._i=0},function(){var t,n=this._t,e=this._i;return e>=n.length?{value:void 0,done:!0}:(t=Un(n,e),this._i+=t.length,{value:t,done:!1})});var Hn=function(){},Qn=function(t,n){return{value:n,done:!!t}},Xn=Hn,Zn=Qn,Kn=dt,Jn=_t;zn(Array,"Array",function(t,n){this._t=Jn(t),this._i=0,this._k=n},function(){var t=this._t,n=this._k,e=this._i++;return!t||e>=t.length?(this._t=void 0,Zn(1)):"keys"==n?Zn(0,e):"values"==n?Zn(0,t[e]):Zn(0,[e,t[e]])},"values");Kn.Arguments=Kn.Array,Xn("keys"),Xn("values"),Xn("entries");for(var Vn=s,$n=R,te=dt,ne=hn("toStringTag"),ee=["NodeList","DOMTokenList","MediaList","StyleSheetList","CSSRuleList"],re=0;re<5;re++){var oe=ee[re],ie=Vn[oe],ae=ie&&ie.prototype;ae&&!ae[ne]&&$n(ae,ne,oe),te[oe]=te.Array}var ue,se,le,ce=vt,fe=hn("toStringTag"),he="Arguments"==ce(function(){return arguments}()),pe=function(t,n){try{return t[n]}catch(t){}},de=function(t){var n,e,r;return void 0===t?"Undefined":null===t?"Null":"string"==typeof(e=pe(n=Object(t),fe))?e:he?ce(n):"Object"==(r=ce(n))&&"function"==typeof n.callee?"Arguments":r},ye=function(t,n,e,r){if(!(t instanceof n)||void 0!==r&&r in t)throw TypeError(e+": incorrect invocation!");return t},ve=y,me=function(t,n,e,r){try{return r?n(ve(e)[0],e[1]):n(e)}catch(n){var o=t.return;throw void 0!==o&&ve(o.call(t)),n}},ge=dt,be=hn("iterator"),we=Array.prototype,_e=function(t){return void 0!==t&&(ge.Array===t||we[be]===t)},je=de,xe=hn("iterator"),Ce=dt,ke=l.getIteratorMethod=function(t){if(void 0!=t)return t[xe]||t["@@iterator"]||Ce[je(t)]},Me=o(function(t){var n=h,e=me,r=_e,o=y,i=Ct,a=ke,u={},s={},l=t.exports=function(t,l,c,f,h){var p,d,y,v,m=h?function(){return t}:a(t),g=n(c,f,l?2:1),b=0;if("function"!=typeof m)throw TypeError(t+" is not iterable!");if(r(m)){for(p=i(t.length);p>b;b++)if(v=l?g(o(d=t[b])[0],d[1]):g(t[b]),v===u||v===s)return v}else for(y=m.call(t);!(d=y.next()).done;)if(v=e(y,g,d.value,l),v===u||v===s)return v};l.BREAK=u,l.RETURN=s}),Oe=y,Pe=c,Te=hn("species"),Se=function(t,n){var e,r=Oe(t).constructor;return void 0===r||void 0==(e=Oe(r)[Te])?n:Pe(e)},Ee=function(t,n,e){var r=void 0===e;switch(n.length){case 0:return r?t():t.call(e);case 1:return r?t(n[0]):t.call(e,n[0]);case 2:return r?t(n[0],n[1]):t.call(e,n[0],n[1]);case 3:return r?t(n[0],n[1],n[2]):t.call(e,n[0],n[1],n[2]);case 4:return r?t(n[0],n[1],n[2],n[3]):t.call(e,n[0],n[1],n[2],n[3])}return t.apply(e,n)},Le=h,De=Ee,Re=en,Ae=_,Fe=s,Ie=Fe.process,Ne=Fe.setImmediate,qe=Fe.clearImmediate,Ge=Fe.MessageChannel,Ye=0,Be={},We="onreadystatechange",ze=function(){var t=+this;if(Be.hasOwnProperty(t)){var n=Be[t];delete Be[t],n()}},Ue=function(t){ze.call(t.data)};Ne&&qe||(Ne=function(t){for(var n=[],e=1;arguments.length>e;)n.push(arguments[e++]);return Be[++Ye]=function(){De("function"==typeof t?t:Function(t),n)},ue(Ye),Ye},qe=function(t){delete Be[t]},"process"==vt(Ie)?ue=function(t){Ie.nextTick(Le(ze,t,1))}:Ge?(se=new Ge,le=se.port2,se.port1.onmessage=Ue,ue=Le(le.postMessage,le,1)):Fe.addEventListener&&"function"==typeof postMessage&&!Fe.importScripts?(ue=function(t){Fe.postMessage(t+"","*")},Fe.addEventListener("message",Ue,!1)):ue=We in Ae("script")?function(t){Re.appendChild(Ae("script"))[We]=function(){Re.removeChild(this),ze.call(t)}}:function(t){setTimeout(Le(ze,t,1),0)});var He={set:Ne,clear:qe},Qe=s,Xe=He.set,Ze=Qe.MutationObserver||Qe.WebKitMutationObserver,Ke=Qe.process,Je=Qe.Promise,Ve="process"==vt(Ke),$e=function(){var t,n,e,r=function(){var r,o;for(Ve&&(r=Ke.domain)&&r.exit();t;){o=t.fn,t=t.next;try{o()}catch(r){throw t?e():n=void 0,r}}n=void 0,r&&r.enter()};if(Ve)e=function(){Ke.nextTick(r)};else if(Ze){var o=!0,i=document.createTextNode("");new Ze(r).observe(i,{characterData:!0}),e=function(){i.data=o=!o}}else if(Je&&Je.resolve){var a=Je.resolve();e=function(){a.then(r)}}else e=function(){Xe.call(Qe,r)};return function(r){var o={fn:r,next:void 0};n&&(n.next=o),t||(t=o,e()),n=o}},tr=R,nr=function(t,n,e){for(var r in n)e&&t[r]?t[r]=n[r]:tr(t,r,n[r]);return t},er=s,rr=l,or=S,ir=m,ar=hn("species"),ur=function(t){var n="function"==typeof rr[t]?rr[t]:er[t];ir&&n&&!n[ar]&&or.f(n,ar,{configurable:!0,get:function(){return this}})},sr=hn("iterator"),lr=!1;try{var cr=[7][sr]();cr.return=function(){lr=!0},Array.from(cr,function(){throw 2})}catch(t){}var fr,hr,pr,dr=function(t,n){if(!n&&!lr)return!1;var e=!1;try{var r=[7],o=r[sr]();o.next=function(){return{done:e=!0}},r[sr]=function(){return o},t(r)}catch(t){}return e},yr=ct,vr=s,mr=h,gr=de,br=Y,wr=p,_r=c,jr=ye,xr=Me,Cr=Se,kr=He.set,Mr=$e(),Or="Promise",Pr=vr.TypeError,Tr=vr.process,Sr=vr[Or],Tr=vr.process,Er="process"==gr(Tr),Lr=function(){},Dr=!!function(){try{var t=Sr.resolve(1),n=(t.constructor={})[hn("species")]=function(t){t(Lr,Lr)};return(Er||"function"==typeof PromiseRejectionEvent)&&t.then(Lr)instanceof n}catch(t){}}(),Rr=function(t,n){return t===n||t===Sr&&n===pr},Ar=function(t){var n;return!(!wr(t)||"function"!=typeof(n=t.then))&&n},Fr=function(t){return Rr(Sr,t)?new Ir(t):new hr(t)},Ir=hr=function(t){var n,e;this.promise=new t(function(t,r){if(void 0!==n||void 0!==e)throw Pr("Bad Promise constructor");n=t,e=r}),this.resolve=_r(n),this.reject=_r(e)},Nr=function(t){try{t()}catch(t){return{error:t}}},qr=function(t,n){if(!t._n){t._n=!0;var e=t._c;Mr(function(){for(var r=t._v,o=1==t._s,i=0,a=function(n){var e,i,a=o?n.ok:n.fail,u=n.resolve,s=n.reject,l=n.domain;try{a?(o||(2==t._h&&Br(t),t._h=1),a===!0?e=r:(l&&l.enter(),e=a(r),l&&l.exit()),e===n.promise?s(Pr("Promise-chain cycle")):(i=Ar(e))?i.call(e,u,s):u(e)):s(r)}catch(t){s(t)}};e.length>i;)a(e[i++]);t._c=[],t._n=!1,n&&!t._h&&Gr(t)})}},Gr=function(t){kr.call(vr,function(){var n,e,r,o=t._v;if(Yr(t)&&(n=Nr(function(){Er?Tr.emit("unhandledRejection",o,t):(e=vr.onunhandledrejection)?e({promise:t,reason:o}):(r=vr.console)&&r.error&&r.error("Unhandled promise rejection",o)}),t._h=Er||Yr(t)?2:1),t._a=void 0,n)throw n.error})},Yr=function(t){if(1==t._h)return!1;for(var n,e=t._a||t._c,r=0;e.length>r;)if(n=e[r++],n.fail||!Yr(n.promise))return!1;return!0},Br=function(t){kr.call(vr,function(){var n;Er?Tr.emit("rejectionHandled",t):(n=vr.onrejectionhandled)&&n({promise:t,reason:t._v})})},Wr=function(t){var n=this;n._d||(n._d=!0,n=n._w||n,n._v=t,n._s=2,n._a||(n._a=n._c.slice()),qr(n,!0))},zr=function(t){var n,e=this;if(!e._d){e._d=!0,e=e._w||e;try{if(e===t)throw Pr("Promise can't be resolved itself");(n=Ar(t))?Mr(function(){var r={_w:e,_d:!1};try{n.call(t,mr(zr,r,1),mr(Wr,r,1))}catch(t){Wr.call(r,t)}}):(e._v=t,e._s=1,qr(e,!1))}catch(t){Wr.call({_w:e,_d:!1},t)}}};Dr||(Sr=function(t){jr(this,Sr,Or,"_h"),_r(t),fr.call(this);try{t(mr(zr,this,1),mr(Wr,this,1))}catch(t){Wr.call(this,t)}},fr=function(t){this._c=[],this._a=void 0,this._s=0,this._d=!1,this._v=void 0,this._h=0,this._n=!1},fr.prototype=nr(Sr.prototype,{then:function(t,n){var e=Fr(Cr(this,Sr));return e.ok="function"!=typeof t||t,e.fail="function"==typeof n&&n,e.domain=Er?Tr.domain:void 0,this._c.push(e),this._a&&this._a.push(e),this._s&&qr(this,!1),e.promise},catch:function(t){return this.then(void 0,t)}}),Ir=function(){var t=new fr;this.promise=t,this.resolve=mr(zr,t,1),this.reject=mr(Wr,t,1)}),br(br.G+br.W+br.F*!Dr,{Promise:Sr}),vn(Sr,Or),ur(Or),pr=l[Or],br(br.S+br.F*!Dr,Or,{reject:function(t){var n=Fr(this),e=n.reject;return e(t),n.promise}}),br(br.S+br.F*(yr||!Dr),Or,{resolve:function(t){if(t instanceof Sr&&Rr(t.constructor,this))return t;var n=Fr(this),e=n.resolve;return e(t),n.promise}}),br(br.S+br.F*!(Dr&&dr(function(t){Sr.all(t).catch(Lr)})),Or,{all:function(t){var n=this,e=Fr(n),r=e.resolve,o=e.reject,i=Nr(function(){var e=[],i=0,a=1;xr(t,!1,function(t){var u=i++,s=!1;e.push(void 0),a++,n.resolve(t).then(function(t){s||(s=!0,e[u]=t,--a||r(e))},o)}),--a||r(e)});return i&&o(i.error),e.promise},race:function(t){var n=this,e=Fr(n),r=e.reject,o=Nr(function(){xr(t,!1,function(t){n.resolve(t).then(e.resolve,r)})});return o&&r(o.error),e.promise}});var Ur=l.Promise,Hr=o(function(t){t.exports={default:Ur,__esModule:!0}}),Qr=r(Hr),Xr=Y,Zr=l,Kr=v,Jr=function(t,n){var e=(Zr.Object||{})[t]||Object[t],r={};r[t]=n(e),Xr(Xr.S+Xr.F*Kr(function(){e(1)}),"Object",r)},Vr=xn,$r=Jt;Jr("keys",function(){return function(t){return $r(Vr(t))}});var to=l.Object.keys,no=o(function(t){t.exports={default:to,__esModule:!0}}),eo=r(no),ro=o(function(t,r){var o=n,i=function(t){var n=t.uri,e=eo(t.qs).map(function(n){return encodeURIComponent(n)+"="+encodeURIComponent(t.qs[n].toString())}).join("&");return n+"?"+e},a=function(t){return new Qr(function(n,r){var a=e,u=t.uri.startsWith("https")?o:a,s=i(t),l=u.get(s,function(t){(t.statusCode<200||t.statusCode>299)&&r(new Error("HTTP Error: "+t.statusCode));var e=[];t.on("data",function(t){return e.push(t)}),t.on("end",function(){return n(JSON.parse(e.join("")))})});l.on("error",function(t){return r(t)})})};Object.defineProperty(r,"__esModule",{value:!0}),r.default=a}),oo=i&&i.__awaiter||function(t,n,e,r){return new(e||(e=Qr))(function(o,i){function a(t){try{s(r.next(t))}catch(t){i(t)}}function u(t){try{s(r.throw(t))}catch(t){i(t)}}function s(t){t.done?o(t.value):new e(function(n){n(t.value)}).then(a,u)}s((r=r.apply(t,n)).next())})},io=ro,ao=function(){function t(n,e,r){u(this,t),this.name=n,this.date=e,this.location=r}return Q(t,null,[{key:"create",value:function(n,e,r){return oo(this,void 0,void 0,et.mark(function o(){var i,a;return et.wrap(function(o){for(;;)switch(o.prev=o.next){case 0:if(i=void 0,a=void 0,n){o.next=3;break}throw new Error("No name was submitted for the person");case 3:if("string"!=typeof e){o.next=9;break}if(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}\.\d{3})?Z/.test(e)){o.next=6;break}throw new TypeError("Date not formatted according to ISO 8601 (YYYY-MM-DDTHH:mmZ)");case 6:i=e,o.next=10;break;case 9:i=e instanceof Date?e.toISOString():(new Date).toISOString();case 10:if("string"!=typeof r){o.next=16;break}return o.next=13,this.getLatLon(r);case 13:a=o.sent,o.next=21;break;case 16:if(!(r.lat<-90||r.lat>90)){o.next=18;break}throw new RangeError("Latitude must be between -90 and 90");case 18:if(!(r.lng<-180||r.lng>180)){o.next=20;break}throw new RangeError("Longitude must be between -180 and 180");case 20:a=r;case 21:return o.abrupt("return",new t(n,i,a));case 22:case"end":return o.stop()}},o,this)}))}},{key:"getTimezone",value:function(t){return oo(this,void 0,void 0,et.mark(function n(){return et.wrap(function(n){for(;;)switch(n.prev=n.next){case 0:return n.next=2,io.default({uri:"https://maps.googleapis.com/maps/api/timezone/json",qs:{key:this._key,location:t.lat+","+t.lng,timestamp:Math.floor(Date.now()/1e3)}}).then(function(t){return t.timeZoneId},function(t){throw Error(t.errorMessage)});case 2:return n.abrupt("return",n.sent);case 3:case"end":return n.stop()}},n,this)}))}},{key:"getLatLon",value:function(t){return oo(this,void 0,void 0,et.mark(function n(){return et.wrap(function(n){for(;;)switch(n.prev=n.next){case 0:return n.next=2,io.default({uri:"https://maps.googleapis.com/maps/api/geocode/json",qs:{key:this._key,address:t}}).then(function(t){return t.results[0].geometry.location},function(t){throw Error(t.error_message)});case 2:return n.abrupt("return",n.sent);case 3:case"end":return n.stop()}},n,this)}))}}]),t}();ao._key="AIzaSyAXnIdQxap1WQuzG0XxHfYlCA5O9GQyvuY";var uo=ao,so={Person:uo},lo=function(){function t(n,e){u(this,t),this.p1=n,this.p2=e,this._types={conjunct:{major:!0,angle:0,orb:6,symbol:"<"},semisextile:{major:!1,angle:30,orb:3,symbol:"y"},decile:{major:!1,angle:36,orb:1.5,symbol:">"},novile:{major:!1,angle:40,orb:1.9,symbol:"M"},semisquare:{major:!1,angle:45,orb:3,symbol:"="},septile:{major:!1,angle:51.417,orb:2,symbol:"V"},sextile:{major:!0,angle:60,orb:6,symbol:"x"},quintile:{major:!1,angle:72,orb:2,symbol:"Y"},bilin:{major:!1,angle:75,orb:.9,symbol:"-"},binovile:{major:!1,angle:80,orb:2,symbol:";"},square:{major:!0,angle:90,orb:6,symbol:"c"},biseptile:{major:!1,angle:102.851,orb:2,symbol:"N"},tredecile:{major:!1,angle:108,orb:2,symbol:"X"},trine:{major:!0,angle:120,orb:6,symbol:"Q"},sesquiquadrate:{major:!1,angle:135,orb:3,symbol:"b"},biquintile:{major:!1,angle:144,orb:2,symbol:"C"},inconjunct:{major:!1,angle:150,orb:3,symbol:"n"},treseptile:{major:!1,angle:154.284,orb:1.1,symbol:"B"},tetranovile:{major:!1,angle:160,orb:3,symbol:":"},tao:{major:!1,angle:165,orb:1.5,symbol:"—"},opposition:{major:!0,angle:180,orb:6,symbol:"m"}};var r=n.longitude,o=e.longitude,i=Math.abs(r-o),a=n.isRetrograde(),s=e.isRetrograde(),l=Math.abs(n.speed),c=Math.abs(e.speed),f=!1;i>180+this._types.opposition.orb&&(i=r>o?360-r+o:360-o+r,f=!0);for(var h in this._types){var p=this._types[h];i>=p.angle-p.orb&&i<=p.angle+p.orb&&(this._type=h)}if("undefined"==typeof this._type)throw new Error("There is no aspect between these two planets.");this._orb=Number((i%1).toFixed(6));var d=i-this._types[this._type].angle;(d<0&&!f&&o>r||d>0&&!f&&r>o||d<0&&f&&r>o||d>0&&f&&o>r)&&(!a&&!s&&c>l||a&&s&&l>c||a&&!s)||(d>0&&!f&&o>r||d<0&&!f&&r>o||d>0&&f&&r>o||d<0&&f&&o>r)&&(!a&&!s&&l>c||a&&s&&c>l||!a&&s)?this._applying=!0:this._applying=!1}return Q(t,[{key:"isApplying",value:function(){return this._applying}},{key:"isMajor",value:function(){return this._types[this._type].major}},{key:"type",get:function(){return this._type}},{key:"orb",get:function(){return this._orb}},{key:"symbol",get:function(){return this._types[this._type].symbol}}]),t}(),co=lo,fo={Aspect:co},ho=o(function(t,n){var e=i&&i.__awaiter||function(t,n,e,r){return new(e||(e=Qr))(function(o,i){function a(t){try{s(r.next(t))}catch(t){i(t)}}function u(t){try{s(r.throw(t))}catch(t){i(t)}}function s(t){t.done?o(t.value):new e(function(n){n(t.value)}).then(a,u)}s((r=r.apply(t,n)).next())})},r=K,o=fo,a=ro;!function(t){t[t.Basic=0]="Basic",t[t.Transits=1]="Transits",t[t.Synastry=2]="Synastry",t[t.Combined=3]="Combined",t[t.Davison=4]="Davison",t[t.CombinedTransits=5]="CombinedTransits",t[t.DavisonTransits=6]="DavisonTransits"}(n.ChartType||(n.ChartType={}));var s=n.ChartType,l=function(){function t(n,e,r,o){var i=arguments.length>4&&void 0!==arguments[4]?arguments[4]:s.Basic;u(this,t),this.name=n,this.p1=e,this.p2=o,this.type=i,this._debug=!1,this._signs=[{name:"aries",symbol:"q",v:1},{name:"taurus",symbol:"w",v:1},{name:"gemini",symbol:"e",v:1},{name:"cancer",symbol:"r",v:1},{name:"leo",symbol:"t",v:1},{name:"virgo",symbol:"z",v:1},{name:"libra",symbol:"u",v:1},{name:"scorpio",symbol:"i",v:1},{name:"sagittarius",symbol:"o",v:1},{name:"capricorn",symbol:"p",v:1},{name:"aquarius",symbol:"ü",v:1},{name:"pisces",symbol:"+",v:1}];var a=void 0;switch(i){case s.Combined:a=this.calculateCombinedPlanets(r),this._planets1=this.getPlanets(a),this._ascendant=a.ascendant,this._houses=a.houses;break;case s.CombinedTransits:a=this.calculateCombinedPlanets(r),this._planets1=this.getPlanets(a),this._planets2=this.getPlanets(r[2]),this._ascendant=a.ascendant,this._houses=a.houses;break;default:this._planets1=this.getPlanets(r[0]),r[1]&&(this._planets2=this.getPlanets(r[1])),this._ascendant=r[0].ascendant,this._houses=r[0].houses}this.calculateAspects()}return Q(t,[{key:"getPlanets",value:function(t){var n=[];for(var e in t.planets){var o=t.planets[e];n.push(new r.Planet(o.name,o.lon,o.lat,o.spd))}return n}},{key:"calculateAspects",value:function(){if(this._aspects=[],this._planets2)for(var t in this._planets1)for(var n in this._planets2)try{this._aspects.push(new o.Aspect(this._planets1[t],this._planets2[n]))}catch(t){this._debug&&console.error(t)}else for(var e in this._planets1)for(var r in this._planets1)if(e!==r&&r>e)try{this._aspects.push(new o.Aspect(this._planets1[e],this._planets1[r]))}catch(t){this._debug&&console.error(t)}}},{key:"calculateCombinedPlanets",value:function(t){var n={planets:{sun:{name:null,lon:null,lat:null,spd:null,r:null},moon:{name:null,lon:null,lat:null,spd:null,r:null},mercury:{name:null,lon:null,lat:null,spd:null,r:null},venus:{name:null,lon:null,lat:null,spd:null,r:null},mars:{name:null,lon:null,lat:null,spd:null,r:null},jupiter:{name:null,lon:null,lat:null,spd:null,r:null},saturn:{name:null,lon:null,lat:null,spd:null,r:null},uranus:{name:null,lon:null,lat:null,spd:null,r:null},neptune:{name:null,lon:null,lat:null,spd:null,r:null},pluto:{name:null,lon:null,lat:null,spd:null,r:null},"north node":{name:"north node",lon:null,lat:null,spd:null,r:null},"south node":{name:"south node",lon:null,lat:null,spd:null,r:null},chiron:{name:null,lon:null,lat:null,spd:null,r:null},pholus:{name:null,lon:null,lat:null,spd:null,r:null},ceres:{name:null,lon:null,lat:null,spd:null,r:null},pallas:{name:null,lon:null,lat:null,spd:null,r:null},juno:{name:null,lon:null,lat:null,spd:null,r:null},vesta:{name:null,lon:null,lat:null,spd:null,r:null},cupido:{name:null,lon:null,lat:null,spd:null,r:null},chariklo:{name:null,lon:null,lat:null,spd:null,r:null},chaos:{name:null,lon:null,lat:null,spd:null,r:null},eris:{name:null,lon:null,lat:null,spd:null,r:null},nessus:{name:null,lon:null,lat:null,spd:null,r:null}},houses:[null,null,null,null,null,null,null,null,null,null,null,null],ascendant:null,mc:null};for(var e in t[0].planets)n.planets[e].name=e,n.planets[e].lon=this.getLonMidpoint(t[0].planets[e].lon,t[1].planets[e].lon),n.planets[e].lat=(t[0].planets[e].lat+t[1].planets[e].lat)/2,n.planets[e].spd=(t[0].planets[e].spd+t[1].planets[e].spd)/2;for(var r in t[0].houses)n.houses[r]=this.getLonMidpoint(t[0].houses[r],t[1].houses[r]);return n.ascendant=this.getLonMidpoint(t[0].ascendant,t[1].ascendant),n.mc=this.getLonMidpoint(t[0].mc,t[1].mc),n}},{key:"getLonMidpoint",value:function(t,n){var e=void 0,r=void 0,o=void 0;return t===n?t:(r=t>n?t:n,o=t0&&void 0!==arguments[0]?arguments[0]:null;return e(this,void 0,void 0,et.mark(function e(){var r;return et.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:if(s.Synastry!==this.type){e.next=2;break}throw new Error("You cannot refresh transits on a synastry chart");case 2:return null===n&&(n=(new Date).toISOString()),e.next=5,t.getChartData(n,this.p1.location);case 5:r=e.sent,this._planets2=this.getPlanets(r),this.calculateAspects();case 8:case"end":return e.stop()}},e,this)}))}},{key:"houses",get:function(){return this._houses}},{key:"aspects",get:function(){return this._aspects}},{key:"ascendant",get:function(){return this._ascendant}},{key:"innerPlanets",get:function(){return this._planets2?this._planets1:[]}},{key:"outerPlanets",get:function(){return this._planets2?this._planets2:this._planets1}}],[{key:"getChartData",value:function(t,n){return e(this,void 0,void 0,et.mark(function e(){return et.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,a.default({uri:"http://www.morphemeris.com/ephemeris.php",qs:{date:t,lat:n.lat,lon:n.lng}}).then(function(t){return t});case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}},e,this)}))}}]),t}();n.Chart=l}),po=i&&i.__awaiter||function(t,n,e,r){return new(e||(e=Qr))(function(o,i){function a(t){try{s(r.next(t))}catch(t){i(t)}}function u(t){try{s(r.throw(t))}catch(t){i(t)}}function s(t){t.done?o(t.value):new e(function(n){n(t.value)}).then(a,u)}s((r=r.apply(t,n)).next())})},yo=ho,vo=function(){function t(){u(this,t)}return Q(t,null,[{key:"create",value:function(n,e){var r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null,o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:yo.ChartType.Basic;return po(this,void 0,void 0,et.mark(function i(){var a,u,s;return et.wrap(function(i){for(;;)switch(i.prev=i.next){case 0:if(null!==n&&"undefined"!=typeof n&&0!==n.length){i.next=2;break}throw Error("Chart must have a name (ChartFactory)");case 2:if(null!==e&&"undefined"!=typeof e){i.next=4;break}throw Error("Person cannot be null or undefined (ChartFactory)");case 4:i.t0=o,i.next=i.t0===yo.ChartType.Synastry?7:i.t0===yo.ChartType.Combined?7:i.t0===yo.ChartType.CombinedTransits?7:i.t0===yo.ChartType.Davison?7:9;break;case 7:if(null!==r){i.next=9;break}throw Error("2nd Person cannot be null for this chart type (ChartFactory)");case 9:a=[],u=void 0,s=void 0,i.t1=o,i.next=i.t1===yo.ChartType.Transits?13:i.t1===yo.ChartType.Synastry?17:i.t1===yo.ChartType.Combined?17:i.t1===yo.ChartType.CombinedTransits?21:i.t1===yo.ChartType.Davison?25:i.t1===yo.ChartType.DavisonTransits?33:39;break;case 13:return i.next=15,Qr.all([yo.Chart.getChartData(e.date,e.location),yo.Chart.getChartData((new Date).toISOString(),e.location)]);case 15:return a=i.sent,i.abrupt("return",new yo.Chart(n,e,a,null,o));case 17:return i.next=19,Qr.all([yo.Chart.getChartData(e.date,e.location),yo.Chart.getChartData(r.date,r.location)]);case 19:return a=i.sent,i.abrupt("return",new yo.Chart(n,e,a,null,o));case 21:return i.next=23,Qr.all([yo.Chart.getChartData(e.date,e.location),yo.Chart.getChartData(r.date,r.location),yo.Chart.getChartData((new Date).toISOString(),e.location)]);case 23:return a=i.sent,i.abrupt("return",new yo.Chart(n,e,a,null,o));case 25:return u=t.getDatetimeMidpoint(e.date,r.date),s=t.getGeoMidpoint(e.location,r.location),i.t2=a,i.next=30,yo.Chart.getChartData(u,s);case 30:return i.t3=i.sent,i.t2.push.call(i.t2,i.t3),i.abrupt("return",new yo.Chart(n,e,a));case 33:return u=t.getDatetimeMidpoint(e.date,r.date),s=t.getGeoMidpoint(e.location,r.location),i.next=37,Qr.all([yo.Chart.getChartData(u,s),yo.Chart.getChartData((new Date).toISOString(),s)]);case 37:return a=i.sent,i.abrupt("return",new yo.Chart(n,e,a,null,o));case 39:return i.t4=a,i.next=42,yo.Chart.getChartData(e.date,e.location);case 42:return i.t5=i.sent,i.t4.push.call(i.t4,i.t5),i.abrupt("return",new yo.Chart(n,e,a));case 45:case"end":return i.stop()}},i,this)}))}},{key:"getGeoMidpoint",value:function(n,e){var r=t.toRadians(n.lat),o=t.toRadians(n.lng),i=t.toRadians(e.lat),a=t.toRadians(e.lng),u=Math.cos(i)*Math.cos(a-o),s=Math.cos(i)*Math.sin(a-o),l=o+Math.atan2(s,Math.cos(r)+u),c=Math.atan2(Math.sin(r)+Math.sin(i),Math.sqrt(Math.pow(Math.cos(r)+u,2)+Math.pow(s,2)));return{lat:t.toDegrees(c),lng:t.toDegrees(l)}}},{key:"getDatetimeMidpoint",value:function(t,n){var e=new Date(t).getTime(),r=new Date(n).getTime(),o=void 0;return e===r?t:(o=e del(['dist']); 24 | export { clean }; 25 | 26 | export function build_dist_js() { 27 | let project = proj.src() 28 | .pipe(maps.init()) 29 | .pipe(ts(proj)); 30 | 31 | let js = project.js 32 | .pipe(concat('astrologyjs.min.js')); 33 | 34 | let dts = project.dts 35 | .pipe(concat('astrologyjs.d.ts')); 36 | 37 | return merge([ 38 | js.pipe(maps.write('.')).pipe(gulp.dest(paths.dist_js)), 39 | dts.pipe(gulp.dest(paths.dist_js)) 40 | ]); 41 | } 42 | 43 | export function build_dist() { 44 | let proj = gulp.src(["src/astrologyjs.ts"]) 45 | .pipe(maps.init()) 46 | .pipe(ts({ 47 | "target": "es6", 48 | "declaration": true, 49 | "noImplicitAny": true, 50 | "suppressImplicitAnyIndexErrors": true, 51 | "noImplicitReturns": true, 52 | // "strictNullChecks": true, 53 | "removeComments": true, 54 | "sourceMap": true 55 | })); 56 | 57 | let js = proj.js; 58 | 59 | let dts = proj.dts 60 | .pipe(concat('astrologyjs.d.ts')); 61 | 62 | return merge([ 63 | js.pipe(maps.write('.')).pipe(gulp.dest(paths.dist)), 64 | dts.pipe(gulp.dest(paths.dist)) 65 | ]); 66 | } 67 | 68 | const build = gulp.series(clean, build_dist); 69 | export { build }; 70 | 71 | const clean_specs = () => del(["spec/*.js"]); 72 | export { clean_specs }; 73 | 74 | export function build_specs() { 75 | return gulp.src(["src/**/*.spec.ts", "src/astrologyjs.ts"]) 76 | .pipe(maps.init()) 77 | .pipe(ts({ 78 | "target": "ES6", 79 | "module": "commonjs" 80 | })) 81 | .pipe(maps.write({includeContent: false, sourceRoot: "../src"})) 82 | .pipe(gulp.dest(paths.spec)); 83 | } 84 | 85 | export function pre_spec() { 86 | return gulp.src(["spec/**/*.js","!spec/**/*.spec.js"]) 87 | .pipe(istanbul()) 88 | .pipe(istanbul.hookRequire()); 89 | } 90 | 91 | export function run_specs() { 92 | return gulp.src(paths.specs) 93 | .pipe(shell('clear')) 94 | .pipe(jasmine({ 95 | reporter: new jsr() 96 | })) 97 | .pipe(istanbul.writeReports({ 98 | print: "detail" 99 | })); 100 | } 101 | 102 | export function remap_coverage() { 103 | return gulp.src('coverage/coverage-final.json') 104 | .pipe(remap({ 105 | basepath: "src", 106 | reports: { 107 | "html": "coverage/html-report", 108 | "json": "coverage/coverage-final.json", 109 | "lcovonly": "coverage/lcov.info" 110 | } 111 | })); 112 | } 113 | 114 | const spec = gulp.series(clean_specs, build_specs, pre_spec, run_specs, remap_coverage); 115 | export { spec }; 116 | 117 | export function watch() { 118 | gulp.watch(paths.src, spec); 119 | } 120 | 121 | export default watch; -------------------------------------------------------------------------------- /jasmine-runner.js: -------------------------------------------------------------------------------- 1 | var Jasmine = require('jasmine'); 2 | var SpecReporter = require('jasmine-spec-reporter'); 3 | 4 | var jrunner = new Jasmine(); 5 | jrunner.env.clearReporters(); // jasmine >= 2.5.2, remove default reporter logs 6 | jrunner.addReporter(new SpecReporter()); // add jasmine-spec-reporter 7 | jrunner.loadConfigFile('./jasmine.json'); // load jasmine.json configuration 8 | jrunner.execute(); 9 | -------------------------------------------------------------------------------- /jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "**/*[sS]pec.js" 5 | ], 6 | "helpers": [ 7 | "helpers/**/*.js" 8 | ], 9 | "stopSpecOnExpectationFailure": false, 10 | "random": false 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "astrologyjs", 3 | "version": "1.3.1", 4 | "description": "An astrological charting library.", 5 | "main": "dist/astrologyjs.min.js", 6 | "jsnext:main": "dist/astrologyjs-es6.js", 7 | "types": "dist/astrologyjs.d.ts", 8 | "scripts": { 9 | "test": "npm run build:test && npm run cover && npm run remap", 10 | "cover": "istanbul cover -x '**/*.spec.js' -x jasmine-runner.js jasmine-runner.js", 11 | "uglify": "uglifyjs dist/astrologyjs.js -m -c warnings=false > dist/astrologyjs.min.js", 12 | "remap:json": "remap-istanbul -i coverage/coverage.json -o coverage/coverage-remapped.json", 13 | "remap:lcov": "remap-istanbul -i coverage/coverage.json -o coverage/coverage-remapped.lcov -t lcovonly", 14 | "remap:html": "remap-istanbul -i coverage/coverage.json -o coverage/lcov-report -t html", 15 | "remap": "npm run remap:json && npm run remap:lcov && npm run remap:html", 16 | "build:def": "tsc -p tsconfig.def.json && mv src/js/astrologyjs.d.ts dist && rm src/js/astrologyjs.js", 17 | "build:test": "tsc -p tsconfig.test.json", 18 | "build:dist": "tsc -p tsconfig.json && rollup -c", 19 | "build": "npm run build:def && npm run build:dist && npm run uglify" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/morphatic/astrologyjs.git" 24 | }, 25 | "keywords": [ 26 | "astrology" 27 | ], 28 | "author": "Morgan Benton (http://morphatic.com)", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/morphatic/astrologyjs/issues" 32 | }, 33 | "homepage": "https://github.com/morphatic/astrologyjs#readme", 34 | "devDependencies": { 35 | "@types/jasmine": "^2.5.37", 36 | "@types/node": "^6.0.48", 37 | "@types/request": "0.0.33", 38 | "@types/request-promise": "^4.1.33", 39 | "babel-plugin-transform-runtime": "^6.15.0", 40 | "babel-preset-es2015-rollup": "^1.2.0", 41 | "chai": "^3.5.0", 42 | "jasmine": "^2.5.2", 43 | "jasmine-core": "^2.5.2", 44 | "jasmine-spec-reporter": "^2.7.0", 45 | "remap-istanbul": "^0.7.0", 46 | "rollup-plugin-babel": "^2.6.1", 47 | "rollup-plugin-commonjs": "^5.0.5", 48 | "rollup-plugin-eslint": "^3.0.0", 49 | "rollup-plugin-json": "^2.0.2", 50 | "rollup-plugin-node-resolve": "^2.0.0" 51 | }, 52 | "dependencies": { 53 | "babel-runtime": "^6.18.0", 54 | "node": "0.0.0" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import json from 'rollup-plugin-json'; 2 | import babel from 'rollup-plugin-babel'; 3 | import eslint from 'rollup-plugin-eslint'; 4 | import resolve from 'rollup-plugin-node-resolve'; 5 | import commonjs from 'rollup-plugin-commonjs'; 6 | 7 | export default { 8 | entry: 'src/js/astrologyjs.js', 9 | format: 'umd', 10 | plugins: [ 11 | resolve({ 12 | jsnext: true, 13 | browser: true, 14 | preferBuiltins: false 15 | }), 16 | commonjs(), 17 | eslint(), 18 | babel({ 19 | exclude: 'node_modules/**', 20 | runtimeHelpers: true 21 | }), 22 | json() 23 | ], 24 | external: ['http', 'https'], 25 | globals: { 26 | 'http': 'http', 27 | 'https': 'https' 28 | }, 29 | exports: 'named', 30 | targets: [ 31 | { 32 | dest: 'dist/astrologyjs.js', 33 | format: 'umd', 34 | moduleName: 'astrologyjs', 35 | sourceMap: true 36 | }, 37 | { 38 | dest: 'dist/astrologyjs-es6.js', 39 | format: 'es' 40 | } 41 | ] 42 | }; -------------------------------------------------------------------------------- /src/aspect.spec.ts: -------------------------------------------------------------------------------- 1 | import { Planet } from "./planet"; 2 | import { Aspect } from "./aspect"; 3 | 4 | describe("An Aspect", () => { 5 | 6 | let a: Aspect; 7 | 8 | /** 9 | * Planets at 2016-06-07 1:00AM EDT, Harrisonburg, VA 10 | */ 11 | beforeEach(() => { 12 | let venus = new Planet("Venus", 76.964243, 0.006298, 1.228344), 13 | pluto = new Planet("Pluto", 286.924401, 1.416432, -0.020512); 14 | a = new Aspect(venus, pluto); 15 | }); 16 | 17 | it("throws an error if there is no aspect between the planets", () => { 18 | let moon = new Planet("Moon", 105.106905, -4.432968, 14.105873), 19 | mars = new Planet("Mars", 236.530798, -1.912147, -0.289540), 20 | ua: Aspect; 21 | try { 22 | ua = new Aspect(moon, mars); 23 | } catch (err) { 24 | expect(err.message).toBe("There is no aspect between these two planets."); 25 | } 26 | }); 27 | 28 | it("has two planets", () => { 29 | expect(a.p1).toBeDefined(); 30 | expect(a.p1.name).toBe("Venus"); 31 | expect(a.p2).toBeDefined(); 32 | expect(a.p2.name).toBe("Pluto"); 33 | }); 34 | 35 | it("has a type", () => { 36 | expect(a.type).toBeDefined(); 37 | expect(a.type).toBe("inconjunct"); 38 | }); 39 | 40 | it("has a symbol", () => { 41 | expect(a.symbol).toBeDefined(); 42 | expect(a.symbol).toBe("n"); 43 | }); 44 | 45 | it("can be considered 'major' or 'minor'", () => { 46 | expect(a.isMajor).toBeDefined(); 47 | expect(a.isMajor()).toBe(false); 48 | }); 49 | 50 | it("has an orb", () => { 51 | expect(a.orb).toBeDefined(); 52 | expect(a.orb).toBe(0.039842); 53 | }); 54 | 55 | it("is applying or separating", () => { 56 | let moon = new Planet("Moon", 105.106905, -4.432968, 14.105873), 57 | mercury = new Planet("Mercury", 53.033163, -3.428666, 1.052675), 58 | mars = new Planet("Mars", 236.530798, -1.912147, -0.289540), 59 | jupiter = new Planet("Jupiter", 164.462849, 1.268437, 0.081820), 60 | saturn = new Planet("Saturn", 252.825595, 1.798099, -0.073836), 61 | neptune = new Planet("Neptune", 342.027801, -0.836814, 0.003678), 62 | northnode = new Planet("North Node", 167.118052, 0.000000, -0.154248), 63 | cupido = new Planet("Cupido", 266.223864, 0.776123, -0.020065); 64 | 65 | // return type should be boolean 66 | expect(a.isApplying()).toEqual(jasmine.any(Boolean)); 67 | 68 | // should be false since aspect is separating 69 | expect(a.isApplying()).toBe(false); 70 | 71 | // testing a variety of aspects that are separating or applying 72 | // under various conditions 73 | a = new Aspect(mars, cupido); 74 | expect(a.isApplying()).toBe(true); 75 | a = new Aspect(mars, jupiter); 76 | expect(a.isApplying()).toBe(true); 77 | a = new Aspect(mercury, neptune); 78 | expect(a.isApplying()).toBe(true); 79 | a = new Aspect(neptune, mercury); // this helps our coverage 80 | expect(a.isApplying()).toBe(true); 81 | // a = new Aspect(mars, northnode); // this test is out of orb, but works 82 | // expect(a.isApplying()).toBe(false); 83 | a = new Aspect(moon, saturn); 84 | expect(a.isApplying()).toBe(false); 85 | a = new Aspect(moon, neptune); 86 | expect(a.isApplying()).toBe(false); 87 | }); 88 | }); -------------------------------------------------------------------------------- /src/aspect.ts: -------------------------------------------------------------------------------- 1 | import { Planet } from "./planet"; 2 | 3 | interface AspectType { 4 | major: boolean; 5 | angle: number; 6 | orb: number; 7 | symbol: string; 8 | } 9 | 10 | interface AspectTypeArray { 11 | [name: string]: AspectType; 12 | } 13 | 14 | /** 15 | * Represents an aspect between two planets 16 | */ 17 | export class Aspect { 18 | 19 | /** 20 | * A label naming the aspect type, e.g. trine 21 | * @type {string} 22 | */ 23 | private _type: string; 24 | 25 | /** 26 | * Number of degrees away from being perfectly in aspect 27 | * @type {number} 28 | */ 29 | private _orb: number; 30 | 31 | /** 32 | * Is the aspect applying or separating 33 | * @type {boolean} 34 | */ 35 | private _applying: boolean; 36 | 37 | /** 38 | * Catalog of all of the aspect types available in our system 39 | * @type {AspectTypeArray} 40 | */ 41 | private _types: AspectTypeArray = { 42 | "conjunct": { major: true, angle: 0, orb: 6 , symbol: "<" }, 43 | "semisextile": { major: false, angle: 30, orb: 3 , symbol: "y" }, 44 | "decile": { major: false, angle: 36, orb: 1.5, symbol: ">" }, 45 | "novile": { major: false, angle: 40, orb: 1.9, symbol: "M" }, 46 | "semisquare": { major: false, angle: 45, orb: 3 , symbol: "=" }, 47 | "septile": { major: false, angle: 51.417, orb: 2 , symbol: "V" }, 48 | "sextile": { major: true, angle: 60, orb: 6 , symbol: "x" }, 49 | "quintile": { major: false, angle: 72, orb: 2 , symbol: "Y" }, 50 | "bilin": { major: false, angle: 75, orb: 0.9, symbol: "-" }, 51 | "binovile": { major: false, angle: 80, orb: 2 , symbol: ";" }, 52 | "square": { major: true, angle: 90, orb: 6 , symbol: "c" }, 53 | "biseptile": { major: false, angle: 102.851, orb: 2 , symbol: "N" }, 54 | "tredecile": { major: false, angle: 108, orb: 2 , symbol: "X" }, 55 | "trine": { major: true, angle: 120, orb: 6 , symbol: "Q" }, 56 | "sesquiquadrate": { major: false, angle: 135, orb: 3 , symbol: "b" }, 57 | "biquintile": { major: false, angle: 144, orb: 2 , symbol: "C" }, 58 | "inconjunct": { major: false, angle: 150, orb: 3 , symbol: "n" }, 59 | "treseptile": { major: false, angle: 154.284, orb: 1.1, symbol: "B" }, 60 | "tetranovile": { major: false, angle: 160, orb: 3 , symbol: ":" }, 61 | "tao": { major: false, angle: 165, orb: 1.5, symbol: "—" }, 62 | "opposition": { major: true, angle: 180, orb: 6 , symbol: "m" } 63 | }; 64 | 65 | /** 66 | * Creates a new Aspect or throws an error if no aspect exists 67 | * between the planets 68 | * @param {Planet} public p1 First planet in the relationship 69 | * @param {Planet} public p2 Second planet in the relationship 70 | */ 71 | constructor(public p1: Planet, public p2: Planet) { 72 | // get key properties of the planets 73 | let l1 = p1.longitude, 74 | l2 = p2.longitude, 75 | ng = Math.abs( l1 - l2 ), 76 | r1 = p1.isRetrograde(), 77 | r2 = p2.isRetrograde(), 78 | s1 = Math.abs(p1.speed), 79 | s2 = Math.abs(p2.speed), 80 | ct = false; // corrected? 81 | 82 | // correct for cases where the angle > 180 + the orb of opposition 83 | if (ng > 180 + this._types["opposition"].orb) { 84 | ng = l1 > l2 ? 360 - l1 + l2 : 360 - l2 + l1; 85 | ct = true; 86 | } 87 | 88 | // determine the aspect type 89 | for (let type in this._types) { 90 | let t = this._types[type]; 91 | if (ng >= t.angle - t.orb && ng <= t.angle + t.orb) { 92 | this._type = type; 93 | } 94 | } 95 | 96 | // bail out if there is no in-orb aspect between these two planets 97 | if (typeof this._type === "undefined") { 98 | throw new Error("There is no aspect between these two planets."); 99 | } 100 | 101 | // determine the orb 102 | this._orb = Number((ng % 1).toFixed(6)); 103 | 104 | // determine if it is applying or not; use speed magnitude (i.e. absolute value) 105 | let orb = ng - this._types[this._type].angle; 106 | // planets are in aspect across 0° Aries 107 | if (( ( (orb < 0 && !ct && l2 > l1) || (orb > 0 && !ct && l1 > l2) || 108 | (orb < 0 && ct && l1 > l2) || (orb > 0 && ct && l2 > l1) ) && 109 | ( (!r1 && !r2 && s2 > s1) || (r1 && r2 && s1 > s2) || (r1 && !r2) ) || 110 | ( ( (orb > 0 && !ct && l2 > l1) || (orb < 0 && !ct && l1 > l2) || 111 | (orb > 0 && ct && l1 > l2) || (orb < 0 && ct && l2 > l1) ) && 112 | ( (!r1 && !r2 && s1 > s2) || (r1 && r2 && s2 > s1) || (!r1 && r2) ) ) ) 113 | ) { 114 | this._applying = true; 115 | } else { 116 | this._applying = false; 117 | } 118 | } 119 | 120 | /** 121 | * Get the type assigned to this aspect 122 | * @return {string} One of the aspect type names 123 | */ 124 | get type(): string { return this._type; } 125 | 126 | /** 127 | * Get the number of degrees away from being in perfect aspect 128 | * @return {number} The number of degrees (absolute value) 129 | */ 130 | get orb(): number { return this._orb; } 131 | 132 | /** 133 | * Get the character that will produce the correct symbol for 134 | * this aspect in the Kairon Semiserif font 135 | * @return {string} A character representing a symbol 136 | */ 137 | get symbol(): string { return this._types[this._type].symbol; } 138 | 139 | /** 140 | * Is the aspect applying or separating? 141 | * @return {boolean} True if the aspect is applying 142 | */ 143 | isApplying(): boolean { return this._applying; } 144 | 145 | /** 146 | * Is this a "major" aspect? i.e. one of those you usually 147 | * hear about in astrological forecasts 148 | * @return {boolean} True if this is a "major" aspect 149 | */ 150 | isMajor(): boolean { return this._types[this._type].major; } 151 | } 152 | -------------------------------------------------------------------------------- /src/astrologyjs.ts: -------------------------------------------------------------------------------- 1 | export { Planet } from "./planet"; 2 | export { Person, Point } from "./person"; 3 | export { Aspect } from "./aspect"; 4 | export { Chart, ChartType } from "./chart"; 5 | export { ChartFactory } from "./chart-factory"; 6 | -------------------------------------------------------------------------------- /src/chart-factory.spec.ts: -------------------------------------------------------------------------------- 1 | import { Person } from "./person"; 2 | import { Chart, ChartType } from "./chart"; 3 | import { ChartFactory } from "./chart-factory"; 4 | 5 | describe("A Chart Factory", () => { 6 | 7 | let c: Chart, 8 | morgan: Person, 9 | nicole: Person, 10 | testAsync = (runAsync: any) => { return (done: any) => { runAsync().then(done, (e: any) => { fail(e); done(); }); }; }; 11 | 12 | beforeAll(testAsync(async () => { 13 | morgan = await Person.create("Morgan", "1974-02-17T23:30Z", {lat: 37.4381927, lng: -79.18932}); 14 | nicole = await Person.create("Nicole", "1976-04-25T13:02Z", {lat: 35.2033533, lng: -80.9796095}); 15 | })); 16 | 17 | it("can convert degrees to radians", () => { 18 | expect(ChartFactory.toRadians(180)).toBeCloseTo(3.14159, 5); 19 | }); 20 | 21 | it("can convert radians to degrees", () => { 22 | expect(ChartFactory.toDegrees(3.14159)).toBeCloseTo(180, 2); 23 | }); 24 | 25 | it("can find the geographic midpoint of two lat/lng pairs", () => { 26 | let mp = ChartFactory.getGeoMidpoint(morgan.location, nicole.location); 27 | expect(mp.lat).toBeCloseTo( 36.32411, 5); 28 | expect(mp.lng).toBeCloseTo(-80.09730, 5); 29 | }); 30 | 31 | it("can find the datetime midpoint between two datetimes", () => { 32 | let d1 = "1974-02-17T23:30:00.000Z", 33 | d2 = "1976-04-25T13:02:00.000Z"; 34 | expect(ChartFactory.getDatetimeMidpoint(d1, d2)).toBe("1975-03-23T18:16:00.000Z"); 35 | expect(ChartFactory.getDatetimeMidpoint(d2, d1)).toBe("1975-03-23T18:16:00.000Z"); 36 | expect(ChartFactory.getDatetimeMidpoint(d1, d1)).toBe(d1); 37 | }); 38 | 39 | it("can get correct chart data", testAsync(async () => { 40 | let date = "1974-02-17T23:30:00.000Z", // this is LOCAL time, NOT corrected for time zone 41 | p = { 42 | lat: 37.4381927, 43 | lng: -79.18932 44 | }, 45 | expected = {"planets":{"sun":{"name":"sun","lon":328.928849,"lat":-0.000030,"spd":1.009225,"r":0},"moon":{"name":"moon","lon":282.376739,"lat":1.370274,"spd":11.765058,"r":0},"mercury":{"name":"mercury","lon":341.436442,"lat":2.930657,"spd":-0.369939,"r":1},"venus":{"name":"venus","lon":296.234418,"lat":6.898336,"spd":0.180950,"r":0},"mars":{"name":"mars","lon":54.859512,"lat":1.560358,"spd":0.535861,"r":0},"jupiter":{"name":"jupiter","lon":325.604490,"lat":-0.768867,"spd":0.239856,"r":0},"saturn":{"name":"saturn","lon":87.876187,"lat":-0.907421,"spd":-0.018641,"r":1},"uranus":{"name":"uranus","lon":207.650784,"lat":0.599481,"spd":-0.015184,"r":1},"neptune":{"name":"neptune","lon":249.481255,"lat":1.582277,"spd":0.012389,"r":0},"pluto":{"name":"pluto","lon":186.394398,"lat":17.038291,"spd":-0.020345,"r":1},"north node":{"name":"north node","lon":266.806626,"lat":0.000000,"spd":-0.041911,"r":1},"south node":{"name":"south node","lon":86.806626,"lat":0.000000,"spd":-0.041911,"r":1},"chiron":{"name":"chiron","lon":17.572261,"lat":1.075862,"spd":0.042981,"r":0},"pholus":{"name":"pholus","lon":336.731188,"lat":-15.156539,"spd":0.050265,"r":0},"ceres":{"name":"ceres","lon":308.550770,"lat":-5.365279,"spd":0.386702,"r":0},"pallas":{"name":"pallas","lon":289.569620,"lat":28.398472,"spd":0.358521,"r":0},"juno":{"name":"juno","lon":304.984621,"lat":8.062667,"spd":0.396055,"r":0},"vesta":{"name":"vesta","lon":197.431809,"lat":10.398225,"spd":-0.007200,"r":1},"cupido":{"name":"cupido","lon":208.369943,"lat":1.060455,"spd":-0.008330,"r":1},"chariklo":{"name":"chariklo","lon":4.202544,"lat":20.675991,"spd":0.056467,"r":0},"chaos":{"name":"chaos","lon":24.103632,"lat":-4.986552,"spd":0.015641,"r":0},"eris":{"name":"eris","lon":12.620390,"lat":-20.186308,"spd":0.008337,"r":0},"nessus":{"name":"nessus","lon":83.280862,"lat":13.098433,"spd":-0.011314,"r":1}},"houses":[156.240991,180.773240,209.977632,242.916806,276.680605,308.179752,336.240991,0.773240,29.977632,62.916806,96.680605,128.179752],"ascendant":156.240991,"mc":62.916806}, 46 | data = await Chart.getChartData(date, p); 47 | expect(data).toEqual(expected); 48 | })); 49 | 50 | it("instantiates a basic chart", testAsync( async () => { 51 | c = await ChartFactory.create("Morgan natal", morgan); 52 | expect(c instanceof Chart).toBe(true); 53 | expect(c._planets1.length).toBe(23); 54 | expect(c._aspects).toBeDefined(); 55 | })); 56 | 57 | it("instantiates a chart with transits", testAsync( async () => { 58 | c = await ChartFactory.create("Morgan transits", morgan, null, ChartType.Transits); 59 | expect(c._planets1.length).toBe(23); 60 | expect(c._planets2.length).toBe(23); 61 | expect(c._aspects).toBeDefined(); 62 | })); 63 | 64 | it("instantiates a synastry chart", testAsync( async () => { 65 | c = await ChartFactory.create("Morgan/Nicole synastry", morgan, nicole, ChartType.Synastry); 66 | expect(c._planets1.length).toBe(23); 67 | expect(c._planets2.length).toBe(23); 68 | expect(c._aspects).toBeDefined(); 69 | })); 70 | 71 | it("instantiates a combined chart", testAsync( async () => { 72 | c = await ChartFactory.create("Morgan/Nicole Combined", morgan, nicole, ChartType.Combined); 73 | expect(c._planets1.length).toBe(23); 74 | expect(c._planets2).not.toBeDefined(); 75 | expect(c._aspects).toBeDefined(); 76 | })); 77 | 78 | it("instantiates a Davison chart", testAsync( async () => { 79 | c = await ChartFactory.create("Morgan/Nicole Davison", morgan, nicole, ChartType.Davison); 80 | expect(c._planets1.length).toBe(23); 81 | expect(c._planets2).not.toBeDefined(); 82 | expect(c._aspects).toBeDefined(); 83 | })); 84 | 85 | it("instantiates a combined chart with transits", testAsync( async () => { 86 | c = await ChartFactory.create("Morgan/Nicole Combined", morgan, nicole, ChartType.CombinedTransits); 87 | expect(c._planets1.length).toBe(23); 88 | expect(c._planets2.length).toBe(23); 89 | expect(c._aspects).toBeDefined(); 90 | })); 91 | 92 | it("instantiates a Davison chart with transits", testAsync( async () => { 93 | c = await ChartFactory.create("Morgan/Nicole Davison", morgan, nicole, ChartType.DavisonTransits); 94 | expect(c._planets1.length).toBe(23); 95 | expect(c._planets2.length).toBe(23); 96 | expect(c._aspects).toBeDefined(); 97 | })); 98 | 99 | it("throws an error if no name was specified", testAsync( async () => { 100 | try { 101 | c = await ChartFactory.create("", morgan); 102 | } catch (err) { 103 | expect(err.message).toBe("Chart must have a name (ChartFactory)"); 104 | } 105 | })); 106 | 107 | it("throws an error if null name was specified", testAsync( async () => { 108 | try { 109 | c = await ChartFactory.create(null, morgan); 110 | } catch (err) { 111 | expect(err.message).toBe("Chart must have a name (ChartFactory)"); 112 | } 113 | })); 114 | 115 | it("throws an error if undefined name was specified", testAsync( async () => { 116 | let name: string; 117 | try { 118 | c = await ChartFactory.create(name, morgan); 119 | } catch (err) { 120 | expect(err.message).toBe("Chart must have a name (ChartFactory)"); 121 | } 122 | })); 123 | 124 | it("throws an error if first person is null", testAsync( async () => { 125 | try { 126 | c = await ChartFactory.create("Morgan natal", null); 127 | } catch (err) { 128 | expect(err.message).toBe("Person cannot be null or undefined (ChartFactory)"); 129 | } 130 | })); 131 | 132 | it("throws an error if first person is undefined", testAsync( async () => { 133 | let person: Person; 134 | try { 135 | c = await ChartFactory.create("Morgan natal", person); 136 | } catch (err) { 137 | expect(err.message).toBe("Person cannot be null or undefined (ChartFactory)"); 138 | } 139 | })); 140 | 141 | it("throws an error if second person is undefined for Synastry", testAsync( async () => { 142 | let person: Person; 143 | try { 144 | c = await ChartFactory.create("Morgan natal", morgan, person, ChartType.Synastry); 145 | } catch (err) { 146 | expect(err.message).toBe("2nd Person cannot be null for this chart type (ChartFactory)"); 147 | } 148 | })); 149 | 150 | }); -------------------------------------------------------------------------------- /src/chart-factory.ts: -------------------------------------------------------------------------------- 1 | import { Person, Point } from "./person"; 2 | import { Chart, ChartType, ChartData } from "./chart"; 3 | 4 | /** 5 | * Usage: let chart: Chart = ChartFactory.create("my chart", person); 6 | */ 7 | export class ChartFactory { 8 | 9 | static async create(name: string, p1: Person, p2: Person = null, type: ChartType = ChartType.Basic) { 10 | // make sure a name was passed in 11 | if (null === name || "undefined" === typeof name || 0 === name.length) { 12 | throw Error("Chart must have a name (ChartFactory)"); 13 | } 14 | // check for undefined people 15 | if (null === p1 || typeof p1 === "undefined") { 16 | throw Error("Person cannot be null or undefined (ChartFactory)"); 17 | } 18 | switch (type) { 19 | case ChartType.Synastry: 20 | case ChartType.Combined: 21 | case ChartType.CombinedTransits: 22 | case ChartType.Davison: 23 | if (null === p2) { 24 | throw Error("2nd Person cannot be null for this chart type (ChartFactory)"); 25 | } 26 | } 27 | 28 | let cdata: Array = [], date: string, p: Point; 29 | switch (type) { 30 | case ChartType.Transits: 31 | cdata = await Promise.all([ 32 | Chart.getChartData(p1.date, p1.location), 33 | Chart.getChartData(new Date().toISOString(), p1.location) 34 | ]); 35 | return new Chart(name, p1, cdata, null, type); 36 | case ChartType.Synastry: 37 | case ChartType.Combined: 38 | cdata = await Promise.all([ 39 | Chart.getChartData(p1.date, p1.location), 40 | Chart.getChartData(p2.date, p2.location) 41 | ]); 42 | return new Chart(name, p1, cdata, null, type); 43 | case ChartType.CombinedTransits: 44 | cdata = await Promise.all([ 45 | Chart.getChartData(p1.date, p1.location), 46 | Chart.getChartData(p2.date, p2.location), 47 | Chart.getChartData(new Date().toISOString(), p1.location) 48 | ]); 49 | return new Chart(name, p1, cdata, null, type); 50 | case ChartType.Davison: 51 | date = ChartFactory.getDatetimeMidpoint(p1.date, p2.date); 52 | p = ChartFactory.getGeoMidpoint(p1.location, p2.location); 53 | cdata.push(await Chart.getChartData(date, p)); 54 | return new Chart(name, p1, cdata); 55 | case ChartType.DavisonTransits: 56 | date = ChartFactory.getDatetimeMidpoint(p1.date, p2.date); 57 | p = ChartFactory.getGeoMidpoint(p1.location, p2.location); 58 | cdata = await Promise.all([ 59 | Chart.getChartData(date, p), 60 | Chart.getChartData(new Date().toISOString(), p) 61 | ]); 62 | return new Chart(name, p1, cdata, null, type); 63 | default: 64 | cdata.push(await Chart.getChartData(p1.date, p1.location)); 65 | return new Chart(name, p1, cdata); 66 | } 67 | } 68 | 69 | /** 70 | * Calculates the lat/lon of the geographic midpoint between two lat/lon pairs 71 | * @param {Point} p1 Latitude/longitude of first location 72 | * @param {Point} p2 Latitude/longitude of second location 73 | * @return {Point} The latitude/longitude of the geographic midpoint 74 | */ 75 | static getGeoMidpoint(p1: Point, p2: Point): Point { 76 | let lat1 = ChartFactory.toRadians( p1.lat ), 77 | lng1 = ChartFactory.toRadians( p1.lng ), 78 | lat2 = ChartFactory.toRadians( p2.lat ), 79 | lng2 = ChartFactory.toRadians( p2.lng ), 80 | bx = Math.cos( lat2 ) * Math.cos( lng2 - lng1 ), 81 | by = Math.cos( lat2 ) * Math.sin( lng2 - lng1 ), 82 | lng3 = lng1 + Math.atan2( by, Math.cos( lat1 ) + bx ), 83 | lat3 = Math.atan2( Math.sin( lat1 ) + Math.sin( lat2 ), 84 | Math.sqrt( Math.pow( Math.cos( lat1 ) + bx, 2 ) + Math.pow( by, 2 ) ) ); 85 | 86 | return { 87 | lat: ChartFactory.toDegrees( lat3 ), 88 | lng: ChartFactory.toDegrees( lng3 ) 89 | }; 90 | } 91 | 92 | /** 93 | * Finds the exact midpoint between two dates 94 | * @param {string} date1 The first date 95 | * @param {string} date2 The second date 96 | * @return {string} The midpoint date as an ISO 8601 string 97 | */ 98 | static getDatetimeMidpoint(date1: string, date2: string): string { 99 | let d1 = new Date(date1).getTime(), 100 | d2 = new Date(date2).getTime(), 101 | ts: number; 102 | 103 | // if two dates are the same, midpoint is just that date 104 | if (d1 === d2) { 105 | return date1; 106 | } 107 | 108 | ts = d1 < d2 ? d1 + ((d2 - d1) / 2) : d2 + ((d1 - d2) / 2); 109 | return new Date(ts).toISOString(); 110 | } 111 | 112 | /** 113 | * Converts decimal degrees to radians 114 | * @param {number} degrees Decimal representation of degrees to be converted 115 | * @return {number} Returns radians 116 | */ 117 | static toRadians = (degrees: number) => degrees * Math.PI / 180; 118 | 119 | /** 120 | * Converts radians to decimal degrees 121 | * @param {number} radians Radians to be converted 122 | * @return {number} Returns decimal degrees 123 | */ 124 | static toDegrees = (radians: number) => radians * 180 / Math.PI; 125 | } 126 | -------------------------------------------------------------------------------- /src/chart.spec.ts: -------------------------------------------------------------------------------- 1 | import { Person } from "./person"; 2 | import { Planet } from "./planet"; 3 | import { Chart, ChartType, ChartDataArray } from "./chart"; 4 | import { ChartFactory } from "./chart-factory"; 5 | 6 | describe("A Chart", () => { 7 | 8 | let c: Chart, 9 | morgan: Person, 10 | nicole: Person, 11 | cdata: ChartDataArray, 12 | testAsync = (runAsync: any) => { return (done: any) => { runAsync().then(done, (e: any) => { fail(e); done(); }); }; }; 13 | 14 | beforeAll(testAsync( async () => { 15 | cdata = [ 16 | {"planets":{"sun":{"name":"sun","lon":328.928849,"lat":-0.000030,"spd":1.009225,"r":0},"moon":{"name":"moon","lon":282.376739,"lat":1.370274,"spd":11.765058,"r":0},"mercury":{"name":"mercury","lon":341.436442,"lat":2.930657,"spd":-0.369939,"r":1},"venus":{"name":"venus","lon":296.234418,"lat":6.898336,"spd":0.180950,"r":0},"mars":{"name":"mars","lon":54.859512,"lat":1.560358,"spd":0.535861,"r":0},"jupiter":{"name":"jupiter","lon":325.604490,"lat":-0.768867,"spd":0.239856,"r":0},"saturn":{"name":"saturn","lon":87.876187,"lat":-0.907421,"spd":-0.018641,"r":1},"uranus":{"name":"uranus","lon":207.650784,"lat":0.599481,"spd":-0.015184,"r":1},"neptune":{"name":"neptune","lon":249.481255,"lat":1.582277,"spd":0.012389,"r":0},"pluto":{"name":"pluto","lon":186.394398,"lat":17.038291,"spd":-0.020345,"r":1},"north node":{"name":"north node","lon":266.806626,"lat":0.000000,"spd":-0.041911,"r":1},"south node":{"name":"south node","lon":86.806626,"lat":0.000000,"spd":-0.041911,"r":1},"chiron":{"name":"chiron","lon":17.572261,"lat":1.075862,"spd":0.042981,"r":0},"pholus":{"name":"pholus","lon":336.731188,"lat":-15.156539,"spd":0.050265,"r":0},"ceres":{"name":"ceres","lon":308.550770,"lat":-5.365279,"spd":0.386702,"r":0},"pallas":{"name":"pallas","lon":289.569620,"lat":28.398472,"spd":0.358521,"r":0},"juno":{"name":"juno","lon":304.984621,"lat":8.062667,"spd":0.396055,"r":0},"vesta":{"name":"vesta","lon":197.431809,"lat":10.398225,"spd":-0.007200,"r":1},"cupido":{"name":"cupido","lon":208.369943,"lat":1.060455,"spd":-0.008330,"r":1},"chariklo":{"name":"chariklo","lon":4.202544,"lat":20.675991,"spd":0.056467,"r":0},"chaos":{"name":"chaos","lon":24.103632,"lat":-4.986552,"spd":0.015641,"r":0},"eris":{"name":"eris","lon":12.620390,"lat":-20.186308,"spd":0.008337,"r":0},"nessus":{"name":"nessus","lon":83.280862,"lat":13.098433,"spd":-0.011314,"r":1}},"houses":[156.240991,180.773240,209.977632,242.916806,276.680605,308.179752,336.240991,0.773240,29.977632,62.916806,96.680605,128.179752],"ascendant":156.240991,"mc":62.916806}, 17 | {"planets":{"sun":{"name":"sun","lon":35.442089,"lat":0.000029,"spd":0.973526,"r":0},"moon":{"name":"moon","lon":353.294510,"lat":4.012451,"spd":11.877258,"r":0},"mercury":{"name":"mercury","lon":55.586041,"lat":2.615789,"spd":1.181069,"r":0},"venus":{"name":"venus","lon":21.142542,"lat":-1.439301,"spd":1.230699,"r":0},"mars":{"name":"mars","lon":108.603737,"lat":1.862012,"spd":0.529565,"r":0},"jupiter":{"name":"jupiter","lon":37.109579,"lat":-0.965454,"spd":0.239407,"r":0},"saturn":{"name":"saturn","lon":116.771040,"lat":0.409800,"spd":0.050679,"r":0},"uranus":{"name":"uranus","lon":215.105574,"lat":0.512017,"spd":-0.042658,"r":1},"neptune":{"name":"neptune","lon":253.544914,"lat":1.562902,"spd":-0.019719,"r":1},"pluto":{"name":"pluto","lon":189.660445,"lat":17.432513,"spd":-0.024474,"r":1},"north node":{"name":"north node","lon":222.554240,"lat":0.000000,"spd":-0.016781,"r":1},"south node":{"name":"south node","lon":42.554240,"lat":0.000000,"spd":-0.016781,"r":1},"chiron":{"name":"chiron","lon":28.137774,"lat":0.155979,"spd":0.060207,"r":0},"pholus":{"name":"pholus","lon":345.437169,"lat":-17.230007,"spd":0.039561,"r":0},"ceres":{"name":"ceres","lon":84.405993,"lat":3.140844,"spd":0.380391,"r":0},"pallas":{"name":"pallas","lon":37.364146,"lat":-18.797203,"spd":0.477827,"r":0},"juno":{"name":"juno","lon":153.874507,"lat":0.578755,"spd":0.036195,"r":0},"vesta":{"name":"vesta","lon":45.474994,"lat":-4.195343,"spd":0.441170,"r":0},"cupido":{"name":"cupido","lon":210.073457,"lat":1.085218,"spd":-0.020578,"r":1},"chariklo":{"name":"chariklo","lon":16.979395,"lat":21.636602,"spd":0.064126,"r":0},"chaos":{"name":"chaos","lon":27.979222,"lat":-4.436924,"spd":0.024757,"r":0},"eris":{"name":"eris","lon":13.809362,"lat":-19.749967,"spd":0.010550,"r":0},"nessus":{"name":"nessus","lon":90.255766,"lat":13.471750,"spd":0.034701,"r":0}},"houses":[76.568871,99.082115,120.919979,145.939862,177.804532,217.261090,256.568871,279.082115,300.919979,325.939862,357.804532,37.261090],"ascendant":76.568871,"mc":325.939862} 18 | ]; 19 | morgan = await Person.create("Morgan", "1974-02-17T23:30Z", {lat: 37.4381927, lng: -79.18932}); 20 | nicole = await Person.create("Nicole", "1976-04-25T13:02Z", {lat: 35.2033533, lng: -80.9796095}); 21 | c = await ChartFactory.create("Morgan transits", morgan, null, ChartType.Transits); 22 | })); 23 | 24 | it("can find the midpoint of two longitudes", () => { 25 | expect(c.getLonMidpoint( 10, 20)).toBe( 15); 26 | expect(c.getLonMidpoint( 0, 180)).toBe( 90); 27 | expect(c.getLonMidpoint(350, 10)).toBe( 0); 28 | expect(c.getLonMidpoint(350, 20)).toBe( 5); 29 | expect(c.getLonMidpoint(340, 10)).toBe(355); 30 | expect(c.getLonMidpoint( 10, 10)).toBe( 10); 31 | }); 32 | 33 | it("has a name", () => { 34 | expect(c.name).toBe("Morgan transits"); 35 | }); 36 | 37 | it("has a type", () => { 38 | expect(c.type).toBe(ChartType.Transits); 39 | }); 40 | 41 | it("can take chart data and return an array of Planet objects", () => { 42 | let planets = c.getPlanets(cdata[0]); 43 | expect(planets.length).toBe(23); 44 | expect(planets[0] instanceof Planet).toBe(true); 45 | }); 46 | 47 | it("should allow transits to be refreshed/reset", testAsync(async () => { 48 | let old_aspects = c._aspects, 49 | nextweek = new Date(); 50 | nextweek.setDate(nextweek.getDate() + 7); 51 | await c.refreshTransits(nextweek.toISOString()); 52 | expect(c._aspects).not.toEqual(old_aspects); 53 | })); 54 | 55 | it("should allow transits to be refreshed/reset defaulting to Date.now() if date unspecified", testAsync(async () => { 56 | try { 57 | let old_aspects = c._aspects; 58 | await c.refreshTransits(); 59 | expect(c._aspects).not.toEqual(old_aspects); 60 | } catch (err) { 61 | console.log(); 62 | } 63 | })); 64 | 65 | it("should throw an error if you try to refresh 'transits' on synastry", testAsync( async () => { 66 | c = await ChartFactory.create("Morgan transits", morgan, nicole, ChartType.Synastry); 67 | try { 68 | c.refreshTransits(); 69 | } catch (err) { 70 | expect(err.message).toBe("You cannot refresh transits on a synastry chart"); 71 | } 72 | })); 73 | 74 | it("should allow access to the array of houses", () => { 75 | expect(c.houses.length).toBe(12); 76 | }); 77 | 78 | it("should allow access to the aspects array", () => { 79 | expect(c.aspects.length).toBeGreaterThan(0); 80 | }); 81 | 82 | it("should allow access to the ascendant", () => { 83 | expect(c.ascendant).toBeTruthy(); 84 | }); 85 | 86 | it("should allow access to the array of inner planets", () => { 87 | expect(c.innerPlanets.length).toBe(23); 88 | }); 89 | 90 | it("should allow access to the array of outer planets", () => { 91 | expect(c.outerPlanets.length).toBe(23); 92 | }); 93 | 94 | it("returns an empty array if there are no inner planets", testAsync( async () => { 95 | c = await ChartFactory.create("Morgan transits", morgan); 96 | expect(c.innerPlanets.length).toBe(0); 97 | })); 98 | 99 | it("returns outer planets even if there are no inner planets", testAsync( async () => { 100 | c = await ChartFactory.create("Morgan transits", morgan); 101 | expect(c.outerPlanets.length).toBe(23); 102 | })); 103 | 104 | }); -------------------------------------------------------------------------------- /src/chart.ts: -------------------------------------------------------------------------------- 1 | import { Person, Point } from "./person"; 2 | import { Planet } from "./planet"; 3 | import { Aspect } from "./aspect"; 4 | import { default as rp } from "./rp"; 5 | 6 | export enum ChartType { 7 | Basic, 8 | Transits, 9 | Synastry, 10 | Combined, 11 | Davison, 12 | CombinedTransits, 13 | DavisonTransits 14 | } 15 | 16 | export interface PlanetData { 17 | name: string; 18 | lon: number; 19 | lat: number; 20 | spd: number; 21 | r: number; 22 | } 23 | 24 | export interface PlanetDataArray { 25 | [name: string]: PlanetData; 26 | } 27 | 28 | export interface ChartData { 29 | planets: PlanetDataArray; 30 | houses: Array; 31 | ascendant: number; 32 | mc: number; 33 | } 34 | 35 | export interface ChartDataArray { 36 | [index: number]: ChartData; 37 | } 38 | 39 | export class Chart { 40 | 41 | _planets1: Array; 42 | _planets2: Array; 43 | _aspects: Array; 44 | _ascendant: number; 45 | _houses: Array; 46 | _debug: boolean = false; 47 | 48 | _signs = [ 49 | {name: "aries", symbol: "q", v: 1}, 50 | {name: "taurus", symbol: "w", v: 1}, 51 | {name: "gemini", symbol: "e", v: 1}, 52 | {name: "cancer", symbol: "r", v: 1}, 53 | {name: "leo", symbol: "t", v: 1}, 54 | {name: "virgo", symbol: "z", v: 1}, 55 | {name: "libra", symbol: "u", v: 1}, 56 | {name: "scorpio", symbol: "i", v: 1}, 57 | {name: "sagittarius", symbol: "o", v: 1}, 58 | {name: "capricorn", symbol: "p", v: 1}, 59 | {name: "aquarius", symbol: "ü", v: 1}, 60 | {name: "pisces", symbol: "+", v: 1} 61 | ]; 62 | 63 | constructor(public name: string, public p1: Person, cdata: ChartDataArray, public p2?: Person, public type: ChartType = ChartType.Basic) { 64 | let pdata: ChartData; 65 | switch (type) { 66 | case ChartType.Combined: 67 | pdata = this.calculateCombinedPlanets(cdata); 68 | this._planets1 = this.getPlanets(pdata); 69 | this._ascendant = pdata.ascendant; 70 | this._houses = pdata.houses; 71 | break; 72 | case ChartType.CombinedTransits: 73 | pdata = this.calculateCombinedPlanets(cdata); 74 | this._planets1 = this.getPlanets(pdata); 75 | this._planets2 = this.getPlanets(cdata[2]); 76 | this._ascendant = pdata.ascendant; 77 | this._houses = pdata.houses; 78 | break; 79 | default: 80 | this._planets1 = this.getPlanets(cdata[0]); 81 | if (cdata[1]) { 82 | this._planets2 = this.getPlanets(cdata[1]); 83 | } 84 | this._ascendant = cdata[0].ascendant; 85 | this._houses = cdata[0].houses; 86 | break; 87 | } 88 | this.calculateAspects(); 89 | } 90 | 91 | /** 92 | * Extracts planet data from ChartData and creates Planet objects for each one 93 | * @param {ChartData} cdata JSON data returned from morphemeris REST API 94 | * @return {Array} An array of Planet objects 95 | */ 96 | getPlanets(cdata: ChartData): Array { 97 | let planets: Array = []; 98 | for (let p in cdata.planets) { 99 | let pd = cdata.planets[p]; 100 | planets.push(new Planet(pd.name, pd.lon, pd.lat, pd.spd)); 101 | } 102 | return planets; 103 | } 104 | 105 | /** 106 | * Calculates the aspects between planets in the chart 107 | */ 108 | calculateAspects(): void { 109 | this._aspects = []; 110 | if (!this._planets2) { 111 | // calculate aspects within the _planets1 array 112 | for (let i in this._planets1) { 113 | for (let j in this._planets1) { 114 | if (i !== j && j > i) { 115 | try { 116 | this._aspects.push(new Aspect(this._planets1[i], this._planets1[j])); 117 | } catch (err) { 118 | if (this._debug) console.error(err); 119 | } 120 | } 121 | } 122 | } 123 | } 124 | else { 125 | // calculate aspects between the _planets1 and _planets2 arrays 126 | for (let i in this._planets1) { 127 | for (let j in this._planets2) { 128 | try { 129 | this._aspects.push(new Aspect(this._planets1[i], this._planets2[j])); 130 | } catch (err) { 131 | if (this._debug) console.error(err); 132 | } 133 | } 134 | } 135 | } 136 | } 137 | 138 | /** 139 | * Calculates longitudes for a combined chart 140 | * @param {ChartData} p1 Planet data from person one 141 | * @param {ChartData} p2 Planet data from person two 142 | */ 143 | calculateCombinedPlanets(cdata: ChartDataArray): ChartData { 144 | let cd: ChartData = {"planets":{"sun":{"name":null,"lon":null,"lat":null,"spd":null,"r":null},"moon":{"name":null,"lon":null,"lat":null,"spd":null,"r":null},"mercury":{"name":null,"lon":null,"lat":null,"spd":null,"r":null},"venus":{"name":null,"lon":null,"lat":null,"spd":null,"r":null},"mars":{"name":null,"lon":null,"lat":null,"spd":null,"r":null},"jupiter":{"name":null,"lon":null,"lat":null,"spd":null,"r":null},"saturn":{"name":null,"lon":null,"lat":null,"spd":null,"r":null},"uranus":{"name":null,"lon":null,"lat":null,"spd":null,"r":null},"neptune":{"name":null,"lon":null,"lat":null,"spd":null,"r":null},"pluto":{"name":null,"lon":null,"lat":null,"spd":null,"r":null},"north node":{"name":"north node","lon":null,"lat":null,"spd":null,"r":null},"south node":{"name":"south node","lon":null,"lat":null,"spd":null,"r":null},"chiron":{"name":null,"lon":null,"lat":null,"spd":null,"r":null},"pholus":{"name":null,"lon":null,"lat":null,"spd":null,"r":null},"ceres":{"name":null,"lon":null,"lat":null,"spd":null,"r":null},"pallas":{"name":null,"lon":null,"lat":null,"spd":null,"r":null},"juno":{"name":null,"lon":null,"lat":null,"spd":null,"r":null},"vesta":{"name":null,"lon":null,"lat":null,"spd":null,"r":null},"cupido":{"name":null,"lon":null,"lat":null,"spd":null,"r":null},"chariklo":{"name":null,"lon":null,"lat":null,"spd":null,"r":null},"chaos":{"name":null,"lon":null,"lat":null,"spd":null,"r":null},"eris":{"name":null,"lon":null,"lat":null,"spd":null,"r":null},"nessus":{"name":null,"lon":null,"lat":null,"spd":null,"r":null}},"houses":[null,null,null,null,null,null,null,null,null,null,null,null], "ascendant": null, "mc": null}; 145 | for (let p in cdata[0].planets) { 146 | cd.planets[p].name = p; 147 | cd.planets[p].lon = this.getLonMidpoint(cdata[0].planets[p].lon, cdata[1].planets[p].lon); 148 | cd.planets[p].lat = (cdata[0].planets[p].lat + cdata[1].planets[p].lat) / 2; 149 | cd.planets[p].spd = (cdata[0].planets[p].spd + cdata[1].planets[p].spd) / 2; 150 | } 151 | for (let h in cdata[0].houses) { 152 | cd.houses[h] = this.getLonMidpoint(cdata[0].houses[h], cdata[1].houses[h]); 153 | } 154 | cd.ascendant = this.getLonMidpoint(cdata[0].ascendant, cdata[1].ascendant); 155 | cd.mc = this.getLonMidpoint(cdata[0].mc, cdata[1].mc); 156 | return cd; 157 | } 158 | 159 | /** 160 | * Finds the midpoint between two planets on the "short" side 161 | * @param {number} l1 Longitude of planet one 162 | * @param {number} l2 Longitude of planet two 163 | * @return {number} Longitude of the midpoint 164 | */ 165 | getLonMidpoint(l1: number, l2: number): number { 166 | let mp: number, high: number, low: number; 167 | 168 | // if they are exactly the same, return either one 169 | if (l1 === l2) { 170 | return l1; 171 | } 172 | 173 | // figure out which has a higher/lower longitude 174 | high = l1 > l2 ? l1 : l2; 175 | low = l1 < l2 ? l1 : l2; 176 | 177 | if (high - low <= 180) { 178 | mp = (high + low) / 2; 179 | } 180 | else { 181 | mp = ((((low + 360) - high) / 2) + high) % 360; 182 | } 183 | 184 | return mp; 185 | } 186 | 187 | /** 188 | * Gets chart data from the online ephemeris 189 | * @param {string} date A UTC datetime string in ISO 8601 format 190 | * @param {Point} p An object with numeric lat and lng properties 191 | * @return {Promise} A JSON object with the data needed to implement a chart 192 | */ 193 | static async getChartData(date: string, p: Point): Promise { 194 | return await rp({ 195 | uri: "http://www.morphemeris.com/ephemeris.php", 196 | qs: { 197 | date: date, 198 | lat: p.lat, 199 | lon: p.lng 200 | } 201 | }).then((cdata: ChartData) => cdata); 202 | } 203 | 204 | /** 205 | * Refresh or set the transits to a new time 206 | * @param {string} date (Optional) Target datetime for transits in ISO 8601 format; defaults to now() 207 | */ 208 | async refreshTransits(date: string = null) { 209 | if (ChartType.Synastry === this.type) { 210 | throw new Error("You cannot refresh transits on a synastry chart"); 211 | } 212 | if (null === date) { 213 | date = new Date().toISOString(); 214 | } 215 | let cdata = await Chart.getChartData(date, this.p1.location); 216 | this._planets2 = this.getPlanets(cdata); 217 | this.calculateAspects(); 218 | } 219 | 220 | get houses(): Array { return this._houses; } 221 | get aspects(): Array { return this._aspects; } 222 | get ascendant(): number { return this._ascendant; } 223 | get innerPlanets(): Array { return this._planets2 ? this._planets1 : []; } 224 | get outerPlanets(): Array { return this._planets2 ? this._planets2 : this._planets1; } 225 | } 226 | -------------------------------------------------------------------------------- /src/person.spec.ts: -------------------------------------------------------------------------------- 1 | import { Person } from "./person"; 2 | 3 | describe("A Person", () => { 4 | 5 | let person: Person, 6 | testAsync = (runAsync: any) => { return (done: any) => { runAsync().then(done, (e: any) => { fail(e); done(); }); }; }; 7 | 8 | it("can be instantiated with lat/lon", testAsync( async () => { 9 | person = await Person.create("Morgan", "1974-02-17T23:30Z", {lat: 37.4381927, lng: -79.18932}); 10 | expect(person).toBeDefined(); 11 | expect(person instanceof Person).toBe(true); 12 | expect(person.name).toBe("Morgan"); 13 | })); 14 | 15 | it("can be instantiated with an address", testAsync( async () => { 16 | person = await Person.create("Morgan", "1974-02-17T23:30Z", "Virginia Baptist Hospital, Rivermont Avenue, Lynchburg, VA"); 17 | expect(person).toBeDefined(); 18 | expect(person instanceof Person).toBe(true); 19 | expect(person.name).toBe("Morgan"); 20 | })); 21 | 22 | it("can be instantiated with a Date object", testAsync( async () => { 23 | let d = new Date(); 24 | person = await Person.create("Morgan", d, "Virginia Baptist Hospital, Rivermont Avenue, Lynchburg, VA"); 25 | expect(person).toBeDefined(); 26 | expect(person instanceof Person).toBe(true); 27 | expect(person.date).toBe(d.toISOString()); 28 | })); 29 | 30 | it("can be instantiated with no date object", testAsync( async () => { 31 | let d = new Date(); 32 | person = await Person.create("Morgan", null, "Virginia Baptist Hospital, Rivermont Avenue, Lynchburg, VA"); 33 | expect(person).toBeDefined(); 34 | expect(person instanceof Person).toBe(true); 35 | let pd = new Date(person.date); 36 | expect(pd.getTime() / 10000).toBeCloseTo(d.getTime() / 10000, 1); 37 | })); 38 | 39 | it("throws an error if the person/event was instantiated without a name", testAsync( async () => { 40 | try { 41 | person = await Person.create("", "1974-02-17T23:30Z", { lat: 37.4381927, lng: -79.18932}); 42 | } catch (err) { 43 | expect(err.message).toBe("No name was submitted for the person"); 44 | } 45 | })); 46 | 47 | it("throws an error if the date is not formatted correctly", testAsync( async () => { 48 | try { 49 | person = await Person.create("Morgan Benton", "1974-02-17T23:30", { lat: 37.4381927, lng: -79.18932}); 50 | } catch (err) { 51 | expect(err.message).toBe("Date not formatted according to ISO 8601 (YYYY-MM-DDTHH:mmZ)"); 52 | } 53 | })); 54 | 55 | it("throws an error if the latitude is out of range", testAsync( async () => { 56 | try { 57 | person = await Person.create("Morgan Benton", "1974-02-17T23:30Z", { lat: 137.4381927, lng: -79.18932}); 58 | } catch (err) { 59 | expect(err.message).toBe("Latitude must be between -90 and 90"); 60 | } 61 | })); 62 | 63 | it("throws an error if the longitude is out of range", testAsync( async () => { 64 | try { 65 | person = await Person.create("Morgan Benton", "1974-02-17T23:30Z", { lat: 37.4381927, lng: -279.18932}); 66 | } catch (err) { 67 | expect(err.message).toBe("Longitude must be between -180 and 180"); 68 | } 69 | })); 70 | 71 | it("can get a lat/lon from an address", testAsync( async () => { 72 | let point = await Person.getLatLon("1990 Buttonwood Ct, Harrisonburg, VA 22802"); 73 | expect(point.lat).toBe(38.48500900000001); 74 | expect(point.lng).toBe(-78.872845); 75 | })); 76 | 77 | it("can get a timezone from a lat/lon", testAsync( async () => { 78 | person = await Person.create("Morgan", "1974-02-17T23:30Z", {lat: 37.4381927, lng: -79.18932}); 79 | let tz = await Person.getTimezone(person.location); 80 | expect(tz).toBe("America/New_York"); 81 | })); 82 | 83 | it("throws an error getting lat/lon if address is unspecified", testAsync( async () => { 84 | try { 85 | let point = await Person.getLatLon(""); 86 | } catch (err) { 87 | // don't know why the error message from the Promise is not passed along... 88 | // expect(err.message).toBe("Invalid request. Invalid 'location' parameter."); 89 | expect(err.message).toBe(""); 90 | } 91 | })); 92 | 93 | it("throws an error getting timezone if lat is out of bounds", testAsync( async () => { 94 | try { 95 | let tz = await Person.getTimezone({lat: 137.4381927, lng: -79.18932}); 96 | } catch (err) { 97 | // don't know why the error message from the Promise is not passed along... 98 | // expect(err.message).toBe("Invalid request. Invalid 'location' parameter."); 99 | expect(err.message).toBe(""); 100 | } 101 | })); 102 | 103 | }); -------------------------------------------------------------------------------- /src/person.ts: -------------------------------------------------------------------------------- 1 | import { default as rp } from "./rp"; 2 | 3 | interface GoogleAddressComponent { 4 | long_name: string; 5 | short_name: string; 6 | types: Array; 7 | } 8 | 9 | export interface GoogleLocation { 10 | lat: number; 11 | lng: number; 12 | } 13 | 14 | export type Point = GoogleLocation; 15 | 16 | interface GoogleViewport { 17 | northeast: GoogleLocation; 18 | southwest: GoogleLocation; 19 | } 20 | 21 | interface GoogleGeocode { 22 | address_components: Array; 23 | formatted_address: string; 24 | geometry: { 25 | location: GoogleLocation; 26 | location_type: string; 27 | viewport: GoogleViewport; 28 | bounds?: GoogleViewport 29 | }; 30 | place_id: string; 31 | types: Array; 32 | partial_match?: boolean; 33 | postcode_localities?: Array; 34 | } 35 | 36 | interface GoogleGeocodeResult { 37 | results: Array; 38 | status: string; 39 | error_message?: string; 40 | } 41 | 42 | interface GoogleTimezoneResult { 43 | status: string; 44 | dstOffset?: number; 45 | rawOffset?: number; 46 | timeZoneId?: string; 47 | timeZoneName?: string; 48 | errorMessage?: string; 49 | } 50 | 51 | 52 | /** 53 | * Represents a person or event for whom a chart will be created 54 | */ 55 | export class Person { 56 | 57 | /** 58 | * Google API key 59 | * @type {string} 60 | */ 61 | private static _key: string = "AIzaSyAXnIdQxap1WQuzG0XxHfYlCA5O9GQyvuY"; 62 | 63 | /** 64 | * Creates a Person object 65 | * @param {string} public name Name of the person or event 66 | * @param {string} public date UTC date in ISO 8601 format, i.e. YYYY-MM-DDTHH:mmZ (caller must convert to UTC) 67 | * @param {Point} location The [lat: number, lon: number] of the event or person's birthplace 68 | */ 69 | constructor(public name: string, public date: string, public location: Point) {} 70 | 71 | /** 72 | * Asynchronous factory function for creating people or events 73 | * @param {string} name Name of the person or event 74 | * @param {Date | string} date Exact datetime for the chart, preferably UTC date in ISO 8601 format, i.e. YYYY-MM-DDTHH:mmZ (caller must convert to UTC) 75 | * @param {Point | string} location Either an address or a lat/lng combination 76 | * @return {Promise} The Person object that was created 77 | */ 78 | static async create(name: string, date: Date | string, location: Point | string): Promise { 79 | 80 | let dt: string, 81 | loc: Point; 82 | 83 | // make sure a name was submitted 84 | if (!name) { 85 | throw new Error("No name was submitted for the person"); 86 | } 87 | 88 | // deal with the type of date submitted 89 | if (typeof date === "string") { 90 | if (!/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}\.\d{3})?Z/.test(date)) { 91 | throw new TypeError("Date not formatted according to ISO 8601 (YYYY-MM-DDTHH:mmZ)"); 92 | } 93 | dt = date; 94 | } 95 | else if (date instanceof Date) { 96 | dt = date.toISOString(); 97 | } 98 | else { 99 | // defaults to "now" 100 | dt = new Date().toISOString(); 101 | } 102 | 103 | // deal with the type of location submitted 104 | if (typeof location === "string") { 105 | loc = await this.getLatLon(location); 106 | } else { 107 | // make sure latitude was valid 108 | if (location.lat < -90 || location.lat > 90) { 109 | throw new RangeError("Latitude must be between -90 and 90"); 110 | } 111 | // make sure longitude was valid 112 | if (location.lng < -180 || location.lng > 180) { 113 | throw new RangeError("Longitude must be between -180 and 180"); 114 | } 115 | loc = location; 116 | } 117 | 118 | return new Person(name, dt, loc); 119 | } 120 | 121 | /** 122 | * Gets a timezone given a latitude and longitude 123 | * @param {Point} p Contains the latitude and longitude in decimal format 124 | */ 125 | static async getTimezone(p: Point): Promise { 126 | return await rp({ 127 | uri: "https://maps.googleapis.com/maps/api/timezone/json", 128 | qs: { 129 | key: this._key, 130 | location: `${p.lat},${p.lng}`, 131 | timestamp: Math.floor(Date.now() / 1000) 132 | } 133 | }).then( 134 | (tzinfo: GoogleTimezoneResult): string => tzinfo.timeZoneId, 135 | (error: GoogleTimezoneResult): any => { throw Error(error.errorMessage); } 136 | ); 137 | } 138 | 139 | /** 140 | * Get a latitude and longitude given an address 141 | * @param {string} address The address of the desired lat/lon 142 | */ 143 | static async getLatLon(address: string): Promise { 144 | return await rp({ 145 | uri: "https://maps.googleapis.com/maps/api/geocode/json", 146 | qs: { 147 | key: this._key, 148 | address: address 149 | } 150 | }).then( 151 | (latlng: GoogleGeocodeResult): Point => latlng.results[0].geometry.location, 152 | (error: GoogleGeocodeResult): Point => { throw Error(error.error_message); } 153 | ); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/planet.spec.ts: -------------------------------------------------------------------------------- 1 | import { Planet } from "./planet"; 2 | 3 | describe("A Planet", () => { 4 | 5 | let p: Planet; 6 | 7 | beforeEach(() => p = new Planet("Sun", 75.853439, -0.000140, 0.957389)); 8 | 9 | // check for existence of basic properties 10 | it("has a name", () => { expect(p.name ).toBeDefined(); }); 11 | it("has a longitude", () => { expect(p.longitude).toBeDefined(); }); 12 | it("has a latitude", () => { expect(p.latitude ).toBeDefined(); }); 13 | it("has a speed", () => { expect(p.speed ).toBeDefined(); }); 14 | 15 | it("can be retrograde", () => { 16 | // check for return type 17 | expect(p.isRetrograde()).toEqual(jasmine.any(Boolean)); 18 | // check for return value false if direct 19 | expect(p.isRetrograde()).toBe(false); 20 | // and true if retrograde 21 | p.speed = -p.speed; 22 | expect(p.isRetrograde()).toBe(true); 23 | }); 24 | 25 | it("has a symbol", () => { 26 | expect(p.symbol).toBe("a"); 27 | }); 28 | 29 | it("can be considered 'major' or 'minor'", () => { 30 | expect(p.isMajor()).toBe(true); 31 | }); 32 | 33 | }); -------------------------------------------------------------------------------- /src/planet.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * One of the planets, asteroids, the sun or moon 3 | */ 4 | export class Planet { 5 | 6 | /** 7 | * The planet name, e.g. Mercury 8 | * @type {string} 9 | */ 10 | name: string; 11 | 12 | /** 13 | * A planet's longitude identifies what sign 14 | * it is in 15 | * @type {number} 16 | */ 17 | longitude: number; 18 | 19 | /** 20 | * A planet's latitude describes it's distance 21 | * from the ecliptic, and can be used to 22 | * determine if it is out of bounds 23 | * @type {number} 24 | */ 25 | latitude: number; 26 | 27 | /** 28 | * A planet's speed allows us to know if it is 29 | * retrograde, and to calculate whether an 30 | * aspect is applying or separating 31 | * @type {number} 32 | */ 33 | speed: number; 34 | 35 | /** 36 | * The symbol for this planet as represented in 37 | * the Kairon Semiserif font 38 | * @type {string} 39 | */ 40 | symbol: string; 41 | 42 | /** 43 | * Dictionary of symbols for the planets for 44 | * use with the Kairon Semiserif font 45 | * @type {Object} 46 | */ 47 | private symbols: {[planet: string]: string} = { 48 | "sun": "a", 49 | "moon": "s", 50 | "mercury": "d", 51 | "venus": "f", 52 | "earth": "g", 53 | "mars": "h", 54 | "jupiter": "j", 55 | "saturn": "k", 56 | "uranus": "ö", 57 | "neptune": "ä", 58 | "pluto": "#", 59 | "south node": "?", 60 | "north node": "ß", 61 | "ceres": "A", 62 | "pallas": "S", 63 | "juno": "D", 64 | "vesta": "F", 65 | "lilith": "ç", 66 | "cupido": "L", 67 | "chiron": "l", 68 | "nessus": "ò", 69 | "pholus": "ñ", 70 | "chariklo": "î", 71 | "eris": "È", 72 | "chaos": "Ê", 73 | "fortuna": "%" 74 | }; 75 | 76 | /** 77 | * Instantiate a new planet object. 78 | * @param {string} name The planet's name 79 | * @param {number} lon The planet's longitude 80 | * @param {number} lat The planet's latitude 81 | * @param {number} spd The planet's speed relative to earth 82 | */ 83 | constructor(name: string, lon: number, lat: number, spd: number) { 84 | this.name = name; 85 | this.longitude = lon; 86 | this.latitude = lat; 87 | this.speed = spd; 88 | this.symbol = this.symbols[name.toLowerCase()]; 89 | } 90 | 91 | /** 92 | * A planet is retrograde when it's speed relative 93 | * to earth is less than zero 94 | * @return {boolean} Whether or not the planet is retrograde 95 | */ 96 | isRetrograde(): boolean { 97 | return this.speed < 0; 98 | } 99 | 100 | /** 101 | * Is this one of the major planets typically included in a chart? 102 | * @return {boolean} Returns true if it is a major planet 103 | */ 104 | isMajor(): boolean { 105 | return ["sun", "moon", "mercury", "venus", "mars", "jupiter", "saturn", 106 | "uranus", "neptune", "pluto", "north node", "south node"] 107 | .indexOf(this.name.toLowerCase()) > -1; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/rp.spec.ts: -------------------------------------------------------------------------------- 1 | import { default as rp } from "./rp"; 2 | 3 | describe("A simple request-promise implementation", () => { 4 | 5 | let https_request = { 6 | uri: "https://maps.googleapis.com/maps/api/timezone/json", 7 | qs: { 8 | key: "AIzaSyAXnIdQxap1WQuzG0XxHfYlCA5O9GQyvuY", 9 | location: `38.4496,-78.8689`, 10 | timestamp: Math.floor(Date.now() / 1000).toString() 11 | } 12 | }, 13 | http_request = { 14 | uri: "http://www.morphemeris.com/ephemeris.php", 15 | qs: { 16 | date: "1974-02-17T23:30Z", 17 | lat: 37.4381927, 18 | lng: -79.18932 19 | } 20 | }, 21 | testAsync = (runAsync: any) => { return (done: any) => { runAsync().then(done, (e: any) => { fail(e); done(); }); }; }; 22 | 23 | it("can make a basic http request", testAsync(async () => { 24 | let json = await rp(http_request), 25 | expected = {"planets":{"sun":{"name":"sun","lon":328.928849,"lat":-0.000030,"spd":1.009225,"r":0},"moon":{"name":"moon","lon":282.376739,"lat":1.370274,"spd":11.765058,"r":0},"mercury":{"name":"mercury","lon":341.436442,"lat":2.930657,"spd":-0.369939,"r":1},"venus":{"name":"venus","lon":296.234418,"lat":6.898336,"spd":0.180950,"r":0},"mars":{"name":"mars","lon":54.859512,"lat":1.560358,"spd":0.535861,"r":0},"jupiter":{"name":"jupiter","lon":325.604490,"lat":-0.768867,"spd":0.239856,"r":0},"saturn":{"name":"saturn","lon":87.876187,"lat":-0.907421,"spd":-0.018641,"r":1},"uranus":{"name":"uranus","lon":207.650784,"lat":0.599481,"spd":-0.015184,"r":1},"neptune":{"name":"neptune","lon":249.481255,"lat":1.582277,"spd":0.012389,"r":0},"pluto":{"name":"pluto","lon":186.394398,"lat":17.038291,"spd":-0.020345,"r":1},"north node":{"name":"north node","lon":266.806626,"lat":0.000000,"spd":-0.041911,"r":1},"south node":{"name":"south node","lon":86.806626,"lat":0.000000,"spd":-0.041911,"r":1},"chiron":{"name":"chiron","lon":17.572261,"lat":1.075862,"spd":0.042981,"r":0},"pholus":{"name":"pholus","lon":336.731188,"lat":-15.156539,"spd":0.050265,"r":0},"ceres":{"name":"ceres","lon":308.550770,"lat":-5.365279,"spd":0.386702,"r":0},"pallas":{"name":"pallas","lon":289.569620,"lat":28.398472,"spd":0.358521,"r":0},"juno":{"name":"juno","lon":304.984621,"lat":8.062667,"spd":0.396055,"r":0},"vesta":{"name":"vesta","lon":197.431809,"lat":10.398225,"spd":-0.007200,"r":1},"cupido":{"name":"cupido","lon":208.369943,"lat":1.060455,"spd":-0.008330,"r":1},"chariklo":{"name":"chariklo","lon":4.202544,"lat":20.675991,"spd":0.056467,"r":0},"chaos":{"name":"chaos","lon":24.103632,"lat":-4.986552,"spd":0.015641,"r":0},"eris":{"name":"eris","lon":12.620390,"lat":-20.186308,"spd":0.008337,"r":0},"nessus":{"name":"nessus","lon":83.280862,"lat":13.098433,"spd":-0.011314,"r":1}},"houses":[156.242746,180.775172,209.979694,242.918870,276.682568,308.181605,336.242746,0.775172,29.979694,62.918870,96.682568,128.181605],"ascendant":156.242746,"mc":62.918870}; 26 | expect(json).toEqual(expected); 27 | })); 28 | 29 | it("can make a basic https request", testAsync(async () => { 30 | let json = await rp(https_request), 31 | expected = { dstOffset : 0, rawOffset : -18000, status : "OK", timeZoneId : "America/New_York", timeZoneName : "Eastern Standard Time" }; 32 | expect(json).toEqual(expected); 33 | })); 34 | 35 | // TODO: write tests for failing cases 36 | 37 | }); -------------------------------------------------------------------------------- /src/rp.ts: -------------------------------------------------------------------------------- 1 | import * as http from "http"; 2 | import * as https from "https"; 3 | 4 | export interface RequestPromiseOptions { 5 | uri: string; 6 | qs: {[name: string]: string|number|boolean}; 7 | } 8 | 9 | const uri = (options: RequestPromiseOptions): string => { 10 | let url: string = options.uri, 11 | qs: string = Object.keys(options.qs).map(key => { 12 | return `${encodeURIComponent(key)}=${encodeURIComponent(options.qs[key].toString())}`; 13 | }).join("&"); 14 | return `${url}?${qs}`; 15 | }; 16 | 17 | const rp = (options: RequestPromiseOptions): Promise => { 18 | return new Promise((resolve, reject) => { 19 | const http = require("http"); 20 | const lib = options.uri.startsWith("https") ? https : http; 21 | const url = uri(options); 22 | const req = lib.get(url, (response: http.ServerResponse) => { 23 | if (response.statusCode < 200 || response.statusCode > 299) { 24 | reject(new Error("HTTP Error: " + response.statusCode)); 25 | } 26 | const body: string[] = []; 27 | response.on("data", (chunk: string) => body.push(chunk)); 28 | response.on("end", () => resolve(JSON.parse(body.join("")))); 29 | }); 30 | req.on("error", (err: Error) => reject(err)); 31 | }); 32 | }; 33 | 34 | export default rp; -------------------------------------------------------------------------------- /tsconfig.def.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "amd", 4 | "target": "es6", 5 | "noImplicitAny": true, 6 | "declaration": true, 7 | "outFile": "src/js/astrologyjs.js", 8 | "moduleResolution": "node" 9 | }, 10 | "include": [ 11 | "src/astrologyjs.ts" 12 | ] 13 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "noImplicitAny": true, 6 | "outDir": "src/js", 7 | "moduleResolution": "node" 8 | }, 9 | "include": [ 10 | "src/astrologyjs.ts" 11 | ] 12 | } -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "noImplicitAny": true, 6 | "outDir": "spec", 7 | "moduleResolution": "node", 8 | "inlineSourceMap": true 9 | }, 10 | "include": [ 11 | "src/astrologyjs.ts", 12 | "src/*.spec.ts" 13 | ] 14 | } --------------------------------------------------------------------------------