rave-engine 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE +33 -0
- package/README.md +354 -0
- package/bin/rave.js +204 -0
- package/package.json +68 -0
- package/src/birth/parseBirth.js +76 -0
- package/src/calc/astronomia.js +163 -0
- package/src/calc/ephemeris.js +61 -0
- package/src/calc/mandala.js +66 -0
- package/src/calc/profile.js +288 -0
- package/src/calc/swisseph.js +87 -0
- package/src/chart.js +84 -0
- package/src/hd/bodygraph.js +270 -0
- package/src/hd/houses.js +229 -0
- package/src/index.js +89 -0
- package/src/timezone/location.js +67 -0
- package/src/timezone/search.js +183 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const { parseBirthToUtc, BirthParseError } = require('./birth/parseBirth');
|
|
2
|
+
const {
|
|
3
|
+
computeEngineTest,
|
|
4
|
+
computeProfileSpheres,
|
|
5
|
+
computeActivations,
|
|
6
|
+
jdUtcFromDate,
|
|
7
|
+
findPreviousSolarLongitude,
|
|
8
|
+
signedAngleDiff,
|
|
9
|
+
} = require('./calc/profile');
|
|
10
|
+
const { mapLongitudeDegrees, normalizeAngleDegrees } = require('./calc/mandala');
|
|
11
|
+
const { ensureEphePath, getBackend } = require('./calc/ephemeris');
|
|
12
|
+
const { searchTimezones, formatOffset } = require('./timezone/search');
|
|
13
|
+
const { locationForTimezone, resolveLocation } = require('./timezone/location');
|
|
14
|
+
const {
|
|
15
|
+
CENTERS,
|
|
16
|
+
CENTER_LABELS,
|
|
17
|
+
GATE_CENTER,
|
|
18
|
+
CHANNELS,
|
|
19
|
+
computeBodygraph,
|
|
20
|
+
} = require('./hd/bodygraph');
|
|
21
|
+
const { computeAngles, computeHouses, signOf, SIGNS } = require('./hd/houses');
|
|
22
|
+
const { computeChart } = require('./chart');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* High-level entry point: take human-friendly birth strings (date/time/timezone)
|
|
26
|
+
* and return the same `engine` payload that the API's /maintenance/engine_test
|
|
27
|
+
* endpoint produces, with the parsed input echoed back for traceability.
|
|
28
|
+
*
|
|
29
|
+
* For the full Gene Keys + Human Design + Astrology output, use `computeChart`.
|
|
30
|
+
*
|
|
31
|
+
* @param {{ birthdate: string, birthtime: string, timezone: string }} input
|
|
32
|
+
*/
|
|
33
|
+
function computeProfile(input) {
|
|
34
|
+
const birthUtc = parseBirthToUtc(input);
|
|
35
|
+
return {
|
|
36
|
+
input: {
|
|
37
|
+
birthdate: input.birthdate,
|
|
38
|
+
birthtime: input.birthtime,
|
|
39
|
+
timezone: input.timezone,
|
|
40
|
+
birth_utc: birthUtc.toISOString(),
|
|
41
|
+
},
|
|
42
|
+
engine: computeEngineTest({ birthUtc }),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = {
|
|
47
|
+
// Top-level convenience
|
|
48
|
+
computeProfile,
|
|
49
|
+
computeChart,
|
|
50
|
+
|
|
51
|
+
// Birth parsing
|
|
52
|
+
parseBirthToUtc,
|
|
53
|
+
BirthParseError,
|
|
54
|
+
|
|
55
|
+
// Engine internals (parity with event-horizon-api)
|
|
56
|
+
computeEngineTest,
|
|
57
|
+
computeProfileSpheres,
|
|
58
|
+
computeActivations,
|
|
59
|
+
jdUtcFromDate,
|
|
60
|
+
findPreviousSolarLongitude,
|
|
61
|
+
signedAngleDiff,
|
|
62
|
+
|
|
63
|
+
// Mandala helpers
|
|
64
|
+
mapLongitudeDegrees,
|
|
65
|
+
normalizeAngleDegrees,
|
|
66
|
+
|
|
67
|
+
// Human Design bodygraph
|
|
68
|
+
computeBodygraph,
|
|
69
|
+
CENTERS,
|
|
70
|
+
CENTER_LABELS,
|
|
71
|
+
GATE_CENTER,
|
|
72
|
+
CHANNELS,
|
|
73
|
+
|
|
74
|
+
// Extended astrology
|
|
75
|
+
computeAngles,
|
|
76
|
+
computeHouses,
|
|
77
|
+
signOf,
|
|
78
|
+
SIGNS,
|
|
79
|
+
|
|
80
|
+
// Ephemeris backend (mostly internal; exposed for power users)
|
|
81
|
+
getBackend,
|
|
82
|
+
ensureEphePath,
|
|
83
|
+
|
|
84
|
+
// Timezone autocomplete + location
|
|
85
|
+
searchTimezones,
|
|
86
|
+
formatOffset,
|
|
87
|
+
locationForTimezone,
|
|
88
|
+
resolveLocation,
|
|
89
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Resolve a representative geographic location (lat/lng) for an IANA timezone.
|
|
2
|
+
//
|
|
3
|
+
// The extended-astrology section (Ascendant / MC / houses) needs a birth
|
|
4
|
+
// LOCATION, not just a timezone. To let a user "just pick the timezone they were
|
|
5
|
+
// born in" and still get angles, we derive a representative location from the
|
|
6
|
+
// most-populous city in that timezone (data from `city-timezones`). An explicit
|
|
7
|
+
// { lat, lng } always overrides this.
|
|
8
|
+
|
|
9
|
+
const cityTimezones = require('city-timezones');
|
|
10
|
+
|
|
11
|
+
let _byTz = null;
|
|
12
|
+
|
|
13
|
+
// Index cityMapping by IANA timezone → most-populous city, built once.
|
|
14
|
+
function buildIndex() {
|
|
15
|
+
if (_byTz) return _byTz;
|
|
16
|
+
const byTz = new Map();
|
|
17
|
+
for (const c of cityTimezones.cityMapping) {
|
|
18
|
+
if (!c || !c.timezone || !Number.isFinite(c.lat) || !Number.isFinite(c.lng)) continue;
|
|
19
|
+
const existing = byTz.get(c.timezone);
|
|
20
|
+
if (!existing || (c.pop || 0) > (existing.pop || 0)) {
|
|
21
|
+
byTz.set(c.timezone, c);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
_byTz = byTz;
|
|
25
|
+
return _byTz;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Representative location for an IANA timezone (its most-populous city).
|
|
30
|
+
* @param {string} ianaName e.g. "Asia/Bangkok"
|
|
31
|
+
* @returns {{ lat:number, lng:number, city:string, province:string|null,
|
|
32
|
+
* country:string|null, source:'timezone-city' }|null}
|
|
33
|
+
*/
|
|
34
|
+
function locationForTimezone(ianaName) {
|
|
35
|
+
if (!ianaName) return null;
|
|
36
|
+
const hit = buildIndex().get(ianaName);
|
|
37
|
+
if (!hit) return null;
|
|
38
|
+
return {
|
|
39
|
+
lat: hit.lat,
|
|
40
|
+
lng: hit.lng,
|
|
41
|
+
city: hit.city || hit.city_ascii || null,
|
|
42
|
+
province: hit.province || null,
|
|
43
|
+
country: hit.country || null,
|
|
44
|
+
source: 'timezone-city',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Normalize a location input. Accepts either explicit coordinates or falls back
|
|
50
|
+
* to the timezone's representative city.
|
|
51
|
+
*
|
|
52
|
+
* @param {{ lat?:number, lng?:number, timezone?:string }} input
|
|
53
|
+
* @returns {{ lat:number, lng:number, source:string, city?, country? }|null}
|
|
54
|
+
*/
|
|
55
|
+
function resolveLocation(input = {}) {
|
|
56
|
+
const { lat, lng, timezone } = input;
|
|
57
|
+
if (Number.isFinite(lat) && Number.isFinite(lng)) {
|
|
58
|
+
return { lat, lng, source: 'explicit' };
|
|
59
|
+
}
|
|
60
|
+
if (timezone) {
|
|
61
|
+
const loc = locationForTimezone(timezone);
|
|
62
|
+
if (loc) return loc;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { locationForTimezone, resolveLocation };
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
const { getTimeZones } = require('@vvo/tzdb');
|
|
2
|
+
const cityTimezones = require('city-timezones');
|
|
3
|
+
|
|
4
|
+
// Build the tzdb index once. ~400 entries.
|
|
5
|
+
//
|
|
6
|
+
// We index by lowercased IANA name for O(1) merge lookups, and keep a flat list
|
|
7
|
+
// for substring scanning. Each entry carries a precomputed `searchHaystack`
|
|
8
|
+
// string so scoring is just a few `indexOf` calls.
|
|
9
|
+
let _tzIndex = null;
|
|
10
|
+
|
|
11
|
+
function buildTzIndex() {
|
|
12
|
+
if (_tzIndex) return _tzIndex;
|
|
13
|
+
const list = getTimeZones();
|
|
14
|
+
const byIana = new Map();
|
|
15
|
+
const indexed = list.map((z) => {
|
|
16
|
+
const haystackParts = [
|
|
17
|
+
z.name,
|
|
18
|
+
z.alternativeName,
|
|
19
|
+
z.abbreviation,
|
|
20
|
+
z.countryName,
|
|
21
|
+
z.countryCode,
|
|
22
|
+
...(z.mainCities || []),
|
|
23
|
+
...(z.group || []),
|
|
24
|
+
].filter(Boolean);
|
|
25
|
+
|
|
26
|
+
const entry = {
|
|
27
|
+
ianaName: z.name,
|
|
28
|
+
alternativeName: z.alternativeName,
|
|
29
|
+
countryName: z.countryName,
|
|
30
|
+
countryCode: z.countryCode,
|
|
31
|
+
mainCities: z.mainCities || [],
|
|
32
|
+
mainCity: (z.mainCities && z.mainCities[0]) || null,
|
|
33
|
+
currentOffsetMinutes: z.currentTimeOffsetInMinutes,
|
|
34
|
+
rawOffsetMinutes: z.rawOffsetInMinutes,
|
|
35
|
+
abbreviation: z.abbreviation,
|
|
36
|
+
currentTimeFormat: z.currentTimeFormat,
|
|
37
|
+
_haystack: haystackParts.join('\u0001').toLowerCase(),
|
|
38
|
+
_ianaLower: z.name.toLowerCase(),
|
|
39
|
+
_altLower: (z.alternativeName || '').toLowerCase(),
|
|
40
|
+
_citiesLower: (z.mainCities || []).map((c) => c.toLowerCase()),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
byIana.set(entry._ianaLower, entry);
|
|
44
|
+
return entry;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
_tzIndex = { list: indexed, byIana };
|
|
48
|
+
return _tzIndex;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function formatOffset(minutes) {
|
|
52
|
+
if (!Number.isFinite(minutes)) return '';
|
|
53
|
+
const sign = minutes >= 0 ? '+' : '-';
|
|
54
|
+
const abs = Math.abs(minutes);
|
|
55
|
+
const hh = String(Math.floor(abs / 60)).padStart(2, '0');
|
|
56
|
+
const mm = String(abs % 60).padStart(2, '0');
|
|
57
|
+
return `${sign}${hh}:${mm}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function buildLabel(entry, hintCity) {
|
|
61
|
+
const offset = formatOffset(entry.currentOffsetMinutes);
|
|
62
|
+
const city = hintCity || entry.mainCity;
|
|
63
|
+
const cityPart = city ? ` — ${city}` : '';
|
|
64
|
+
const countryPart = entry.countryName ? ` (${entry.countryName})` : '';
|
|
65
|
+
return `${entry.ianaName} ${offset}${cityPart}${countryPart}`.trim();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Score a tzdb entry against a lowercased query.
|
|
70
|
+
* Higher is better; 0 means "no match".
|
|
71
|
+
*/
|
|
72
|
+
function scoreTzEntry(entry, q) {
|
|
73
|
+
if (!q) return 0;
|
|
74
|
+
|
|
75
|
+
// Strongest: exact IANA hit.
|
|
76
|
+
if (entry._ianaLower === q) return 1000;
|
|
77
|
+
|
|
78
|
+
// IANA prefix / contains.
|
|
79
|
+
if (entry._ianaLower.startsWith(q)) return 800;
|
|
80
|
+
// Match against the segment after a slash, e.g. "tokyo" → "asia/tokyo".
|
|
81
|
+
const lastSegment = entry._ianaLower.split('/').pop();
|
|
82
|
+
if (lastSegment.startsWith(q)) return 750;
|
|
83
|
+
|
|
84
|
+
// City prefix.
|
|
85
|
+
for (const c of entry._citiesLower) {
|
|
86
|
+
if (c === q) return 700;
|
|
87
|
+
if (c.startsWith(q)) return 650;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Alternative name prefix (e.g. "central european" → "Central European Time").
|
|
91
|
+
if (entry._altLower.startsWith(q)) return 500;
|
|
92
|
+
|
|
93
|
+
// Generic contains anywhere in the haystack.
|
|
94
|
+
if (entry._haystack.includes(q)) return 200;
|
|
95
|
+
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Search timezones by free-text query.
|
|
101
|
+
*
|
|
102
|
+
* Combines @vvo/tzdb (rich IANA metadata + current offsets) with
|
|
103
|
+
* city-timezones (city → IANA lookup, including province/state matches like
|
|
104
|
+
* "Bali" → Asia/Makassar). Results are deduplicated by IANA name and ranked
|
|
105
|
+
* by a small scoring function.
|
|
106
|
+
*
|
|
107
|
+
* @param {string} query
|
|
108
|
+
* @param {{ limit?: number }} [options]
|
|
109
|
+
* @returns {Array<{
|
|
110
|
+
* ianaName: string,
|
|
111
|
+
* mainCity: string|null,
|
|
112
|
+
* countryName: string|null,
|
|
113
|
+
* currentOffsetMinutes: number,
|
|
114
|
+
* abbreviation: string|null,
|
|
115
|
+
* label: string,
|
|
116
|
+
* score: number,
|
|
117
|
+
* }>}
|
|
118
|
+
*/
|
|
119
|
+
function searchTimezones(query, options = {}) {
|
|
120
|
+
const limit = Number.isFinite(options.limit) ? options.limit : 10;
|
|
121
|
+
const q = String(query || '').trim().toLowerCase();
|
|
122
|
+
if (!q) return [];
|
|
123
|
+
|
|
124
|
+
const { list, byIana } = buildTzIndex();
|
|
125
|
+
|
|
126
|
+
// 1) Score every tzdb entry directly.
|
|
127
|
+
const scored = new Map(); // ianaLower -> { entry, score, hintCity }
|
|
128
|
+
for (const entry of list) {
|
|
129
|
+
const s = scoreTzEntry(entry, q);
|
|
130
|
+
if (s > 0) {
|
|
131
|
+
scored.set(entry._ianaLower, { entry, score: s, hintCity: null });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 2) Mine city-timezones for province/state-level hits and fold them in.
|
|
136
|
+
// findFromCityStateProvince does case-insensitive substring matching across
|
|
137
|
+
// city, state and province, which is what makes "bali" → Asia/Makassar work.
|
|
138
|
+
let cityHits = [];
|
|
139
|
+
try {
|
|
140
|
+
cityHits = cityTimezones.findFromCityStateProvince(q) || [];
|
|
141
|
+
} catch (_e) {
|
|
142
|
+
cityHits = [];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const hit of cityHits) {
|
|
146
|
+
if (!hit || !hit.timezone) continue;
|
|
147
|
+
const ianaLower = String(hit.timezone).toLowerCase();
|
|
148
|
+
const entry = byIana.get(ianaLower);
|
|
149
|
+
if (!entry) continue;
|
|
150
|
+
|
|
151
|
+
// City score: lower than direct IANA/city tzdb hits (which the loop above
|
|
152
|
+
// already caught), but better than a generic substring miss.
|
|
153
|
+
const cityScore = 600;
|
|
154
|
+
|
|
155
|
+
const existing = scored.get(ianaLower);
|
|
156
|
+
if (!existing || existing.score < cityScore) {
|
|
157
|
+
scored.set(ianaLower, { entry, score: cityScore, hintCity: hit.city });
|
|
158
|
+
} else if (!existing.hintCity && hit.city) {
|
|
159
|
+
// Keep the higher score, but enrich the label with the matched city name.
|
|
160
|
+
existing.hintCity = hit.city;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return Array.from(scored.values())
|
|
165
|
+
.sort((a, b) => {
|
|
166
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
167
|
+
// Tie-break: prefer larger populations indirectly via tzdb's "main city"
|
|
168
|
+
// ordering. Falls back to alphabetical.
|
|
169
|
+
return a.entry.ianaName.localeCompare(b.entry.ianaName);
|
|
170
|
+
})
|
|
171
|
+
.slice(0, limit)
|
|
172
|
+
.map(({ entry, score, hintCity }) => ({
|
|
173
|
+
ianaName: entry.ianaName,
|
|
174
|
+
mainCity: entry.mainCity,
|
|
175
|
+
countryName: entry.countryName,
|
|
176
|
+
currentOffsetMinutes: entry.currentOffsetMinutes,
|
|
177
|
+
abbreviation: entry.abbreviation,
|
|
178
|
+
label: buildLabel(entry, hintCity),
|
|
179
|
+
score,
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
module.exports = { searchTimezones, formatOffset };
|