UNPKG

@eaprelsky/nocturna-wheel

Version:

A JavaScript library for rendering astrological natal charts

1,275 lines (1,125 loc) 188 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.NocturnaWheel = {})); })(this, (function (exports) { 'use strict'; /** * ServiceRegistry.js * A simple service locator/registry with all dependencies inlined. */ // Self-contained implementation class ServiceRegistry { // Private map to store service instances static #instances = new Map(); /** * Registers a service instance with the registry * @param {string} key - Service identifier * @param {Object} instance - Service instance */ static register(key, instance) { this.#instances.set(key, instance); } /** * Retrieves a service instance from the registry * @param {string} key - Service identifier * @returns {Object|undefined} The service instance, or undefined if not found */ static get(key) { return this.#instances.get(key); } /** * Checks if a service is registered * @param {string} key - Service identifier * @returns {boolean} True if the service is registered */ static has(key) { return this.#instances.has(key); } /** * Clears all registered services * Useful for testing or reinitialization */ static clear() { this.#instances.clear(); } /** * Gets or creates a basic SvgUtils-compatible instance * @returns {Object} An object with SVG utility methods */ static getSvgUtils() { if (!this.has('svgUtils')) { // Create a simple SVG utilities object const svgUtils = { svgNS: "http://www.w3.org/2000/svg", createSVGElement(tagName, attributes = {}) { const element = document.createElementNS(this.svgNS, tagName); for (const [key, value] of Object.entries(attributes)) { element.setAttribute(key, value); } return element; }, addTooltip(element, text) { const title = document.createElementNS(this.svgNS, "title"); title.textContent = text; element.appendChild(title); return element; }, pointOnCircle(centerX, centerY, radius, angle) { const radians = (angle - 90) * (Math.PI / 180); return { x: centerX + radius * Math.cos(radians), y: centerY + radius * Math.sin(radians) }; } }; this.register('svgUtils', svgUtils); } return this.get('svgUtils'); } /** * Gets or creates an IconProvider instance * @param {string} basePath - Optional base path for SVG assets * @returns {Object} The IconProvider instance */ static getIconProvider(basePath = './assets/svg/zodiac/') { if (!this.has('iconProvider')) { // Create a simple icon provider const iconProvider = { basePath: basePath, getPlanetIconPath(planetName) { return `${this.basePath}zodiac-planet-${planetName.toLowerCase()}.svg`; }, getZodiacIconPath(signName) { return `${this.basePath}zodiac-sign-${signName.toLowerCase()}.svg`; }, getAspectIconPath(aspectType) { return `${this.basePath}zodiac-aspect-${aspectType.toLowerCase()}.svg`; }, createTextFallback(svgUtils, options, text) { const { x, y, size = '16px', color = '#000000', className = 'icon-fallback' } = options; const textElement = svgUtils.createSVGElement("text", { x: x, y: y, 'text-anchor': 'middle', 'dominant-baseline': 'middle', 'font-size': size, 'class': className, 'fill': color }); textElement.textContent = text; return textElement; } }; this.register('iconProvider', iconProvider); } return this.get('iconProvider'); } /** * Initializes all core services at once * @param {Object} options - Initialization options */ static initializeServices(options = {}) { // Initialize SvgUtils this.getSvgUtils(); // Initialize IconProvider with the assets base path this.getIconProvider(options.assetBasePath || './assets/svg/zodiac/'); console.log("ServiceRegistry: Core services initialized"); } } /** * ChartConfig.js * Configuration class for the natal chart rendering. */ class ChartConfig { /** * Creates a new configuration with default settings * @param {Object} customConfig - Custom configuration to merge with defaults */ constructor(customConfig = {}) { // Astronomical data - pure positional data without styling this.astronomicalData = { ascendant: 0, // Ascendant longitude in degrees mc: 90, // Midheaven longitude in degrees latitude: 51.5, // Default latitude (London) houseSystem: "Placidus", // Default house system planets: { // Default planet positions sun: 0, moon: 0, mercury: 0, venus: 0, mars: 0, jupiter: 0, saturn: 0, uranus: 0, neptune: 0, pluto: 0 } }; // Aspect settings this.aspectSettings = { enabled: true, orb: 6, // Default orb for aspects types: { conjunction: { angle: 0, orb: 8, color: "#ff0000", enabled: true }, opposition: { angle: 180, orb: 8, color: "#0000ff", enabled: true }, trine: { angle: 120, orb: 6, color: "#00ff00", enabled: true }, square: { angle: 90, orb: 6, color: "#ff00ff", enabled: true }, sextile: { angle: 60, orb: 4, color: "#00ffff", enabled: true } // Other aspects can be added here } }; // Planet settings this.planetSettings = { enabled: true, primaryEnabled: true, // Toggle for primary (inner circle) planets secondaryEnabled: true, // Toggle for secondary (innermost circle) planets dotSize: 3, // Size of the position dot iconSize: 24, // Size of the planet icon orbs: { // Default orbs for each planet sun: 8, moon: 8, mercury: 6, venus: 6, mars: 6, jupiter: 6, saturn: 6, uranus: 4, neptune: 4, pluto: 4 }, colors: { // Default colors for each planet sun: "#ff9900", moon: "#aaaaaa", mercury: "#3399cc", venus: "#cc66cc", mars: "#cc3333", jupiter: "#9966cc", saturn: "#336633", uranus: "#33cccc", neptune: "#3366ff", pluto: "#663366" }, visible: { // Default visibility for each planet sun: true, moon: true, mercury: true, venus: true, mars: true, jupiter: true, saturn: true, uranus: true, neptune: true, pluto: true } }; // House settings - only UI related settings, no calculations this.houseSettings = { enabled: true, lineColor: "#666666", textColor: "#333333", fontSize: 10, rotationAngle: 0 // Custom rotation angle for house system }; // Zodiac settings this.zodiacSettings = { enabled: true, colors: { aries: "#ff6666", taurus: "#66cc66", gemini: "#ffcc66", cancer: "#6699cc", leo: "#ff9900", virgo: "#996633", libra: "#6699ff", scorpio: "#cc3366", sagittarius: "#cc66ff", capricorn: "#339966", aquarius: "#3399ff", pisces: "#9966cc" }, fontSize: 10 }; // Radii for different chart layers this.radius = { innermost: 90, // Innermost circle (for dual charts, transits, synastry) zodiacInner: 120, // Inner circle (aspect container) zodiacMiddle: 150, // Middle circle (house boundaries) zodiacOuter: 180, // Outer circle (zodiac ring) planet: 105, // Default planet placement radius aspectInner: 20, // Center space for aspects aspectOuter: 120, // Outer boundary for aspects houseNumberRadius: 210 // Radius for house numbers }; // SVG settings this.svg = { width: 460, height: 460, viewBox: "0 0 460 460", center: { x: 230, y: 230 } }; // Assets settings this.assets = { basePath: "./assets/", zodiacIconPath: "svg/zodiac/", planetIconPath: "svg/zodiac/" }; // Theme settings this.theme = { backgroundColor: "transparent", textColor: "#333333", lineColor: "#666666", lightLineColor: "#cccccc", fontFamily: "'Arial', sans-serif" }; // House cusps cache - will be populated by HouseCalculator this.houseCusps = []; // Merge custom config with defaults (deep merge) this.mergeConfig(customConfig); // Initialize house cusps if we have enough data this._initializeHouseCusps(); } /** * Merges custom configuration with the default configuration * @param {Object} customConfig - Custom configuration object */ mergeConfig(customConfig) { // Helper function for deep merge const deepMerge = (target, source) => { if (typeof source !== 'object' || source === null) { return source; } for (const key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { if (typeof source[key] === 'object' && source[key] !== null && typeof target[key] === 'object' && target[key] !== null) { // If both are objects, recurse target[key] = deepMerge(target[key], source[key]); } else { // Otherwise just copy target[key] = source[key]; } } } return target; }; // Apply deep merge to all top-level properties for (const key in customConfig) { if (Object.prototype.hasOwnProperty.call(customConfig, key) && Object.prototype.hasOwnProperty.call(this, key)) { this[key] = deepMerge(this[key], customConfig[key]); } } } /** * Initializes house cusps using the HouseCalculator * @private */ _initializeHouseCusps() { // Only calculate if we have the necessary data if (typeof this.astronomicalData.ascendant === 'number' && typeof this.astronomicalData.mc === 'number') { try { // Create calculator instance const houseCalculator = new HouseCalculator(); // Calculate house cusps using the current house system this.houseCusps = houseCalculator.calculateHouseCusps( this.astronomicalData.ascendant, this.astronomicalData.houseSystem, { latitude: this.astronomicalData.latitude, mc: this.astronomicalData.mc } ); } catch (error) { console.error("Failed to calculate house cusps:", error); // Set empty cusps array if calculation fails this.houseCusps = []; } } } /** * Gets settings for a specific planet * @param {string} planetName - Name of the planet * @returns {Object} - Planet settings */ getPlanetSettings(planetName) { const planetLower = planetName.toLowerCase(); return { color: this.planetSettings.colors[planetLower] || "#000000", size: this.planetSettings.size, visible: this.planetSettings.visible[planetLower] !== false }; } /** * Gets settings for a specific aspect * @param {string} aspectType - Type of aspect * @returns {Object} - Aspect settings */ getAspectSettings(aspectType) { const aspectLower = aspectType.toLowerCase(); return this.aspectSettings.types[aspectLower] || { angle: 0, orb: this.aspectSettings.orb, color: "#999999", enabled: true }; } /** * Gets settings for a specific zodiac sign * @param {string} signName - Name of the zodiac sign * @returns {Object} - Zodiac sign settings */ getZodiacSettings(signName) { const signLower = signName.toLowerCase(); return { color: this.zodiacSettings.colors[signLower] || "#666666", fontSize: this.zodiacSettings.fontSize }; } /** * Updates aspect settings * @param {Object} settings - New aspect settings */ updateAspectSettings(settings) { this.aspectSettings = { ...this.aspectSettings, ...settings }; } /** * Updates planet settings * @param {Object} settings - New planet settings */ updatePlanetSettings(settings) { this.planetSettings = { ...this.planetSettings, ...settings }; } /** * Updates house settings (visual settings only) * @param {Object} settings - New house settings */ updateHouseSettings(settings) { this.houseSettings = { ...this.houseSettings, ...settings }; } /** * Updates zodiac settings * @param {Object} settings - New zodiac settings */ updateZodiacSettings(settings) { this.zodiacSettings = { ...this.zodiacSettings, ...settings }; } /** * Sets radius for a specific layer * @param {string} layerName - Layer name * @param {number} value - Radius value */ setRadius(layerName, value) { if (Object.prototype.hasOwnProperty.call(this.radius, layerName)) { this.radius[layerName] = value; } } /** * Toggles the visibility of a planet * @param {string} planetName - Name of the planet * @param {boolean} visible - Whether the planet should be visible */ togglePlanetVisibility(planetName, visible) { if (this.planetSettings && this.planetSettings.visible) { this.planetSettings.visible[planetName] = visible; } } /** * Toggles the visibility of houses * @param {boolean} visible - Whether houses should be visible */ toggleHousesVisibility(visible) { this.houseSettings.enabled = visible; } /** * Toggles the visibility of aspects * @param {boolean} visible - Whether aspects should be visible */ toggleAspectsVisibility(visible) { this.aspectSettings.enabled = visible; } /** * Toggles the visibility of primary planets (inner circle) * @param {boolean} visible - Whether primary planets should be visible */ togglePrimaryPlanetsVisibility(visible) { this.planetSettings.primaryEnabled = visible; } /** * Toggles the visibility of secondary planets (innermost circle) * @param {boolean} visible - Whether secondary planets should be visible */ toggleSecondaryPlanetsVisibility(visible) { this.planetSettings.secondaryEnabled = visible; } /** * Sets the current house system and recalculates house cusps * @param {string} systemName - Name of the house system to use * @returns {boolean} - Success status */ setHouseSystem(systemName) { // Update the house system name this.astronomicalData.houseSystem = systemName; // Recalculate house cusps with new system this._initializeHouseCusps(); return true; } /** * Gets the current house cusps * @returns {Array} - Array of house cusps */ getHouseCusps() { // If we don't have house cusps data yet, calculate it if (!this.houseCusps || this.houseCusps.length === 0) { this._initializeHouseCusps(); } // Ensure we're returning the correct format - convert to legacy format if needed if (this.houseCusps.length > 0 && typeof this.houseCusps[0] === 'number') { return this.houseCusps.map(longitude => ({ lon: longitude })); } return this.houseCusps; } /** * Sets the Ascendant position and recalculates house cusps * @param {number} ascendant - Ascendant longitude in degrees * @returns {boolean} - Success status */ setAscendant(ascendant) { if (typeof ascendant !== 'number' || ascendant < 0 || ascendant >= 360) { return false; } this.astronomicalData.ascendant = ascendant; this._initializeHouseCusps(); return true; } /** * Sets the Midheaven position and recalculates house cusps * @param {number} mc - Midheaven longitude in degrees * @returns {boolean} - Success status */ setMidheaven(mc) { if (typeof mc !== 'number' || mc < 0 || mc >= 360) { return false; } this.astronomicalData.mc = mc; this._initializeHouseCusps(); return true; } /** * Sets the geographic latitude and recalculates house cusps * @param {number} latitude - Geographic latitude in degrees * @returns {boolean} - Success status */ setLatitude(latitude) { if (typeof latitude !== 'number' || latitude < -90 || latitude > 90) { return false; } this.astronomicalData.latitude = latitude; this._initializeHouseCusps(); return true; } /** * Sets a planet's position * @param {string} planetName - Name of the planet * @param {number} longitude - Longitude in degrees * @returns {boolean} - Success status */ setPlanetPosition(planetName, longitude) { const planetLower = planetName.toLowerCase(); if (typeof longitude !== 'number' || longitude < 0 || longitude >= 360) { return false; } if (this.astronomicalData.planets.hasOwnProperty(planetLower)) { this.astronomicalData.planets[planetLower] = longitude; return true; } return false; } /** * Gets a planet's position * @param {string} planetName - Name of the planet * @returns {number|null} - Planet longitude or null if not found */ getPlanetPosition(planetName) { const planetLower = planetName.toLowerCase(); if (this.astronomicalData.planets.hasOwnProperty(planetLower)) { return this.astronomicalData.planets[planetLower]; } return null; } /** * Gets the current house system * @returns {string} - Name of the current house system */ getHouseSystem() { return this.astronomicalData.houseSystem; } /** * Gets the available house systems by creating a temporary calculator * @returns {Array} - Array of available house system names */ getAvailableHouseSystems() { const calculator = new HouseCalculator(); return calculator.getAvailableHouseSystems(); } } /** * SvgUtils.js * Utility class for working with SVG elements */ class SvgUtils { constructor() { this.svgNS = "http://www.w3.org/2000/svg"; } /** * Creates an SVG element with the specified tag * @param {string} tagName - Name of the SVG tag * @param {Object} attributes - Object with attributes to set * @returns {Element} Created SVG element */ createSVGElement(tagName, attributes = {}) { const element = document.createElementNS(this.svgNS, tagName); for (const [key, value] of Object.entries(attributes)) { element.setAttribute(key, value); } return element; } /** * Adds a tooltip (title) to an SVG element * @param {Element} element - SVG element * @param {string} text - Tooltip text * @returns {Element} The element with tooltip */ addTooltip(element, text) { const title = document.createElementNS(this.svgNS, "title"); title.textContent = text; element.appendChild(title); return element; } /** * Calculates coordinates of a point on a circle * @param {number} centerX - X coordinate of the center * @param {number} centerY - Y coordinate of the center * @param {number} radius - Circle radius * @param {number} angle - Angle in degrees * @returns {Object} Object with x and y coordinates */ pointOnCircle(centerX, centerY, radius, angle) { // Convert angle to radians (accounting for 0 starting at the top) const radians = (angle - 90) * (Math.PI / 180); return { x: centerX + radius * Math.cos(radians), y: centerY + radius * Math.sin(radians) }; } } /** * SVGManager.js * Handles the creation, management, and querying of the main SVG element and its layer groups. */ class SVGManager { /** * Constructor * @param {Object} options - Manager options * @param {SvgUtils} [options.svgUtils] - SvgUtils instance (optional, will use from registry if not provided) */ constructor(options = {}) { this.svgNS = "http://www.w3.org/2000/svg"; this.svg = null; // Reference to the main SVG element this.groups = {}; // References to the layer groups (g elements) // Use injected svgUtils or get from registry this.svgUtils = options.svgUtils || ServiceRegistry.getSvgUtils(); // Define standard group order (bottom to top) this.groupOrder = [ 'zodiac', 'houseDivisions', 'aspects', // Render aspects below planets and houses 'primaryPlanets', // Inner circle planets 'secondaryPlanets', // Innermost circle planets 'houses' // House numbers on top // Add other groups if needed, e.g., 'tooltips' ]; } /** * Initializes the main SVG element within a container. * @param {string|Element} containerSelector - ID/CSS selector of the container element or the element itself. * @param {Object} options - SVG attributes (e.g., viewBox, width, height, class, preserveAspectRatio). * @returns {SVGElement | null} The created SVG element or null if container not found. */ initialize(containerSelector, options = {}) { // Handle both string selectors and DOM elements let container; if (typeof containerSelector === 'string') { container = document.querySelector(containerSelector); if (!container) { console.error(`SVGManager: Container not found with selector: ${containerSelector}`); return null; } } else if (containerSelector instanceof Element || containerSelector instanceof HTMLElement) { container = containerSelector; } else { console.error(`SVGManager: Invalid container. Expected string selector or DOM element.`); return null; } // Clear container container.innerHTML = ''; // Default options const defaultOptions = { width: "100%", height: "100%", viewBox: "0 0 460 460", // Default size, should match ChartConfig ideally preserveAspectRatio: "xMidYMid meet", class: "nocturna-wheel-svg" // Default class }; const svgOptions = { ...defaultOptions, ...options }; // Create SVG element this.svg = document.createElementNS(this.svgNS, "svg"); // Set attributes for (const [key, value] of Object.entries(svgOptions)) { this.svg.setAttribute(key, value); } // Append to container container.appendChild(this.svg); console.log("SVGManager: SVG initialized"); return this.svg; } /** * Creates the standard layer groups within the SVG in the predefined order. */ createStandardGroups() { if (!this.svg) { console.error("SVGManager: Cannot create groups, SVG not initialized."); return; } // Clear existing groups before creating new ones (or check if they exist) this.groups = {}; // Remove existing group elements from SVG if any this.svg.querySelectorAll('g').forEach(g => g.remove()); console.log("SVGManager: Creating standard groups:", this.groupOrder); this.groupOrder.forEach(groupName => { this.createGroup(groupName); }); // Create a legacy 'planets' group for backward compatibility // This will be deprecated in future versions this.createGroup('planets'); } /** * Creates a named group (<g>) element and appends it to the SVG. * If the group already exists, it returns the existing group. * @param {string} name - The name (and ID) for the group. * @returns {SVGElement | null} The created or existing group element, or null if SVG not initialized. */ createGroup(name) { if (!this.svg) { console.error(`SVGManager: Cannot create group '${name}', SVG not initialized.`); return null; } if (this.groups[name]) { return this.groups[name]; // Return existing group } const group = document.createElementNS(this.svgNS, "g"); group.setAttribute("id", `group-${name}`); // Set ID for easy debugging/selection group.setAttribute("class", `svg-group svg-group-${name}`); // Add class this.svg.appendChild(group); // Append to SVG (order matters based on creation sequence) this.groups[name] = group; // console.log(`SVGManager: Created group '${name}'`); return group; } /** * Retrieves a previously created group element by name. * @param {string} name - The name of the group. * @returns {SVGElement | null} The group element or null if not found or not initialized. */ getGroup(name) { if (!this.svg) { console.warn(`SVGManager: Cannot get group '${name}', SVG not initialized.`); return null; } // For backward compatibility, map 'planets' to 'primaryPlanets' if (name === 'planets') { console.warn('SVGManager: Using deprecated "planets" group. Use "primaryPlanets" or "secondaryPlanets" instead.'); name = 'primaryPlanets'; } if (!this.groups[name]) { console.warn(`SVGManager: Group '${name}' not found. Creating it.`); // Attempt to create if missing, might indicate an issue elsewhere return this.createGroup(name); } return this.groups[name]; } /** * Retrieves all created group elements. * @returns {Object<string, SVGElement>} An object mapping group names to their SVGElement references. */ getAllGroups() { return this.groups; } /** * Returns the main SVG element. * @returns {SVGElement | null} The main SVG element or null if not initialized. */ getSVG() { return this.svg; } } // End of SVGManager class /** * BaseRenderer.js * Self-contained base class for all renderers. * This version has all dependencies inlined to avoid import issues. */ // Define a completely self-contained renderer base class class BaseRenderer { /** * Constructor * @param {Object} options - Renderer options. * @param {string} options.svgNS - SVG namespace. * @param {Object} options.config - Chart configuration object. * @param {Object} options.svgUtils - SVG utility service. */ constructor(options) { if (!options || !options.svgNS || !options.config) { throw new Error(`${this.constructor.name}: Missing required options (svgNS, config)`); } // Store base options this.svgNS = options.svgNS; this.config = options.config; this.options = options; // Use the provided svgUtils or create minimal internal SVG utilities this.svgUtils = options.svgUtils || { // Store SVG namespace svgNS: this.svgNS, // Create SVG element createSVGElement: (tagName, attributes = {}) => { const element = document.createElementNS(this.svgNS, tagName); for (const [key, value] of Object.entries(attributes)) { element.setAttribute(key, value); } return element; }, // Add tooltip to element addTooltip: (element, text) => { const title = document.createElementNS(this.svgNS, "title"); title.textContent = text; element.appendChild(title); return element; }, // Calculate point on circle pointOnCircle: (centerX, centerY, radius, angle) => { const radians = (angle - 90) * (Math.PI / 180); return { x: centerX + radius * Math.cos(radians), y: centerY + radius * Math.sin(radians) }; } }; // Get dimensions from config this.centerX = this.config.svg.center.x; this.centerY = this.config.svg.center.y; this.innerRadius = this.config.radius.zodiacInner; this.middleRadius = this.config.radius.zodiacMiddle; this.outerRadius = this.config.radius.zodiacOuter; } /** * Clears all child nodes of the given parent SVG group. * @param {Element} parentGroup - The SVG group to clear. */ clearGroup(parentGroup) { if (parentGroup) { parentGroup.innerHTML = ''; } } /** * Abstract render method to be implemented by subclasses. * @param {Element} parentGroup - The parent SVG group element. * @returns {Array} Array of rendered SVG elements. */ render(parentGroup) { throw new Error(`${this.constructor.name}: render() not implemented.`); } } /** * AstrologyUtils.js * Utility class for astrological calculations */ class AstrologyUtils { /** * Capitalizes the first letter of a string * @param {string} string - Input string * @returns {string} String with capitalized first letter */ static capitalizeFirstLetter(string) { return string.charAt(0).toUpperCase() + string.slice(1); } /** * Determines the house based on position and rotation angle * @param {number} position - Position in degrees (0-359) * @param {number} rotationAngle - Rotation angle of the house system * @returns {number} House number (1-12) */ static getHouseFromPosition(position, rotationAngle = 0) { // Adjust position relative to house system rotation const adjustedPosition = (position - rotationAngle + 360) % 360; // Determine house return Math.floor(adjustedPosition / 30) + 1; } /** * Returns the list of zodiac sign names * @returns {Array} Array of zodiac sign names */ static getZodiacSigns() { return [ "aries", "taurus", "gemini", "cancer", "leo", "virgo", "libra", "scorpio", "sagittarius", "capricorn", "aquarius", "pisces" ]; } /** * Returns the available house systems with descriptions * @returns {Object} Object mapping house system names to their descriptions */ static getHouseSystems() { return { "Placidus": "Most commonly used house system in Western astrology, based on time of day", "Koch": "Developed by Walter Koch, a time-based system similar to Placidus", "Campanus": "Medieval system dividing the prime vertical into equal parts", "Regiomontanus": "Similar to Campanus, but using the celestial equator instead of prime vertical", "Equal": "Divides the ecliptic into 12 equal segments of 30° each from the Ascendant", "Whole Sign": "Assigns the entire rising sign to the 1st house, with subsequent signs as houses", "Porphyry": "Simple system that divides each quadrant into three equal parts", "Topocentric": "Modern system similar to Placidus but more accurate for extreme latitudes" }; } /** * Returns the list of planets * @returns {Array} Array of planet names */ static getPlanets() { return [ "sun", "moon", "mercury", "venus", "mars", "jupiter", "saturn", "uranus", "neptune", "pluto" ]; } /** * Converts a house number to a Roman numeral * @param {number} house - House number (1-12) * @returns {string} Roman numeral */ static houseToRoman(house) { const romanNumerals = ["I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII"]; return romanNumerals[house - 1] || ''; } /** * Returns the full name of a planet in the specified language * @param {string} planetCode - Planet code (sun, moon, etc.) * @param {string} language - Language code (default: 'en') * @returns {string} Full planet name */ static getPlanetFullName(planetCode, language = 'en') { const planetNames = { en: { "sun": "Sun", "moon": "Moon", "mercury": "Mercury", "venus": "Venus", "mars": "Mars", "jupiter": "Jupiter", "saturn": "Saturn", "uranus": "Uranus", "neptune": "Neptune", "pluto": "Pluto" }, ru: { "sun": "Солнце", "moon": "Луна", "mercury": "Меркурий", "venus": "Венера", "mars": "Марс", "jupiter": "Юпитер", "saturn": "Сатурн", "uranus": "Уран", "neptune": "Нептун", "pluto": "Плутон" } }; const names = planetNames[language] || planetNames.en; return names[planetCode.toLowerCase()] || this.capitalizeFirstLetter(planetCode); } /** * Returns the full name of a zodiac sign in the specified language * @param {string} signCode - Zodiac sign code (aries, taurus, etc.) * @param {string} language - Language code (default: 'en') * @returns {string} Full zodiac sign name */ static getZodiacSignFullName(signCode, language = 'en') { const signNames = { en: { "aries": "Aries", "taurus": "Taurus", "gemini": "Gemini", "cancer": "Cancer", "leo": "Leo", "virgo": "Virgo", "libra": "Libra", "scorpio": "Scorpio", "sagittarius": "Sagittarius", "capricorn": "Capricorn", "aquarius": "Aquarius", "pisces": "Pisces" }, ru: { "aries": "Овен", "taurus": "Телец", "gemini": "Близнецы", "cancer": "Рак", "leo": "Лев", "virgo": "Дева", "libra": "Весы", "scorpio": "Скорпион", "sagittarius": "Стрелец", "capricorn": "Козерог", "aquarius": "Водолей", "pisces": "Рыбы" } }; const names = signNames[language] || signNames.en; return names[signCode.toLowerCase()] || this.capitalizeFirstLetter(signCode); } } /** * ZodiacRenderer.js * Class for rendering the zodiac circle and signs. */ class ZodiacRenderer extends BaseRenderer { /** * Constructor * @param {Object} options - Renderer options. * @param {string} options.svgNS - SVG namespace. * @param {ChartConfig} options.config - Chart configuration object. * @param {string} options.assetBasePath - Base path for assets. * @param {IconProvider} [options.iconProvider] - Icon provider service. */ constructor(options) { super(options); if (!options.assetBasePath) { throw new Error("ZodiacRenderer: Missing required option assetBasePath"); } this.iconProvider = options.iconProvider; // Store the icon provider this.signIconRadius = (this.outerRadius + this.middleRadius) / 2; this.signIconSize = 30; } /** * Renders the zodiac wheel components. * @param {Element} parentGroup - The parent SVG group element. * @returns {Array} Array of rendered SVG elements (or empty array). */ render(parentGroup) { if (!parentGroup) { console.error("ZodiacRenderer: parentGroup is null or undefined."); return []; } this.clearGroup(parentGroup); const renderedElements = []; renderedElements.push(...this.renderBaseCircles(parentGroup)); renderedElements.push(...this.renderDivisionLines(parentGroup)); renderedElements.push(...this.renderZodiacSigns(parentGroup)); console.log("ZodiacRenderer: Rendering complete."); return renderedElements; } /** * Renders the base circles for the chart layout. * @param {Element} parentGroup - The parent SVG group. * @returns {Array<Element>} Array containing the created circle elements. */ renderBaseCircles(parentGroup) { const elements = []; const circles = [ { r: this.outerRadius, class: "chart-outer-circle" }, { r: this.middleRadius, class: "chart-middle-circle" }, { r: this.innerRadius, class: "chart-inner-circle" } ]; circles.forEach(circleData => { const circle = this.svgUtils.createSVGElement("circle", { cx: this.centerX, cy: this.centerY, r: circleData.r, class: `zodiac-element ${circleData.class}` // Add base class }); parentGroup.appendChild(circle); elements.push(circle); }); return elements; } /** * Renders the division lines between zodiac signs. * @param {Element} parentGroup - The parent SVG group. * @returns {Array<Element>} Array containing the created line elements. */ renderDivisionLines(parentGroup) { const elements = []; for (let i = 0; i < 12; i++) { const angle = i * 30; // 30 degrees per sign // Lines span from the inner zodiac ring to the outer zodiac ring const point1 = this.svgUtils.pointOnCircle(this.centerX, this.centerY, this.middleRadius, angle); const point2 = this.svgUtils.pointOnCircle(this.centerX, this.centerY, this.outerRadius, angle); // Instead of skipping lines, use special classes for cardinal points let specialClass = ""; if (angle === 0) specialClass = "aries-point"; // Aries point (0°) else if (angle === 90) specialClass = "cancer-point"; // Cancer point (90°) else if (angle === 180) specialClass = "libra-point"; // Libra point (180°) else if (angle === 270) specialClass = "capricorn-point"; // Capricorn point (270°) const line = this.svgUtils.createSVGElement("line", { x1: point1.x, y1: point1.y, x2: point2.x, y2: point2.y, class: `zodiac-element zodiac-division-line ${specialClass}` }); parentGroup.appendChild(line); elements.push(line); } return elements; } /** * Renders the zodiac sign icons. * @param {Element} parentGroup - The parent SVG group. * @returns {Array<Element>} Array containing the created image elements. */ renderZodiacSigns(parentGroup) { const elements = []; const zodiacSigns = AstrologyUtils.getZodiacSigns(); // Assumes AstrologyUtils is available for (let i = 0; i < 12; i++) { const signName = zodiacSigns[i]; // Place icon in the middle of the sign's 30-degree sector const angle = i * 30 + 15; // Calculate position for the icon center const point = this.svgUtils.pointOnCircle(this.centerX, this.centerY, this.signIconRadius, angle); // Get icon path using IconProvider if available let iconHref; if (this.iconProvider) { iconHref = this.iconProvider.getZodiacIconPath(signName); } else { // Fallback to old path construction iconHref = `${this.options.assetBasePath}svg/zodiac/zodiac-sign-${signName}.svg`; } console.log(`Loading zodiac sign: ${iconHref}`); const icon = this.svgUtils.createSVGElement("image", { x: point.x - this.signIconSize / 2, // Offset to center the icon y: point.y - this.signIconSize / 2, width: this.signIconSize, height: this.signIconSize, href: iconHref, class: `zodiac-element zodiac-sign zodiac-sign-${signName}` // Add base and specific class }); // Fallback for missing icons icon.addEventListener('error', () => { console.warn(`Zodiac sign icon not found: ${iconHref}`); icon.setAttribute('href', ''); // Remove broken link // Create a text fallback const fallbackText = signName.substring(0, 3).toUpperCase(); // Use IconProvider's createTextFallback if available let textElement; if (this.iconProvider) { textElement = this.iconProvider.createTextFallback( this.svgUtils, { x: point.x, y: point.y,