klio 1.1.7

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.
@@ -0,0 +1,990 @@
1
+ const swisseph = require('swisseph');
2
+ const fs = require('fs');
3
+ const moment = require('moment-timezone');
4
+ const { planets, signs, elements, decans, dignities } = require('./astrologyConstants');
5
+ const { loadConfig } = require('../config/configService');
6
+ const path = require('path');
7
+
8
+ // Standardort (Berlin, Deutschland) - kann später konfiguriert werden
9
+ const defaultLatitude = 52.5200; // Berlin Breitengrad
10
+ const defaultLongitude = 13.4050; // Berlin Längengrad
11
+
12
+ // Swisseph initialisieren
13
+ swisseph.swe_set_ephe_path(__dirname + '/../../ephe');
14
+
15
+ // Funktion zur Berechnung der aktuellen Zeit in der konfigurierten Zeitzone
16
+ function getCurrentTimeInTimezone() {
17
+ try {
18
+ const configPath = path.join(__dirname, '../../astrocli-config.json');
19
+ if (fs.existsSync(configPath)) {
20
+ const configData = fs.readFileSync(configPath, 'utf8');
21
+ const config = JSON.parse(configData);
22
+
23
+ if (config && config.currentLocation && config.currentLocation.timezone) {
24
+ // Verwende die konfigurierte Zeitzone
25
+ const now = moment().tz(config.currentLocation.timezone);
26
+ return {
27
+ year: now.year(),
28
+ month: now.month() + 1, // moment Monate sind 0-basiert
29
+ day: now.date(),
30
+ hour: now.hours(),
31
+ minute: now.minutes()
32
+ };
33
+ }
34
+ }
35
+ } catch (error) {
36
+ console.log('Keine Zeitzonenkonfiguration gefunden, verwende lokale Zeit');
37
+ }
38
+
39
+ // Fallback: Verwende lokale Systemzeit
40
+ const now = new Date();
41
+ return {
42
+ year: now.getFullYear(),
43
+ month: now.getMonth() + 1,
44
+ day: now.getDate(),
45
+ hour: now.getHours(),
46
+ minute: now.getMinutes()
47
+ };
48
+ }
49
+
50
+ // Funktion zum Laden der Geburtsdaten aus der Konfiguration
51
+ function getBirthDataFromConfig() {
52
+ try {
53
+ const configPath = path.join(__dirname, '../../astrocli-config.json');
54
+ if (fs.existsSync(configPath)) {
55
+ const configData = fs.readFileSync(configPath, 'utf8');
56
+ const config = JSON.parse(configData);
57
+
58
+ if (config && config.birthData) {
59
+ // Parse Geburtsdatum (Format: TT.MM.JJJJ)
60
+ const dateParts = config.birthData.date.split('.');
61
+ const day = parseInt(dateParts[0]);
62
+ const month = parseInt(dateParts[1]);
63
+ const year = parseInt(dateParts[2]);
64
+
65
+ // Parse Geburtszeit (Format: HH:MM)
66
+ const timeParts = config.birthData.time.split(':');
67
+ const hour = parseInt(timeParts[0]);
68
+ const minute = parseInt(timeParts[1]);
69
+
70
+ return {
71
+ year: year,
72
+ month: month,
73
+ day: day,
74
+ hour: hour,
75
+ minute: minute,
76
+ location: config.birthData.location
77
+ };
78
+ }
79
+ }
80
+ } catch (error) {
81
+ console.log('Fehler beim Laden der Geburtsdaten:', error.message);
82
+ }
83
+
84
+ return null;
85
+ }
86
+
87
+ // Funktion zur Berechnung des Julian Days in UTC
88
+ function calculateJulianDayUTC(dateComponents, timezoneOffsetMinutes = 0) {
89
+ // Berechne den Julian Day in lokaler Zeit
90
+ const localJulianDay = swisseph.swe_julday(
91
+ dateComponents.year,
92
+ dateComponents.month,
93
+ dateComponents.day,
94
+ dateComponents.hour + dateComponents.minute / 60,
95
+ swisseph.SE_GREG_CAL
96
+ );
97
+
98
+ // Konvertiere zu UTC, indem wir die Zeitzonenverschiebung berücksichtigen
99
+ // Die Swiss Ephemeris erwartet UTC, also müssen wir die Zeitzone anpassen
100
+ // timezoneOffsetMinutes ist (Lokalzeit - UTC) in Minuten.
101
+ // In JS gibt getTimezoneOffset() (UTC - Lokalzeit) in Minuten zurück.
102
+ // Wir müssen also vorsichtig sein, welches Format wir verwenden.
103
+ // Wenn timezoneOffsetMinutes (Lokalzeit - UTC) ist, dann:
104
+ // utcJulianDay = localJulianDay - (offset / 1440)
105
+ const utcJulianDay = localJulianDay - (timezoneOffsetMinutes / 1440); // 1440 Minuten pro Tag
106
+
107
+ return utcJulianDay;
108
+ }
109
+
110
+ /**
111
+ * Hilfsfunktion zur Ermittlung des Zeitzonen-Offsets für ein bestimmtes Datum und einen Ort.
112
+ * @param {Object} dateComponents - {year, month, day, hour, minute}
113
+ * @param {string} timezone - Zeitzone (z.B. "Europe/Zurich")
114
+ * @returns {number} Offset in Minuten (Lokalzeit - UTC)
115
+ */
116
+ function getTimezoneOffset(dateComponents, timezone) {
117
+ if (!timezone) return -new Date().getTimezoneOffset();
118
+
119
+ try {
120
+ const dateStr = `${dateComponents.year}-${String(dateComponents.month).padStart(2, '0')}-${String(dateComponents.day).padStart(2, '0')} ${String(dateComponents.hour).padStart(2, '0')}:${String(dateComponents.minute).padStart(2, '0')}`;
121
+ const m = moment.tz(dateStr, "YYYY-MM-DD HH:mm", timezone);
122
+ return m.utcOffset(); // Gibt Offset in Minuten zurück (z.B. 120 für UTC+2)
123
+ } catch (error) {
124
+ console.error('Fehler bei der Offset-Berechnung:', error);
125
+ return -new Date().getTimezoneOffset();
126
+ }
127
+ }
128
+
129
+ // Funktion zur Berechnung der Häuser
130
+ function calculateHouses(julianDay, houseSystem = 'K', useBirthLocation = false) {
131
+ return new Promise((resolve, reject) => {
132
+ // Lade die konfigurierten Standortdaten
133
+ const configPath = path.join(__dirname, '../../astrocli-config.json');
134
+ let config;
135
+ try {
136
+ if (fs.existsSync(configPath)) {
137
+ const configData = fs.readFileSync(configPath, 'utf8');
138
+ config = JSON.parse(configData);
139
+ }
140
+ } catch (error) {
141
+ console.log('Keine Konfiguration gefunden, verwende Standardort (Berlin)');
142
+ }
143
+
144
+ let latitude, longitude, locationName;
145
+
146
+ if (useBirthLocation && config && config.birthData && config.birthData.location) {
147
+ // Verwende Geburtsort für Geburtscharts
148
+ latitude = config.birthData.location.latitude;
149
+ longitude = config.birthData.location.longitude;
150
+ locationName = `${config.birthData.location.name}, ${config.birthData.location.country}`;
151
+ console.log(`Verwende Geburtsort: ${locationName} (${latitude}° Breitengrad, ${longitude}° Längengrad)`);
152
+ } else if (useBirthLocation) {
153
+ // Geburtsort ist konfiguriert, aber kein Standort - verwende Standardort
154
+ latitude = defaultLatitude;
155
+ longitude = defaultLongitude;
156
+ locationName = 'Berlin, Deutschland';
157
+ console.log(`Verwende Standard-Geburtsort: ${locationName} (${latitude}° Breitengrad, ${longitude}° Längengrad)`);
158
+ console.log('⚠️ Kein Geburtsort in der Konfiguration gefunden.');
159
+ } else {
160
+ // Verwende aktuellen Standort für aktuelle Berechnungen
161
+ latitude = config && config.currentLocation ? config.currentLocation.latitude : defaultLatitude;
162
+ longitude = config && config.currentLocation ? config.currentLocation.longitude : defaultLongitude;
163
+ locationName = config && config.currentLocation ? `${config.currentLocation.name}, ${config.currentLocation.country}` : 'Berlin, Deutschland';
164
+ }
165
+
166
+ swisseph.swe_houses(julianDay, latitude, longitude, houseSystem, function(result) {
167
+ if (result.error) {
168
+ console.error('Fehler bei der Hausberechnung:', result.error);
169
+ reject(result.error);
170
+ } else {
171
+ // Die Swiss Ephemeris gibt im result.house Objekt oft Indices als Strings zurück ("1", "2", ...)
172
+ // Wir konvertieren dies in ein sauberes 0-basiertes Array (0-11) für eine konsistente Verarbeitung.
173
+ const houseCusps = [];
174
+
175
+ // Wir versuchen zuerst die Indizes 1 bis 12 direkt abzurufen
176
+ for (let i = 1; i <= 12; i++) {
177
+ const cusp = result.house[i];
178
+ if (cusp !== undefined) {
179
+ houseCusps.push(cusp);
180
+ }
181
+ }
182
+
183
+ // Falls wir nicht 12 Häuser haben (z.B. wenn das Objekt andere Keys hat),
184
+ // versuchen wir alle numerischen Keys zu sammeln
185
+ if (houseCusps.length < 12) {
186
+ const keys = Object.keys(result.house).filter(k => !isNaN(k)).sort((a,b) => parseInt(a) - parseInt(b));
187
+
188
+ if (keys.length >= 13) {
189
+ // Wahrscheinlich Index 0 = AC, 1-12 = Häuser
190
+ houseCusps.length = 0;
191
+ for (let i = 1; i <= 12; i++) {
192
+ const key = keys.find(k => parseInt(k) === i);
193
+ if (key !== undefined) {
194
+ houseCusps.push(result.house[key]);
195
+ }
196
+ }
197
+ } else if (keys.length === 12) {
198
+ // Wahrscheinlich direkt 0-11 = Häuser
199
+ houseCusps.length = 0;
200
+ for (let i = 0; i < 12; i++) {
201
+ houseCusps.push(result.house[keys[i]]);
202
+ }
203
+ }
204
+ }
205
+
206
+ // Falls wir immer noch keine 12 Häuser haben (sollte nicht passieren),
207
+ // füllen wir mit den verfügbaren Daten auf oder 0
208
+ while (houseCusps.length < 12) {
209
+ houseCusps.push(0);
210
+ }
211
+
212
+ const formattedResult = {
213
+ ...result,
214
+ house: houseCusps
215
+ };
216
+
217
+ resolve(formattedResult);
218
+ }
219
+ });
220
+ });
221
+ }
222
+
223
+ // Funktion zur Bestimmung des Hauses für einen Planeten
224
+ function getPlanetHouse(planetLongitude, houseCusps) {
225
+ // Normalisiere die Planetenlänge auf 0-360 Bereich
226
+ planetLongitude = planetLongitude % 360;
227
+ if (planetLongitude < 0) planetLongitude += 360;
228
+
229
+ // Hausgrenzen sind in houseCusps[0] bis houseCusps[11]
230
+ // Wir müssen finden, zwischen welchen zwei Hausspitzen der Planet liegt
231
+ for (let i = 0; i < 12; i++) {
232
+ const currentCusp = houseCusps[i];
233
+ const nextCusp = houseCusps[(i + 1) % 12];
234
+
235
+ // Normalisiere die Hausspitzen auf 0-360 Bereich
236
+ const normalizedCurrentCusp = currentCusp % 360;
237
+ const normalizedNextCusp = nextCusp % 360;
238
+
239
+ // Berücksichtige den Übergang über 360°
240
+ if (normalizedNextCusp < normalizedCurrentCusp) {
241
+ // Fall: Hausgrenze überquert 0°/360°
242
+ // Ein Planet ist in diesem Haus, wenn er >= der aktuellen Hausspitze ODER < der nächsten Hausspitze ist
243
+ if (planetLongitude >= normalizedCurrentCusp || planetLongitude < normalizedNextCusp) {
244
+ return i + 1; // Häuser sind 1-basiert
245
+ }
246
+ } else {
247
+ // Normalfall
248
+ if (planetLongitude >= normalizedCurrentCusp && planetLongitude < normalizedNextCusp) {
249
+ return i + 1; // Häuser sind 1-basiert
250
+ }
251
+ }
252
+ }
253
+
254
+ // Debug: Zeige die Hausspitzen an, wenn kein Haus gefunden wurde
255
+ console.log('Kein Haus gefunden für Planet:', planetLongitude);
256
+ console.log('Hausspitzen:', houseCusps);
257
+
258
+ // Fallback: Sollte nicht vorkommen
259
+ return 1;
260
+ }
261
+
262
+ // Funktion zur Berechnung der astrologischen Daten
263
+ function getAstrologicalData(planetName, customDate = null) {
264
+ const planet = planets[planetName];
265
+ if (planet === undefined) {
266
+ console.error(`Ungültiger Planet: ${planetName}. Verfügbare Planeten:`, Object.keys(planets).join(', '));
267
+ process.exit(1);
268
+ }
269
+
270
+ // Verwende das angegebene Datum oder das aktuelle Datum (mit Zeitzonenberücksichtigung)
271
+ let calcYear, calcMonth, calcDay, calcHour, calcMinute;
272
+
273
+ if (customDate) {
274
+ calcYear = customDate.year;
275
+ calcMonth = customDate.month;
276
+ calcDay = customDate.day;
277
+ calcHour = customDate.hour;
278
+ calcMinute = customDate.minute;
279
+ } else {
280
+ // Verwende die aktuelle Zeit in der konfigurierten Zeitzone
281
+ const timeData = getCurrentTimeInTimezone();
282
+ calcYear = timeData.year;
283
+ calcMonth = timeData.month;
284
+ calcDay = timeData.day;
285
+ calcHour = timeData.hour;
286
+ calcMinute = timeData.minute;
287
+ }
288
+
289
+ const julianDay = swisseph.swe_julday(calcYear, calcMonth, calcDay, calcHour + calcMinute / 60, swisseph.SE_GREG_CAL);
290
+ const flag = swisseph.SEFLG_SWIEPH | swisseph.SEFLG_SPEED;
291
+ const result = swisseph.swe_calc_ut(julianDay, planet, flag);
292
+
293
+ if (result.error) {
294
+ console.error('Fehler bei der Berechnung:', result.error);
295
+ process.exit(1);
296
+ }
297
+
298
+ const longitude = result.longitude;
299
+ const signIndex = Math.floor(longitude / 30);
300
+ const degreeInSign = (longitude % 30).toFixed(2);
301
+ const sign = signs[signIndex];
302
+ const element = elements[signIndex];
303
+ const decan = decans[Math.floor((longitude % 30) / 10)];
304
+
305
+ const dignityInfo = dignities[planet];
306
+ let dignity = 'Neutral';
307
+ if (sign === dignityInfo.sign) {
308
+ dignity = 'Herrscher';
309
+ } else if (sign === dignityInfo.exaltation) {
310
+ dignity = 'Erhöhung';
311
+ } else if (sign === dignityInfo.fall) {
312
+ dignity = 'Fall';
313
+ } else if (sign === dignityInfo.detriment) {
314
+ dignity = 'Detriment';
315
+ }
316
+
317
+ return {
318
+ planet: planetName,
319
+ longitude,
320
+ sign,
321
+ degreeInSign,
322
+ dignity,
323
+ element,
324
+ decan
325
+ };
326
+ }
327
+
328
+ // Funktion zur Identifizierung kritischer Planeten
329
+ function getCriticalPlanets(customDate = null) {
330
+ const criticalPlanets = [];
331
+
332
+ // Kritische Grade: 0°, 13°, 26° (kardinale Grade) und 29° (anaretischer Grad)
333
+ const criticalDegrees = [0, 13, 26, 29];
334
+ const orb = 1; // Toleranz von 1 Grad
335
+
336
+ // Verwende das angegebene Datum oder die aktuelle Zeit
337
+ let timeData;
338
+ if (customDate) {
339
+ timeData = customDate;
340
+ } else {
341
+ timeData = getCurrentTimeInTimezone();
342
+ }
343
+
344
+ const julianDay = swisseph.swe_julday(
345
+ timeData.year,
346
+ timeData.month,
347
+ timeData.day,
348
+ timeData.hour + timeData.minute / 60,
349
+ swisseph.SE_GREG_CAL
350
+ );
351
+
352
+ // Berechne Positionen aller Planeten
353
+ for (const [name, planetId] of Object.entries(planets)) {
354
+ const flag = swisseph.SEFLG_SWIEPH | swisseph.SEFLG_SPEED;
355
+ const result = swisseph.swe_calc_ut(julianDay, planetId, flag);
356
+
357
+ if (result.error) {
358
+ console.error(`Fehler bei der Berechnung für ${name}:`, result.error);
359
+ continue;
360
+ }
361
+
362
+ const longitude = result.longitude;
363
+ const degreeInSign = longitude % 30;
364
+ const signIndex = Math.floor(longitude / 30);
365
+ const sign = signs[signIndex];
366
+
367
+ // Prüfe, ob der Planet auf einem kritischen Grad steht
368
+ const isCritical = criticalDegrees.some(criticalDegree => {
369
+ return Math.abs(degreeInSign - criticalDegree) <= orb;
370
+ });
371
+
372
+ if (isCritical) {
373
+ criticalPlanets.push({
374
+ name,
375
+ sign,
376
+ degree: degreeInSign.toFixed(2),
377
+ isCritical: true,
378
+ criticalType: degreeInSign >= 28.5 ? 'Anaretisch (29°)' : 'Kardinal (0°, 13°, 26°)'
379
+ });
380
+ }
381
+ }
382
+
383
+ return criticalPlanets;
384
+ }
385
+
386
+ // Funktion zur Berechnung von Planetenaspekten
387
+ function calculatePlanetAspects(planetName, dateComponents, useHuberOrbs = false) {
388
+ // Berechne Planetenposition
389
+ const targetPlanetData = getAstrologicalData(planetName, dateComponents);
390
+ const targetPlanetLongitude = targetPlanetData.longitude;
391
+
392
+ // Berechne Positionen aller anderen Planeten
393
+ const planetPositions = {};
394
+ for (const [name, planetId] of Object.entries(planets)) {
395
+ if (name === planetName) continue;
396
+ const planetData = getAstrologicalData(name, dateComponents);
397
+ planetPositions[name] = planetData.longitude;
398
+ }
399
+
400
+ // Berechne Aspekte (Konjunktion, Opposition, Quadrat, Trigon, Sextil)
401
+ const aspects = [];
402
+ const aspectTypes = useHuberOrbs ? [
403
+ { name: 'Konjunktion', angle: 0, orb: 8 },
404
+ { name: 'Opposition', angle: 180, orb: 8 },
405
+ { name: 'Quadrat', angle: 90, orb: 6 },
406
+ { name: 'Trigon', angle: 120, orb: 6 },
407
+ { name: 'Sextil', angle: 60, orb: 4 }
408
+ ] : [
409
+ { name: 'Konjunktion', angle: 0, orb: 10 },
410
+ { name: 'Opposition', angle: 180, orb: 10 },
411
+ { name: 'Quadrat', angle: 90, orb: 8 },
412
+ { name: 'Trigon', angle: 120, orb: 8 },
413
+ { name: 'Sextil', angle: 60, orb: 6 }
414
+ ];
415
+
416
+ for (const [name, planetLongitude] of Object.entries(planetPositions)) {
417
+ const angleDiff = Math.abs(targetPlanetLongitude - planetLongitude) % 360;
418
+ const normalizedAngle = Math.min(angleDiff, 360 - angleDiff);
419
+
420
+ for (const aspect of aspectTypes) {
421
+ if (Math.abs(normalizedAngle - aspect.angle) <= aspect.orb) {
422
+ aspects.push({
423
+ type: aspect.name,
424
+ planet: name,
425
+ angle: normalizedAngle.toFixed(2),
426
+ orb: Math.abs(normalizedAngle - aspect.angle).toFixed(2)
427
+ });
428
+ }
429
+ }
430
+ }
431
+
432
+ return aspects;
433
+ }
434
+
435
+ // Funktion zur Anzeige von Planetenaspekten
436
+ function showPlanetAspects(planetName, dateComponents, useBirthData = false, useHuberOrbs = false) {
437
+ const aspects = calculatePlanetAspects(planetName, dateComponents, useHuberOrbs);
438
+
439
+ const planetLabel = planetName.charAt(0).toUpperCase() + planetName.slice(1);
440
+
441
+ console.log(`Aspekte für ${planetLabel}:`);
442
+ console.log('=================================================================');
443
+ console.log('| Aspekt | Planet | Winkel | Orb |');
444
+ console.log('=================================================================');
445
+
446
+ if (aspects.length === 0) {
447
+ console.log('Keine signifikanten Aspekte gefunden.');
448
+ } else {
449
+ aspects.forEach(aspect => {
450
+ const aspectName = aspect.type.padEnd(11, ' ');
451
+ const planetNameFormatted = aspect.planet.charAt(0).toUpperCase() + aspect.planet.slice(1);
452
+ const planetFormatted = planetNameFormatted.padEnd(8, ' ');
453
+ const angle = aspect.angle.padEnd(6, ' ');
454
+ const orb = aspect.orb.padEnd(4, ' ');
455
+
456
+ console.log(`| ${aspectName} | ${planetFormatted} | ${angle}° | ${orb}° |`);
457
+ });
458
+ }
459
+
460
+ console.log('=================================================================');
461
+
462
+ if (useBirthData) {
463
+ console.log('\nDiese Analyse basiert auf deinem Geburtshoroskop.');
464
+ } else {
465
+ console.log('\nDiese Analyse basiert auf der aktuellen Planetenposition.');
466
+ }
467
+ }
468
+
469
+ // Funktion zur Berechnung aller aktiven Aspekte zwischen allen Planeten
470
+ function getAllActiveAspects(dateComponents) {
471
+ const allAspects = [];
472
+ const planetNames = Object.keys(planets);
473
+ const seenAspects = new Set(); // Zur Vermeidung von Duplikaten
474
+
475
+ // Berechne Aspekte für alle Planetenpaare
476
+ for (let i = 0; i < planetNames.length; i++) {
477
+ const planet1 = planetNames[i];
478
+ const aspects = calculatePlanetAspects(planet1, dateComponents, true); // Immer Huber-Orbs verwenden
479
+
480
+ aspects.forEach(aspect => {
481
+ // Erstelle eine eindeutige Kennung für den Aspekt (alphabetisch sortiert)
482
+ const planetPair = [planet1, aspect.planet].sort().join('-');
483
+ const aspectKey = `${planetPair}-${aspect.type}`;
484
+
485
+ // Füge den Aspekt nur hinzu, wenn er noch nicht existiert
486
+ if (!seenAspects.has(aspectKey)) {
487
+ seenAspects.add(aspectKey);
488
+ allAspects.push({
489
+ planet1: planet1,
490
+ planet2: aspect.planet,
491
+ type: aspect.type,
492
+ angle: aspect.angle,
493
+ orb: aspect.orb
494
+ });
495
+ }
496
+ });
497
+ }
498
+
499
+ return allAspects;
500
+ }
501
+
502
+ // Funktion zur Überprüfung der Rückläufigkeit eines Planeten
503
+ function isPlanetRetrograde(planetName, dateComponents) {
504
+ try {
505
+ const planet = planets[planetName];
506
+ if (planet === undefined) return false;
507
+
508
+ // Berechne die Position mit Geschwindigkeitsinformation
509
+ const julianDay = swisseph.swe_julday(
510
+ dateComponents.year,
511
+ dateComponents.month,
512
+ dateComponents.day,
513
+ dateComponents.hour + dateComponents.minute / 60,
514
+ swisseph.SE_GREG_CAL
515
+ );
516
+
517
+ const flag = swisseph.SEFLG_SWIEPH | swisseph.SEFLG_SPEED;
518
+ const result = swisseph.swe_calc_ut(julianDay, planet, flag);
519
+
520
+ if (result.error) {
521
+ console.log('Fehler bei Rückläufigkeitsprüfung:', result.error);
522
+ return false;
523
+ }
524
+
525
+ // Die Geschwindigkeit befindet sich in result.longitudeSpeed
526
+ // Swiss Ephemeris gibt die Geschwindigkeit als separate Eigenschaften zurück
527
+ if (result.longitudeSpeed !== undefined) {
528
+ return result.longitudeSpeed < 0;
529
+ }
530
+
531
+ return false;
532
+ } catch (error) {
533
+ console.log('Fehler in isPlanetRetrograde:', error.message);
534
+ return false;
535
+ }
536
+ }
537
+
538
+ // Funktion zur Bestimmung der Aspekt-Phase (annähernd, exakt, separativ)
539
+ function determineAspectPhase(planet1, planet2, dateComponents, aspectType, targetAngle) {
540
+ // Berechne aktuelle Positionen und Geschwindigkeiten
541
+ const planet1Data = getAstrologicalData(planet1, dateComponents);
542
+ const planet2Data = getAstrologicalData(planet2, dateComponents);
543
+
544
+ // Berechne Positionen für einen leicht späteren Zeitpunkt (1 Tag später)
545
+ const nextDate = {
546
+ year: dateComponents.year,
547
+ month: dateComponents.month,
548
+ day: dateComponents.day + 1,
549
+ hour: dateComponents.hour,
550
+ minute: dateComponents.minute
551
+ };
552
+
553
+ const planet1NextData = getAstrologicalData(planet1, nextDate);
554
+ const planet2NextData = getAstrologicalData(planet2, nextDate);
555
+
556
+ // Berechne aktuellen Winkel
557
+ const currentAngleDiff = Math.abs(planet1Data.longitude - planet2Data.longitude) % 360;
558
+ const currentNormalizedAngle = Math.min(currentAngleDiff, 360 - currentAngleDiff);
559
+
560
+ // Berechne zukünftigen Winkel
561
+ const nextAngleDiff = Math.abs(planet1NextData.longitude - planet2NextData.longitude) % 360;
562
+ const nextNormalizedAngle = Math.min(nextAngleDiff, 360 - nextAngleDiff);
563
+
564
+ // Bestimme die Phase basierend auf der Winkelentwicklung
565
+ const currentDistanceToTarget = Math.abs(currentNormalizedAngle - targetAngle);
566
+ const nextDistanceToTarget = Math.abs(nextNormalizedAngle - targetAngle);
567
+
568
+ // Wenn der Winkel sich dem Ziel nähert = annähernd
569
+ // Wenn der Winkel genau auf dem Ziel ist = exakt
570
+ // Wenn der Winkel sich vom Ziel entfernt = separativ
571
+
572
+ if (currentDistanceToTarget < 0.5) {
573
+ return 'exakt';
574
+ } else if (nextDistanceToTarget < currentDistanceToTarget) {
575
+ return 'annähernd';
576
+ } else {
577
+ return 'separativ';
578
+ }
579
+ }
580
+
581
+ // Funktion zur Analyse der Elementverteilung
582
+ function analyzeElementDistribution(dateComponents, useBirthData = false) {
583
+ const elementCounts = {
584
+ 'Feuer': 0,
585
+ 'Erde': 0,
586
+ 'Luft': 0,
587
+ 'Wasser': 0
588
+ };
589
+
590
+ const planetElements = {};
591
+
592
+ // Berechne Elemente aller Planeten
593
+ for (const [name, planetId] of Object.entries(planets)) {
594
+ const data = getAstrologicalData(name, dateComponents);
595
+ elementCounts[data.element]++;
596
+ planetElements[name] = data.element;
597
+ }
598
+
599
+ // Berechne Gesamtzahl der Planeten
600
+ const totalPlanets = Object.keys(planets).length;
601
+
602
+ // Berechne Prozentsätze
603
+ const elementPercentages = {};
604
+ for (const [element, count] of Object.entries(elementCounts)) {
605
+ elementPercentages[element] = ((count / totalPlanets) * 100).toFixed(1);
606
+ }
607
+
608
+ // Erstelle horizontales Chart
609
+ console.log('Elementverteilung der Planeten:');
610
+ console.log('================================================================================');
611
+
612
+ // Maximale Balkenlänge (pro Planet)
613
+ const maxBars = 20;
614
+
615
+ for (const [element, count] of Object.entries(elementCounts)) {
616
+ const percentage = elementPercentages[element];
617
+ const barLength = Math.round((count / totalPlanets) * maxBars);
618
+ const bar = '|'.repeat(barLength);
619
+ const paddedBar = bar.padEnd(maxBars, ' ');
620
+
621
+ console.log(`${element.padEnd(10)}: ${paddedBar} ${barLength.toString().padStart(2)}/${totalPlanets} (${percentage}%)`);
622
+ }
623
+
624
+ console.log('================================================================================');
625
+
626
+ // Zeige detaillierte Planeten-Element-Zuordnung
627
+ console.log('\nPlaneten nach Elementen:');
628
+ for (const [element, count] of Object.entries(elementCounts)) {
629
+ const planetsInElement = Object.entries(planetElements)
630
+ .filter(([_, el]) => el === element)
631
+ .map(([name]) => name.charAt(0).toUpperCase() + name.slice(1))
632
+ .join(', ');
633
+
634
+ console.log(`${element}: ${planetsInElement}`);
635
+ }
636
+
637
+ if (useBirthData) {
638
+ console.log('\nDiese Analyse basiert auf deinem Geburtshoroskop.');
639
+ } else {
640
+ console.log('\nDiese Analyse basiert auf der aktuellen Planetenposition.');
641
+ }
642
+
643
+ return {
644
+ elementCounts,
645
+ elementPercentages,
646
+ planetElements
647
+ };
648
+ }
649
+
650
+ // Funktion zur Anzeige aller aktiven Aspekte
651
+ function showAllActiveAspects(dateComponents, useBirthData = false) {
652
+ const allAspects = getAllActiveAspects(dateComponents);
653
+
654
+ // Sortiere Aspekte nach Typ und dann nach Orb (genaueste zuerst)
655
+ const sortedAspects = [...allAspects].sort((a, b) => {
656
+ // Sortiere zuerst nach Aspekt-Typ
657
+ const aspectOrder = {
658
+ 'Konjunktion': 1,
659
+ 'Opposition': 2,
660
+ 'Quadrat': 3,
661
+ 'Trigon': 4,
662
+ 'Sextil': 5
663
+ };
664
+
665
+ const typeCompare = aspectOrder[a.type] - aspectOrder[b.type];
666
+ if (typeCompare !== 0) return typeCompare;
667
+
668
+ // Dann nach Orb (kleinster Orb zuerst = genaueste Aspekte)
669
+ return parseFloat(a.orb) - parseFloat(b.orb);
670
+ });
671
+
672
+ console.log('Aktive Aspekte (alle Planeten) - Klassifiziert nach Präzision:');
673
+ console.log('==========================================================================');
674
+ console.log('| Planet 1 | Planet 2 | Winkel | Orb | Status |');
675
+ console.log('==========================================================================');
676
+
677
+ if (sortedAspects.length === 0) {
678
+ console.log('Keine signifikanten Aspekte gefunden.');
679
+ } else {
680
+ // Gruppiere nach Aspekt-Typ für bessere Übersicht
681
+ const aspectsByType = {};
682
+ sortedAspects.forEach(aspect => {
683
+ if (!aspectsByType[aspect.type]) {
684
+ aspectsByType[aspect.type] = [];
685
+ }
686
+ aspectsByType[aspect.type].push(aspect);
687
+ });
688
+
689
+ // Zeige Aspekte gruppiert nach Typ
690
+ const aspectTypes = ['Konjunktion', 'Opposition', 'Quadrat', 'Trigon', 'Sextil'];
691
+ aspectTypes.forEach(type => {
692
+ if (aspectsByType[type]) {
693
+ console.log(`\n--- ${type} ---`);
694
+ aspectsByType[type].forEach(aspect => {
695
+ // Prüfe Rückläufigkeit für beide Planeten
696
+ const planet1Retrograde = isPlanetRetrograde(aspect.planet1, dateComponents);
697
+ const planet2Retrograde = isPlanetRetrograde(aspect.planet2, dateComponents);
698
+
699
+ // Formatiere Planetenamen mit Rückläufigkeitskennzeichnung (R)
700
+ const planet1Formatted = aspect.planet1.charAt(0).toUpperCase() + aspect.planet1.slice(1) + (planet1Retrograde ? '(R)' : '');
701
+ const planet1Padded = planet1Formatted.padEnd(12, ' ');
702
+ const planet2Formatted = aspect.planet2.charAt(0).toUpperCase() + aspect.planet2.slice(1) + (planet2Retrograde ? '(R)' : '');
703
+ const planet2Padded = planet2Formatted.padEnd(12, ' ');
704
+ const angle = aspect.angle.padEnd(6, ' ');
705
+ const orb = aspect.orb.padEnd(4, ' ');
706
+
707
+ // Bestimme die Aspekt-Phase basierend auf der Bewegungsrichtung
708
+ const aspectAngles = {
709
+ 'Konjunktion': 0,
710
+ 'Opposition': 180,
711
+ 'Quadrat': 90,
712
+ 'Trigon': 120,
713
+ 'Sextil': 60
714
+ };
715
+
716
+ const phase = determineAspectPhase(
717
+ aspect.planet1,
718
+ aspect.planet2,
719
+ dateComponents,
720
+ aspect.type,
721
+ aspectAngles[aspect.type]
722
+ );
723
+ const statusPadded = phase.padEnd(11, ' ');
724
+
725
+ console.log(`| ${planet1Padded} | ${planet2Padded} | ${angle}° | ${orb}° | ${statusPadded} |`);
726
+ });
727
+ }
728
+ });
729
+ }
730
+
731
+ console.log('\n==========================================================================');
732
+ console.log(`Gesamt: ${sortedAspects.length} aktive Aspekte gefunden`);
733
+
734
+ if (useBirthData) {
735
+ console.log('\nDiese Analyse basiert auf deinem Geburtshoroskop.');
736
+ } else {
737
+ console.log('\nDiese Analyse basiert auf der aktuellen Planetenposition.');
738
+ }
739
+
740
+ console.log('(R) = Rückläufiger Planet');
741
+ }
742
+
743
+ module.exports = {
744
+ calculateHouses,
745
+ getPlanetHouse,
746
+ getAstrologicalData,
747
+ calculatePlanetAspects,
748
+ showPlanetAspects,
749
+ getAllActiveAspects,
750
+ showAllActiveAspects,
751
+ getBirthDataFromConfig,
752
+ getCriticalPlanets,
753
+ getCurrentTimeInTimezone,
754
+ getTimezoneOffset,
755
+ isPlanetRetrograde,
756
+ determineAspectPhase,
757
+ calculateJulianDayUTC,
758
+ analyzeElementDistribution,
759
+ getPastAspects,
760
+ getAspectAngle,
761
+ getFutureAspects
762
+ };
763
+
764
+ // Funktion zur Berechnung vergangener Aspekte zwischen zwei Planeten
765
+ function getPastAspects(planet1, planet2, aspectType, count, endDate = null) {
766
+ // Standardmäßig verwenden wir das aktuelle Datum als Enddatum
767
+ if (!endDate) {
768
+ endDate = getCurrentTimeInTimezone();
769
+ }
770
+
771
+ const pastAspects = [];
772
+ const targetAngle = getAspectAngle(aspectType);
773
+
774
+ if (targetAngle === null) {
775
+ console.error(`Ungültiger Aspekt-Typ: ${aspectType}`);
776
+ return [];
777
+ }
778
+
779
+ // Wir gehen rückwärts in der Zeit, beginnend vom Enddatum
780
+ // Jeder Schritt ist 1 Tag rückwärts
781
+ let currentDate = {...endDate};
782
+
783
+ // Maximal 500 Jahre zurückgehen, um Endlosschleifen zu vermeiden
784
+ const maxYears = 500;
785
+ const daysToCheck = maxYears * 365;
786
+
787
+ // Wir verwenden eine sehr kleine Toleranz für exakte Aspekte (0.00001 Grad)
788
+ const exactOrb = 0.01;
789
+
790
+ // Gehe rückwärts durch die Zeit
791
+ for (let i = 0; i < daysToCheck && pastAspects.length < count; i++) {
792
+ // Berechne einen Tag früher
793
+ currentDate = subtractDays(currentDate, 1);
794
+
795
+ // Berechne die Positionen der beiden Planeten
796
+ const planet1Data = getAstrologicalData(planet1, currentDate);
797
+ const planet2Data = getAstrologicalData(planet2, currentDate);
798
+
799
+ // Berechne den Winkel zwischen den Planeten
800
+ const angleDiff = Math.abs(planet1Data.longitude - planet2Data.longitude) % 360;
801
+ const normalizedAngle = Math.min(angleDiff, 360 - angleDiff);
802
+
803
+ // Prüfe, ob der Winkel exakt dem Zielwinkel entspricht (mit minimaler ranz)
804
+ if (Math.abs(normalizedAngle - targetAngle) <= exactOrb) {
805
+ // Prüfe, ob dies ein exakter Aspekt ist (nicht nur annähernd)
806
+ // Wir prüfen auch den nächsten Tag, um sicherzustellen, dass es sich um einen exakten Punkt handelt
807
+ const nextDate = addDays(currentDate, 1);
808
+ const planet1NextData = getAstrologicalData(planet1, nextDate);
809
+ const planet2NextData = getAstrologicalData(planet2, nextDate);
810
+
811
+ const nextAngleDiff = Math.abs(planet1NextData.longitude - planet2NextData.longitude) % 360;
812
+ const nextNormalizedAngle = Math.min(nextAngleDiff, 360 - nextAngleDiff);
813
+
814
+ // Wenn der Winkel sich vom Ziel entfernt, ist dies ein exakter Aspekt
815
+ const currentDistance = Math.abs(normalizedAngle - targetAngle);
816
+ const nextDistance = Math.abs(nextNormalizedAngle - targetAngle);
817
+
818
+ if (nextDistance > currentDistance) {
819
+ // Dies ist ein exakter Aspekt
820
+ // Wir prüfen auch, ob dieser Aspekt bereits in der Liste ist (mit 3 Tagen Abstand)
821
+ const isDuplicate = pastAspects.some(aspect => {
822
+ const daysDiff = Math.abs(
823
+ new Date(aspect.date.year, aspect.date.month - 1, aspect.date.day) -
824
+ new Date(currentDate.year, currentDate.month - 1, currentDate.day)
825
+ ) / (1000 * 60 * 60 * 24);
826
+ return daysDiff < 3; // Mindestens 3 Tage Abstand zwischen Aspekten
827
+ });
828
+
829
+ if (!isDuplicate) {
830
+ pastAspects.push({
831
+ date: {...currentDate},
832
+ planet1: planet1,
833
+ planet2: planet2,
834
+ type: aspectType,
835
+ angle: normalizedAngle.toFixed(2),
836
+ planet1Position: `${planet1Data.sign} ${planet1Data.degreeInSign}°`,
837
+ planet2Position: `${planet2Data.sign} ${planet2Data.degreeInSign}°`
838
+ });
839
+ }
840
+ }
841
+ }
842
+ }
843
+
844
+ return pastAspects;
845
+ }
846
+
847
+ // Hilfsfunktion zur Bestimmung des Zielwinkels für einen Aspekt
848
+ function getAspectAngle(aspectType) {
849
+ const aspectAngles = {
850
+ 'k': 0, // Konjunktion
851
+ 'konjunktion': 0,
852
+ 'o': 180, // Opposition
853
+ 'opposition': 180,
854
+ 'q': 90, // Quadrat
855
+ 'quadrat': 90,
856
+ 't': 120, // Trigon
857
+ 'trigon': 120,
858
+ 's': 60, // Sextil
859
+ 'sextil': 60
860
+ };
861
+
862
+ return aspectAngles[aspectType.toLowerCase()] !== undefined
863
+ ? aspectAngles[aspectType.toLowerCase()]
864
+ : null;
865
+ }
866
+
867
+ // Hilfsfunktion zum Subtrahieren von Tagen von einem Datum (unterstützt auch gebrochene Tage)
868
+ function subtractDays(date, days) {
869
+ // Konvertiere das Datum in Millisekunden seit Epoche
870
+ const originalDate = new Date(date.year, date.month - 1, date.day, date.hour, date.minute);
871
+ const millisecondsPerDay = 24 * 60 * 60 * 1000;
872
+ const millisecondsToSubtract = days * millisecondsPerDay;
873
+
874
+ const newDate = new Date(originalDate.getTime() - millisecondsToSubtract);
875
+ return {
876
+ year: newDate.getFullYear(),
877
+ month: newDate.getMonth() + 1,
878
+ day: newDate.getDate(),
879
+ hour: newDate.getHours(),
880
+ minute: newDate.getMinutes()
881
+ };
882
+ }
883
+
884
+ // Hilfsfunktion zum Addieren von Tagen zu einem Datum (unterstützt auch gebrochene Tage)
885
+ function addDays(date, days) {
886
+ // Konvertiere das Datum in Millisekunden seit Epoche
887
+ const originalDate = new Date(date.year, date.month - 1, date.day, date.hour, date.minute);
888
+ const millisecondsPerDay = 24 * 60 * 60 * 1000;
889
+ const millisecondsToAdd = days * millisecondsPerDay;
890
+
891
+ const newDate = new Date(originalDate.getTime() + millisecondsToAdd);
892
+ return {
893
+ year: newDate.getFullYear(),
894
+ month: newDate.getMonth() + 1,
895
+ day: newDate.getDate(),
896
+ hour: newDate.getHours(),
897
+ minute: newDate.getMinutes()
898
+ };
899
+ }
900
+
901
+ // Funktion zur Berechnung zukünftiger Aspekte zwischen zwei Planeten
902
+ function getFutureAspects(planet1, planet2, aspectType, count, startDate = null) {
903
+ // Standardmäßig verwenden wir das aktuelle Datum als Startdatum
904
+ if (!startDate) {
905
+ startDate = getCurrentTimeInTimezone();
906
+ }
907
+
908
+ const futureAspects = [];
909
+ const targetAngle = getAspectAngle(aspectType);
910
+
911
+ if (targetAngle === null) {
912
+ console.error(`Ungültiger Aspekt-Typ: ${aspectType}`);
913
+ return [];
914
+ }
915
+
916
+ // Wir gehen vorwärts in der Zeit, beginnend vom Startdatum
917
+ let currentDate = {...startDate};
918
+
919
+ // Maximal 1000 Jahre vorwärts gehen, um Endlosschleifen zu vermeiden
920
+ const maxYears = 1000;
921
+ const daysToCheck = maxYears * 365;
922
+
923
+ // Wir verwenden eine größere Toleranz für exakte Aspekte (0.5 Grad)
924
+ const exactOrb = 0.01;
925
+
926
+ // Bestimme die Schrittgröße basierend auf den Planeten
927
+ // Für langsame Planeten (Saturn, Uranus, Neptun, Pluto) verwenden wir kleinere Schritte
928
+ const slowPlanets = ['saturn', 'uranus', 'neptun', 'pluto'];
929
+ const isSlowAspect = slowPlanets.includes(planet1) && slowPlanets.includes(planet2);
930
+ const stepSize = isSlowAspect ? 0.1 : 1; // 0.1 Tage für langsame Planeten, 1 Tag für schnelle
931
+
932
+ // Gehe vorwärts durch die Zeit
933
+ let i = 0;
934
+ while (i < daysToCheck && futureAspects.length < count) {
935
+ // Berechne den nächsten Schritt
936
+ currentDate = addDays(currentDate, stepSize);
937
+
938
+ // Berechne die Positionen der beiden Planeten
939
+ const planet1Data = getAstrologicalData(planet1, currentDate);
940
+ const planet2Data = getAstrologicalData(planet2, currentDate);
941
+
942
+ // Berechne den Winkel zwischen den Planeten
943
+ const angleDiff = Math.abs(planet1Data.longitude - planet2Data.longitude) % 360;
944
+ const normalizedAngle = Math.min(angleDiff, 360 - angleDiff);
945
+
946
+ // Prüfe, ob der Winkel exakt dem Zielwinkel entspricht (mit minimaler Toleranz)
947
+ if (Math.abs(normalizedAngle - targetAngle) <= exactOrb) {
948
+ // Prüfe, ob dies ein exakter Aspekt ist (nicht nur annähernd)
949
+ // Wir prüfen auch den vorherigen Schritt, um sicherzustellen, dass es sich um einen exakten Punkt handelt
950
+ const prevDate = subtractDays(currentDate, stepSize);
951
+ const planet1PrevData = getAstrologicalData(planet1, prevDate);
952
+ const planet2PrevData = getAstrologicalData(planet2, prevDate);
953
+
954
+ const prevAngleDiff = Math.abs(planet1PrevData.longitude - planet2PrevData.longitude) % 360;
955
+ const prevNormalizedAngle = Math.min(prevAngleDiff, 360 - prevAngleDiff);
956
+
957
+ // Wenn der Winkel sich dem Ziel nähert, ist dies ein exakter Aspekt
958
+ const currentDistance = Math.abs(normalizedAngle - targetAngle);
959
+ const prevDistance = Math.abs(prevNormalizedAngle - targetAngle);
960
+
961
+ if (currentDistance < prevDistance) {
962
+ // Dies ist ein exakter Aspekt
963
+ // Wir prüfen auch, ob dieser Aspekt bereits in der Liste ist (mit 3 Tagen Abstand)
964
+ const isDuplicate = futureAspects.some(aspect => {
965
+ const daysDiff = Math.abs(
966
+ new Date(aspect.date.year, aspect.date.month - 1, aspect.date.day) -
967
+ new Date(currentDate.year, currentDate.month - 1, currentDate.day)
968
+ ) / (1000 * 60 * 60 * 24);
969
+ return daysDiff < 3; // Mindestens 3 Tage Abstand zwischen Aspekten
970
+ });
971
+
972
+ if (!isDuplicate) {
973
+ futureAspects.push({
974
+ date: {...currentDate},
975
+ planet1: planet1,
976
+ planet2: planet2,
977
+ type: aspectType,
978
+ angle: normalizedAngle.toFixed(2),
979
+ planet1Position: `${planet1Data.sign} ${planet1Data.degreeInSign}°`,
980
+ planet2Position: `${planet2Data.sign} ${planet2Data.degreeInSign}°`
981
+ });
982
+ }
983
+ }
984
+ }
985
+
986
+ i += stepSize;
987
+ }
988
+
989
+ return futureAspects;
990
+ }