UNPKG

@astrodraw/astrochart

Version:

A free and open-source JavaScript library for generating SVG charts to display planets in astrology.

345 lines (293 loc) 12.2 kB
import type { AstroData, LocatedPoint } from './radix' import type { Settings } from './settings' /** * Calculate position of the point on the circle. * * @param {int} cx - center x * @param {int} cy - center y * @param {int} radius * @param {double} angle - degree * * @return {{x: number, y: number}} Obj - {x:10, y:20} */ export const getPointPosition = (cx: number, cy: number, radius: number, angle: number, astrology: { SHIFT_IN_DEGREES: number }): { x: number; y: number } => { const angleInRadius = (astrology.SHIFT_IN_DEGREES - angle) * Math.PI / 180 const xPos = cx + radius * Math.cos(angleInRadius) const yPos = cy + radius * Math.sin(angleInRadius) return { x: xPos, y: yPos } } export const degreeToRadians = (degrees: number): number => degrees * Math.PI / 180 export const radiansToDegree = (radians: number): number => radians * 180 / Math.PI interface TextLocation { text: string; x: number; y: number } /** * Calculates positions of the point description * * @param {Object} point * @param {Array<String>} texts * * @return {Array<Object>} [{text:"abc", x:123, y:456}, {text:"cvb", x:456, y:852}, ...] */ export const getDescriptionPosition = function (point: { x: number; y: number }, texts: string[], astrology: { COLLISION_RADIUS: number; SYMBOL_SCALE: number }): TextLocation[] { const RATION = 1.4 const result: TextLocation[] = [] const posX = point.x + (astrology.COLLISION_RADIUS / RATION * astrology.SYMBOL_SCALE) const posY = point.y - (astrology.COLLISION_RADIUS * astrology.SYMBOL_SCALE) texts.forEach((text, idx) => { result.push({ text, x: posX, y: posY + (astrology.COLLISION_RADIUS / RATION * astrology.SYMBOL_SCALE * idx) }) }, this) return result } /** * Checks a source data * @private * * @param {Object} data * @return {{hasError: boolean, messages: string[]}} status */ export const validate = (data: AstroData): { hasError: boolean; messages: string[] } => { const status = { hasError: false, messages: [] as string[] } if (data == null) { status.messages.push('Data is not set.') status.hasError = true return status } if (data.planets == null) { status.messages.push('There is not property \'planets\'.') status.hasError = true } for (const property in data.planets) { if (data.planets.hasOwnProperty(property)) { if (!Array.isArray(data.planets[property])) { status.messages.push('The planets property \'' + property + '\' has to be Array.') status.hasError = true } } } if (data.cusps != null && !Array.isArray(data.cusps)) { status.messages.push('Property \'cusps\' has to be Array.') status.hasError = true } if (data.cusps != null && data.cusps.length !== 12) { status.messages.push('Count of \'cusps\' values has to be 12.') status.hasError = true } return status } /** * Get empty DOMElement with ID * * @param{String} elementID * @param{DOMElement} parent * @return {DOMElement} */ export const getEmptyWrapper = (parent: Element, elementID: string, _paperElementId: string): Element => { const element = document.getElementById(elementID) if (element != null) { removeChilds(element) return element } const paper = document.getElementById(_paperElementId) if (paper == null) throw new Error('Paper element should exist') const wrapper = document.createElementNS(paper.namespaceURI, 'g') wrapper.setAttribute('id', elementID) parent.appendChild(wrapper) return wrapper } /** * Remove childs * * @param{DOMElement} parent */ export const removeChilds = (parent: HTMLElement): void => { if (parent == null) { return } let last while ((last = parent.lastChild) != null) { parent.removeChild(last) } } /** * Check circle collision between two objects * * @param {Object} circle1, {x:123, y:123, r:50} * @param {Object} circle2, {x:456, y:456, r:60} * @return {boolean} */ export const isCollision = (circle1: { x: number; y: number; r: number }, circle2: { x: number; y: number; r: number }): boolean => { // Calculate the vector between the circles’ center points const vx = circle1.x - circle2.x const vy = circle1.y - circle2.y const magnitude = Math.sqrt(vx * vx + vy * vy) // circle.radius has been set to astrology.COLLISION_RADIUS; const totalRadii = circle1.r + circle2.r return magnitude <= totalRadii } /** * Places a new point in the located list * * @param {Array<Object>} locatedPoints, [{name:"Mars", x:123, y:123, r:50, ephemeris:45.96}, {name:"Sun", x:1234, y:1234, r:50, ephemeris:100.96}] * @param {Object} point, {name:"Venus", x:78, y:56, r:50, angle:15.96} * @param {Object} universe - current universe * @return {Array<Object>} locatedPoints */ export const assemble = (locatedPoints: LocatedPoint[], point: LocatedPoint, universe: { cx: number; cy: number; r: number }, astrology: Settings): LocatedPoint[] => { // first item if (locatedPoints.length === 0) { locatedPoints.push(point) return locatedPoints } if ((2 * Math.PI * universe.r) - (2 * (astrology.COLLISION_RADIUS * astrology.SYMBOL_SCALE) * (locatedPoints.length + 2)) <= 0) { if (astrology.DEBUG) console.log('Universe circumference: ' + (2 * Math.PI * universe.r) + ', Planets circumference: ' + (2 * (astrology.COLLISION_RADIUS * astrology.SYMBOL_SCALE) * (locatedPoints.length + 2))) throw new Error('Unresolved planet collision. Try change SYMBOL_SCALE or paper size.') } let hasCollision = false let locatedButInCollisionPoint locatedPoints.sort(comparePoints) for (let i = 0, ln = locatedPoints.length; i < ln; i++) { if (isCollision(locatedPoints[i], point)) { hasCollision = true locatedButInCollisionPoint = locatedPoints[i] locatedButInCollisionPoint.index = i if (astrology.DEBUG) console.log('Resolve collision: ' + locatedButInCollisionPoint.name + ' X ' + point.name) break } } if (hasCollision && locatedButInCollisionPoint != null && locatedButInCollisionPoint.index != null) { placePointsInCollision(locatedButInCollisionPoint, point) let newPointPosition = getPointPosition(universe.cx, universe.cy, universe.r, locatedButInCollisionPoint.angle, astrology) locatedButInCollisionPoint.x = newPointPosition.x locatedButInCollisionPoint.y = newPointPosition.y newPointPosition = getPointPosition(universe.cx, universe.cy, universe.r, point.angle, astrology) point.x = newPointPosition.x point.y = newPointPosition.y // remove locatedButInCollisionPoint from locatedPoints locatedPoints.splice(locatedButInCollisionPoint.index, 1) // call recursive locatedPoints = assemble(locatedPoints, locatedButInCollisionPoint, universe, astrology) locatedPoints = assemble(locatedPoints, point, universe, astrology) } else { locatedPoints.push(point) } locatedPoints.sort(comparePoints) return locatedPoints } /** * Sets the positions of two points that are in collision. * * @param {Object} p1, {..., pointer:123, angle:456} * @param {Object} p2, {..., pointer:23, angle:56} */ export const placePointsInCollision = (p1: LocatedPoint, p2: LocatedPoint): void => { const step = 1 let adjustedP1Pointer = p1.pointer === undefined ? p1.angle : p1.pointer let adjustedP2Pointer = p2.pointer === undefined ? p2.angle : p2.pointer // solving problems with zero crossing if (Math.abs(adjustedP1Pointer - adjustedP2Pointer) > 180) { adjustedP1Pointer = (adjustedP1Pointer + 180) % 360 adjustedP2Pointer = (adjustedP2Pointer + 180) % 360 } if (adjustedP1Pointer <= adjustedP2Pointer) { p1.angle = p1.angle - step p2.angle = p2.angle + step } else if (adjustedP1Pointer >= adjustedP2Pointer) { p1.angle = p1.angle + step p2.angle = p2.angle - step } p1.angle = (p1.angle + 360) % 360 p2.angle = (p2.angle + 360) % 360 } /** * Check collision between angle and object * * @param {double} angle * @param {Array<Object>} points, [{x:456, y:456, r:60, angle:123}, ...] * @return {boolean} */ export const isInCollision = (angle: number, points: string | any[], astrology: Settings): boolean => { const deg360 = radiansToDegree(2 * Math.PI) const collisionRadius = (astrology.COLLISION_RADIUS * astrology.SYMBOL_SCALE) / 2 let result = false for (let i = 0, ln = points.length; i < ln; i++) { if (Math.abs(points[i].angle - angle) <= collisionRadius || (deg360 - Math.abs(points[i].angle - angle)) <= collisionRadius) { result = true break } } return result } interface InitialEndPosition { startX: number startY: number endX: number endY: number } /** * Calculates positions of the dashed line passing through the obstacle. * * * @param {double} centerX * @param {double} centerY * @param {double} angle - line angle * @param {double} lineStartRadius * @param {double} lineEndRadius * @param {double} obstacleRadius * @param {Array<Object>} obstacles, [{x:456, y:456, r:60, angle:123}, ...] * * @return {Array<any>} [{startX:1, startY:1, endX:4, endY:4}, {startX:6, startY:6, endX:8, endY:8}] */ export const getDashedLinesPositions = (centerX: number, centerY: number, angle: number, lineStartRadius: number, lineEndRadius: number, obstacleRadius: number, obstacles: LocatedPoint[], astrology: Settings): InitialEndPosition[] => { let startPos let endPos const result = [] if (isInCollision(angle, obstacles, astrology)) { startPos = getPointPosition(centerX, centerY, lineStartRadius, angle, astrology) endPos = getPointPosition(centerX, centerY, obstacleRadius - (astrology.COLLISION_RADIUS * astrology.SYMBOL_SCALE), angle, astrology) result.push({ startX: startPos.x, startY: startPos.y, endX: endPos.x, endY: endPos.y }) // the second part of the line when is space if ((obstacleRadius + 2 * (astrology.COLLISION_RADIUS * astrology.SYMBOL_SCALE)) < lineEndRadius) { startPos = getPointPosition(centerX, centerY, obstacleRadius + 2 * (astrology.COLLISION_RADIUS * astrology.SYMBOL_SCALE), angle, astrology) endPos = getPointPosition(centerX, centerY, lineEndRadius, angle, astrology) result.push({ startX: startPos.x, startY: startPos.y, endX: endPos.x, endY: endPos.y }) } } else { startPos = getPointPosition(centerX, centerY, lineStartRadius, angle, astrology) endPos = getPointPosition(centerX, centerY, lineEndRadius, angle, astrology) result.push({ startX: startPos.x, startY: startPos.y, endX: endPos.x, endY: endPos.y }) } return result } /** * Calculate ruler positions. * * @param {Double} centerX * @param {Double} centerY * @param {Double} startRadius * @param {Double} endRadius * @param {Boolean} startAngle * * @return {Array<Object>} [ {startX:1,startY:2, endX:3, endX:4 }, ...] */ export const getRulerPositions = (centerX: number, centerY: number, startRadius: number, endRadius: number, startAngle: number, astrology: { SHIFT_IN_DEGREES: number }): InitialEndPosition[] => { const result = [] const rayRadius = endRadius const halfRayRadius = (startRadius <= endRadius) ? rayRadius - (Math.abs(endRadius - startRadius) / 2) : rayRadius + (Math.abs(endRadius - startRadius) / 2) for (let i = 0, start = 0, step = 5; i < 72; i++) { const angle = start + startAngle const startPos = getPointPosition(centerX, centerY, startRadius, angle, astrology) const endPos = getPointPosition(centerX, centerY, (i % 2 === 0 ? rayRadius : halfRayRadius), angle, astrology) result.push({ startX: startPos.x, startY: startPos.y, endX: endPos.x, endY: endPos.y }) start += step } return result } /** * Compare two points * * @param {Object} pointA, {name:"Venus", x:78, y:56, r:50, angle:15.96} * @param {Object} pointB, {name:"Mercury", x:78, y:56, r:50, angle:20.26} * @return */ export const comparePoints = (pointA: { angle: number }, pointB: { angle: number }): number => { return pointA.angle - pointB.angle }