diff --git a/backend/core/moon_time/calc.py b/backend/core/moon_time/calc.py deleted file mode 100644 index 5ca1093..0000000 --- a/backend/core/moon_time/calc.py +++ /dev/null @@ -1,96 +0,0 @@ -from copy import deepcopy -import numpy as np -from .utils import ( - find_moon_idx, - angle_btw_moon_stars, - geo_estimate_by_stars, - angle_error, -) -from core.positioning.calc import stars_convert, calc_z -from core.positioning.find_z.utils.math import minimize -from core.positioning.top_point.methods.matrix_inverse_normalized import intersection - - -from core.astro_coord.calc import get_RaDecs_by_names -from core.astro_coord.data import starZH2EN, solar_bodies - -async def calc( - photo: dict, - approx_timestamp: float, - scope_days: float, - is_fix_gravity: bool = False, - is_fix_refraction: bool = False, -) -> float: - """ - 根据星星(含月)相互角距计算对应时间 - - params: - photo: 数据,第一个是月亮: - stars: list, star points - lines: (n, 2, 2), plumb lines - approx_timestamp: 大致时间戳 - scope_days: 日期搜索范围大小(单位天) - is_fix_gravity: 是否修正重力 - is_fix_refraction: 是否修正大气折射 - return: - float 返回对应时间戳 - """ - - # 数据转换 - moon_idx = find_moon_idx(photo["stars"]) - if moon_idx == -1: - raise ValueError("没有找到月亮数据") - # 获取灭点、焦距 - points, hour_decs, _ = stars_convert( - [photo["stars"][i] for i in range(len(photo["stars"])) if i != moon_idx] - ) # 剔除月的数据 - try: - top_point = intersection(np.array(photo["lines"])) - except: - raise ValueError("无法找到灭点") - try: - z = calc_z(points, hour_decs, top_point, is_fix_refraction) - except: - raise ValueError("无法计算焦距") - # 获取月与各星相互角距作为目标值 - points, _, star_names = stars_convert(photo["stars"]) - target_angles = angle_btw_moon_stars(points, moon_idx, z) - - # 提前获取太阳系外天体的赤经赤纬(用于后续同步计算) - fixed_star_names = [] - for star_name in star_names: - operate_name = star_name - if operate_name in starZH2EN: - operate_name = starZH2EN[operate_name] - operate_name = operate_name.lower() - if operate_name not in solar_bodies: - fixed_star_names.append(operate_name) - pre_fetched_ra_decs = await get_RaDecs_by_names(fixed_star_names) - - # 根据星星信息计算大致日期下的地理坐标 - geo_estimate = await geo_estimate_by_stars( - deepcopy(photo), approx_timestamp, moon_idx, is_fix_gravity, is_fix_refraction - ) - - s_per_day = 86400 - - def optimize_loop(): - # 将scope_days划分为每20天一个区间,对每个区间使用三段二分法minimize搜索最小误差,最后返回最小误差对应的时间 - min_time = int(approx_timestamp - (scope_days * s_per_day) / 2) - max_time = int(approx_timestamp + (scope_days * s_per_day) / 2) - min_error = float("inf") - opt_time = 0 - opt_func = lambda timestamp: angle_error( - timestamp, approx_timestamp, star_names, geo_estimate, moon_idx, target_angles, pre_fetched_ra_decs - ) - for left_i in range(min_time, max_time, 20 * s_per_day): - right_i = min(left_i + 20 * s_per_day, max_time) - opt_time_single = minimize(opt_func, left_i, right_i, 1, 100) - time_error = opt_func(opt_time_single) - if time_error < min_error: - min_error = time_error - opt_time = opt_time_single - return opt_time - - import asyncio - return await asyncio.to_thread(optimize_loop) diff --git a/backend/core/moon_time/utils.py b/backend/core/moon_time/utils.py deleted file mode 100644 index cc68a07..0000000 --- a/backend/core/moon_time/utils.py +++ /dev/null @@ -1,154 +0,0 @@ -import numpy as np -import astronomy as ast -from core.utils import wrap_angle_in_deg -from core.positioning.locator.utils.math import vector_angle -from core.positioning.calc import calc_geo as geo_calc -from core.astro_coord.calc import get_HaDecs_by_names, get_HaDecs_sync -from core.positioning.locator.utils.math import sph2cart -from core.positioning.calc import stars_convert - - -def find_moon_idx(stars: list[dict]) -> int: - """ - 找到月亮的索引 - - params: - stars: star points - return: - 返回月亮的索引 - """ - - for i, star in enumerate(stars): - if star["name"] in ["月", "月亮", "月球", "moon"]: - return i - return -1 - - -def angle_btw_moon_stars(points: np.ndarray, moon_idx: int, z: float) -> np.ndarray: - """ - 计算月亮与星星的角距 - - params: - points: (n, 2), star points - moon_idx: 月的索引 - z: 焦距 - return: - 返回角距列表 - """ - - points_3d = np.concatenate([points, np.ones((len(points), 1)) * z], axis=1) - target_angles = vector_angle(points_3d[moon_idx], points_3d) - return target_angles - - -async def geo_estimate_by_stars( - data: dict, - approx_timestamp: float, - moon_idx: int, - is_fix_gravity: bool, - is_fix_refraction: bool, -) -> dict: - """ - 根据星星信息计算大致日期下的地理坐标 - - params: - data: a dict including: - stars: (n, 2), star points - lines: (n, 2, 2), plumb lines - approx_timestamp: 大致时间戳 - moon_idx: 月的索引 - is_fix_gravity: 是否修正重力 - is_fix_refraction: 是否修正大气折射 - return: - 返回地理位置列表 - """ - - # 数据转换 - _, _, star_names = stars_convert(data["stars"]) - # 根据大致日期获取各星时角赤纬 - approx_star_HaDecs, is_success = await get_HaDecs_by_names(star_names, approx_timestamp) - if is_success != "success": - raise ValueError("无法获取天体坐标") - for i, star_name in enumerate(star_names): - data["stars"][i]["lon"] = np.deg2rad( - 360 - approx_star_HaDecs[star_name][0] * 15 # type: ignore - ) - data["stars"][i]["lat"] = np.deg2rad(approx_star_HaDecs[star_name][1]) # type: ignore - # 计算大致日期下的地理坐标 - data["stars"].pop(moon_idx) # 剔除月的数据 - try: - geo_estimate = geo_calc( - data, - is_fix_refraction=is_fix_refraction, - is_fix_gravity=is_fix_gravity, - ) - except: - raise ValueError("无法计算地理位置") - return geo_estimate - - -def s2sidereal_days(s: float) -> float: - """ - 将秒转换为恒星天 - - params: - s: number 秒 - return: - number 恒星天 - """ - - s_in_sidereal_day = 86164.0905 - return s / s_in_sidereal_day - - -def angle_error( - timestamp: float, - approx_timestamp: float, - star_names: list[str], - geo_estimate: dict, - moon_idx: int, - target_angles: np.ndarray, - pre_fetched_ra_decs: dict, -): - """ - 误差函数,计算每一天的星星(含月)相互角距,输出误差 - - params: - timestamp: 时间戳 - approx_timestamp: 大致日期 - star_names: 星星名字列表 - geo_estimate: 地理坐标 - moon_idx: 月的索引 - target_angles: 目标角距 - return: - number 返回星角距误差和 - """ - - # 使用恒星日周期快速计算该时间下观测者地理坐标 - observer_lon = ( - np.rad2deg(geo_estimate["lon"]) - - s2sidereal_days(timestamp - approx_timestamp) * 360 - ) - observer_lon = wrap_angle_in_deg(observer_lon) - # 得到该时间所计算的观测者地理坐标 - observer = ast.Observer(np.rad2deg(geo_estimate["lat"]), observer_lon, 0) - # 获取该时间、该地理坐标下的天体时角赤纬 - star_HaDecs, _ = get_HaDecs_sync(star_names, timestamp, observer, pre_fetched_ra_decs) - moon_HaDec = star_HaDecs[star_names[moon_idx]] - assert moon_HaDec[0] and moon_HaDec[1] - moon_vec = np.array( - sph2cart(np.deg2rad(moon_HaDec[0] * 15), np.deg2rad(moon_HaDec[1])) - ) - # 计算每颗星星(不包含月)与月的角距误差 - error = 0 - for i, star_name in enumerate(star_names): - if i == moon_idx: - continue - star_HaDec = star_HaDecs[star_name] - assert star_HaDec[0] and star_HaDec[1] - star_vec = np.array( - sph2cart(np.deg2rad(star_HaDec[0] * 15), np.deg2rad(star_HaDec[1])) - ) - angle = vector_angle(star_vec, moon_vec) - error += (np.rad2deg(angle - target_angles[i])) ** 2 - return error diff --git a/backend/main.py b/backend/main.py index 5ac8293..4b1c373 100644 --- a/backend/main.py +++ b/backend/main.py @@ -2,7 +2,7 @@ from fastapi.middleware.cors import CORSMiddleware from contextlib import asynccontextmanager import uvicorn.config -from routers import astro_coord, moon_time, positioning, astrometry +from routers import positioning, astrometry from routers.limiter import limiter from slowapi import _rate_limit_exceeded_handler from slowapi.errors import RateLimitExceeded @@ -35,8 +35,6 @@ async def lifespan(app: FastAPI): # 分发路由 app.include_router(positioning.router, prefix="/api/positioning", tags=["positioning"]) -app.include_router(astro_coord.router, prefix="/api/astrocoord", tags=["astrocoord"]) -app.include_router(moon_time.router, prefix="/api/moontime", tags=["moontime"]) app.include_router(astrometry.router, prefix="/api/astrometry", tags=["astrometry"]) diff --git a/backend/routers/astro_coord.py b/backend/routers/astro_coord.py deleted file mode 100644 index 94c69df..0000000 --- a/backend/routers/astro_coord.py +++ /dev/null @@ -1,33 +0,0 @@ -from fastapi import APIRouter, Request -from schemas import astro_coord -from core.astro_coord.calc import get_HaDecs_by_names - -from .limiter import limiter -from config import LIGHT_RATE_LIMIT - -router = APIRouter() - - -@router.post("", response_model=astro_coord.AstroCoord) -@limiter.limit(LIGHT_RATE_LIMIT) -async def http_astro_coord(request: Request, data: astro_coord.AstroTime): - """ - 获取指定时间的天体时角和赤纬 - - Params: - request: Request, slowapi必需 - data: - star_names: list[str] 天体名称列表 - timestamp: number 时间戳 - - Returns: - a dict: - detail: str, 计算情况 - haDecs: list[float | None] 各天体的时角和赤纬列表(角度) - """ - - haDecs, detail = await get_HaDecs_by_names(data.starNames, data.timestamp) - return { - "detail": detail, - "haDecs": haDecs, - } diff --git a/backend/routers/moon_time.py b/backend/routers/moon_time.py deleted file mode 100644 index 89ebd1d..0000000 --- a/backend/routers/moon_time.py +++ /dev/null @@ -1,50 +0,0 @@ -from fastapi import APIRouter, Request -from core.moon_time.calc import calc -from schemas import moon_time -import asyncio - -from .limiter import limiter -from config import HEAVY_RATE_LIMIT - -router = APIRouter() - - -@router.post("", response_model=moon_time.MoonTimeResponse) -@limiter.limit(HEAVY_RATE_LIMIT) -async def http_time_by_moon(request: Request, data: moon_time.MoonTimeRequest): - """ - 通过月亮与星星的相对位置计算时间 - - Params: - request: Request, slowapi必需 - data: - photo: a dict including: - stars: list, star points - lines: (n, 2, 2), plumb lines - approxTimestamp: number, approximate timestamp - scopeDays: number, date search range size (in days) - isFixRefraction: whether to fix refraction - isFixGravity: whether to fix gravity - - Returns: - a dict: - time: float, the timestamp - detail: str, the detail of the calculation - """ - detail = "success" - try: - time = await asyncio.to_thread( - calc, - data.photo.model_dump(), - data.approxTimestamp, - data.scopeDays, - data.isFixGravity, - data.isFixRefraction, - ) - except ValueError as e: - time = None - detail = str(e) - except Exception: - time = None - detail = "时间计算过程中发生未知矩阵运算错误,请检查传入点位是否合理" - return {"time": time, "detail": detail} diff --git a/backend/schemas/astro_coord.py b/backend/schemas/astro_coord.py deleted file mode 100644 index 5f16056..0000000 --- a/backend/schemas/astro_coord.py +++ /dev/null @@ -1,30 +0,0 @@ -from pydantic import BaseModel, Field -from config import MAX_NUM_OF_STARS - - -class AstroTime(BaseModel): - starNames: list[str] = Field(..., title="Star Names", max_length=MAX_NUM_OF_STARS) - timestamp: float = Field(..., title="Timestamp", gt=0) - - model_config = { - "json_schema_extra": { - "examples": [ - { - "starNames": [ - "北落师门", - "火鸟六", - "土司空", - "室宿一", - "危宿三", - "月球", - ], - "timestamp": 1728921600, - } - ] - } - } - - -class AstroCoord(BaseModel): - detail: str - haDecs: dict[str, list[float | None]] diff --git a/backend/schemas/moon_time.py b/backend/schemas/moon_time.py deleted file mode 100644 index 36b4246..0000000 --- a/backend/schemas/moon_time.py +++ /dev/null @@ -1,86 +0,0 @@ -from pydantic import BaseModel, Field -from .base import PointLines, Stars -from config import MAX_NUM_OF_STARS, MAX_MOON_SEARCH_RANGE - - -class MoonPointLines(PointLines): - # 对于标月定时,星体数量至少为4 - stars: list[Stars] = Field( - ..., title="List of stars", min_length=4, max_length=MAX_NUM_OF_STARS - ) - - -class MoonTimeRequest(BaseModel): - photo: MoonPointLines - approxTimestamp: float = Field(..., title="Approximate timestamp", gt=0) - scopeDays: float = Field(..., title="Scope days", gt=0, le=MAX_MOON_SEARCH_RANGE) - isFixRefraction: bool = False - isFixGravity: bool = False - - model_config = { - "json_schema_extra": { - "examples": [ - { - "photo": { - "stars": [ - { - "x": -456, - "y": 226, - "name": "北落师门", - "lat": -0.514698517559122, - "lon": 1.4148802469500708, - }, - { - "x": -1771, - "y": 226, - "name": "火鸟六", - "lat": -0.7359784771918016, - "lon": 1.8010525244668565, - }, - { - "x": -1190, - "y": -1129, - "name": "土司空", - "lat": -0.3115297329153774, - "lon": 1.8766165544319517, - }, - { - "x": 1240, - "y": -1817, - "name": "室宿一", - "lat": 0.26775668134988717, - "lon": 1.4453427873820661, - }, - { - "x": 1879, - "y": -685, - "name": "危宿三", - "lat": 0.17437230642688134, - "lon": 1.0936691241925116, - }, - { - "x": 287, - "y": -528, - "name": "月", - "lat": -0.15740097203546588, - "lon": 1.4272146342180179, - }, - ], - "lines": [ - [[0.0, 0.0], [0.0, 1.0]], - [[0.0, -5196.15], [1.0, -5196.15]], - ], - }, - "approxTimestamp": 1727712000, - "scopeDays": 365, - "isFixRefraction": False, - "isFixGravity": False, - } - ] - } - } - - -class MoonTimeResponse(BaseModel): - detail: str - time: float | None diff --git a/backend/tests/test_astro_coord.py b/backend/tests/test_astro_coord.py index ec5d243..7322a4a 100644 --- a/backend/tests/test_astro_coord.py +++ b/backend/tests/test_astro_coord.py @@ -39,25 +39,3 @@ async def run_test(): assert result[star_name] == pytest.approx(target[star_name]) assert end_time - start_time < 0.05 asyncio.run(run_test()) - - -def test_remote(): - url = f"{BACKEND_API_BASEURL}/astrocoord" - data = {"starNames": star_names, "timestamp": timestamp} - resp = httpx.post(url, json=data) - assert resp.status_code == 200 - result = resp.json() - assert result["detail"] == "success" - for star_name in star_names: - assert result["haDecs"][star_name] == pytest.approx(target[star_name]) - - # 验证缓存功能 - start_time = time.time() - resp = httpx.post(url, json=data) - end_time = time.time() - assert resp.status_code == 200 - result = resp.json() - assert result["detail"] == "success" - for star_name in star_names: - assert result["haDecs"][star_name] == pytest.approx(target[star_name]) - assert end_time - start_time < 0.05 diff --git a/backend/tests/test_moon_time.py b/backend/tests/test_moon_time.py deleted file mode 100644 index 4e3801e..0000000 --- a/backend/tests/test_moon_time.py +++ /dev/null @@ -1,85 +0,0 @@ -import httpx -import pytest -import datetime -from core.moon_time.calc import calc -from config import BACKEND_API_BASEURL - - -photo = { - "stars": [ - { - "x": -456, - "y": 226, - "name": "北落师门", - "lat": -0.514698517559122, - "lon": 1.4148802469500708, - }, - { - "x": -1771, - "y": 226, - "name": "火鸟六", - "lat": -0.7359784771918016, - "lon": 1.8010525244668565, - }, - { - "x": -1190, - "y": -1129, - "name": "土司空", - "lat": -0.3115297329153774, - "lon": 1.8766165544319517, - }, - { - "x": 1240, - "y": -1817, - "name": "室宿一", - "lat": 0.26775668134988717, - "lon": 1.4453427873820661, - }, - { - "x": 1879, - "y": -685, - "name": "危宿三", - "lat": 0.17437230642688134, - "lon": 1.0936691241925116, - }, - { - "x": 287, - "y": -528, - "name": "月", - "lat": -0.15740097203546588, - "lon": 1.4272146342180179, - }, - ], - "lines": [[[0.0, 0.0], [0.0, 1.0]], [[0.0, -5196.15], [1.0, -5196.15]]], -} -is_fix_refraction = False -is_fix_gravity = False - -approx_timestamp = datetime.datetime(2024, 10, 1, 0).timestamp() # 1727712000 -scope_days = 365 - -target = datetime.datetime(2024, 10, 14, 16, tzinfo=datetime.timezone.utc).timestamp() - - -def test_local(): - import asyncio - real_timestamp = asyncio.run(calc( - photo, approx_timestamp, scope_days, is_fix_refraction, is_fix_gravity - )) - assert real_timestamp == pytest.approx(target) - - -def test_remote(): - url = f"{BACKEND_API_BASEURL}/moontime" - post_data = { - "photo": photo, - "approxTimestamp": approx_timestamp, - "scopeDays": scope_days, - "isFixRefraction": is_fix_refraction, - "isFixGravity": is_fix_gravity, - } - resp = httpx.post(url, json=post_data) - assert resp.status_code == 200 - result = resp.json() - assert result["detail"] == "success" - assert result["time"] == pytest.approx(target) diff --git a/package.json b/package.json index 5fb5010..192e2f6 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "author": "", "license": "AGPL-3.0-only", "dependencies": { + "astronomy-engine": "^2.1.19", "fabric": "^6.9.1", "fuzzysort": "^3.1.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c38b76d..26b9a5f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + astronomy-engine: + specifier: ^2.1.19 + version: 2.1.19 fabric: specifier: ^6.9.1 version: 6.9.1 @@ -487,6 +490,9 @@ packages: resolution: {integrity: sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==} engines: {node: '>=12.0.0'} + astronomy-engine@2.1.19: + resolution: {integrity: sha512-8yWKNf7UeNbH458h3sAJ6ZgAjE5jTXp/mNNRFoC20j2SHwZIjAQeEsBB2Q3uCFRaTCCJRv33K2XhkhZQMXoX6w==} + async@2.6.4: resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==} @@ -1212,7 +1218,7 @@ packages: glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me globals@17.6.0: resolution: {integrity: sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==} @@ -2338,6 +2344,7 @@ packages: tar@6.2.1: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me terser-webpack-plugin@5.6.0: resolution: {integrity: sha512-Eum+5ajkaOhf5KbM26osvv21kLD7BaGqQ1UA4Ami4arYwylmGUQTgHFpHDdmJod1q4QXa66p0to/FBKID+J1vA==} @@ -2495,6 +2502,7 @@ packages: uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true vary@1.1.2: @@ -2584,6 +2592,7 @@ packages: whatwg-encoding@2.0.0: resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} engines: {node: '>=12'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-mimetype@3.0.0: resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} @@ -3166,6 +3175,8 @@ snapshots: pvutils: 1.1.5 tslib: 2.8.1 + astronomy-engine@2.1.19: {} + async@2.6.4: dependencies: lodash: 4.17.21 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 6af511a..6f13d8b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,2 +1,4 @@ +allowBuilds: + canvas: set this to true or false onlyBuiltDependencies: - canvas diff --git a/src/css/site.css b/src/css/site.css index a6dcde7..1d1cda0 100644 --- a/src/css/site.css +++ b/src/css/site.css @@ -39,6 +39,11 @@ td:hover { width: 13%; } +.inputTable .col-pl-x, +.inputTable .col-pl-y { + width: 21%; +} + .inputTable .col-action { width: 10%; } diff --git a/src/index.html b/src/index.html index 2a1d73c..330e2e5 100644 --- a/src/index.html +++ b/src/index.html @@ -202,12 +202,47 @@

测量

″ - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
序号铅垂线的序号端点1 x端点1的横坐标端点1 y端点1的纵坐标端点2 x端点2的横坐标端点2 y端点2的纵坐标操作删除该行
1
2
+
+ `; + } + + bindPLTableRow(id) { + const x1Input = document.getElementById(`pl${id}_x1`); + const y1Input = document.getElementById(`pl${id}_y1`); + const x2Input = document.getElementById(`pl${id}_x2`); + const y2Input = document.getElementById(`pl${id}_y2`); + const removeButton = document.querySelector(`.removePlRow[data-pl-id="${id}"]`); + + const handlePLCoordInput = () => { + const x1 = parseFloat(x1Input.value); + const y1 = parseFloat(y1Input.value); + const x2 = parseFloat(x2Input.value); + const y2 = parseFloat(y2Input.value); + + if (!this.interactPhoto.movable) { + this.interactPhoto.tips.innerHTML = '请先拖拽星空照片到此处'; + return; + } + + // 确保 this.array 拥有足够的 PLLine 对象 + while (this.array.length < id) { + this.array.push(new PLLine(this.interactPhoto)); + } + + let pl = this.array[id - 1]; + + // 处理第一个端点 + if (Number.isFinite(x1) && Number.isFinite(y1)) { + if (pl.points[0]) { + pl.points[0].setRealXY(x1, y1); + } else { + let newPointID = (id - 1) * 2; + let newPoint = new PLpoint( + [x1, y1], + this.interactPhoto, + this.interactPhoto.canvas, + newPointID, + '#35dc96' + ); + pl.points.push(newPoint); + newPoint.deleter.on('mousedown', () => { + this.remove(newPoint.id); + }); + } + } + + // 处理第二个端点 + if (Number.isFinite(x2) && Number.isFinite(y2)) { + if (pl.points[1]) { + pl.points[1].setRealXY(x2, y2); + } else if (pl.points[0]) { + let newPointID = (id - 1) * 2 + 1; + let newPoint = new PLpoint( + [x2, y2], + this.interactPhoto, + this.interactPhoto.canvas, + newPointID, + '#35dc96' + ); + pl.points.push(newPoint); + newPoint.deleter.on('mousedown', () => { + this.remove(newPoint.id); + }); + } + } + + // 更新或创建线段 + if (pl.points.length === 2) { + if (pl.lineObject) { + let line = pl.lineObject.line; + line.set({ + x1: pl.points[0].coordinate[0], + y1: pl.points[0].coordinate[1], + x2: pl.points[1].coordinate[0], + y2: pl.points[1].coordinate[1], + }); + line.setCoords(); + } else { + let lineCoord = [pl.points[0].coordinate, pl.points[1].coordinate].flat(); + pl.lineObject = new LineObject(lineCoord, this.interactPhoto.canvas, '#35dc96'); + pl.addMoveLineEvent(); + } + } + + this.interactPhoto.canvas.renderAll(); + this.interactPhoto.updateCalculButton(); + }; + + if (x1Input) x1Input.oninput = handlePLCoordInput; + if (y1Input) y1Input.oninput = handlePLCoordInput; + if (x2Input) x2Input.oninput = handlePLCoordInput; + if (y2Input) y2Input.oninput = handlePLCoordInput; + + if (removeButton) { + removeButton.onclick = () => { + this.removePLTableRow(id); + }; + } + } + + updateTableRow(id) { + let pl = this.array[id - 1]; + if (!pl) return; + + const x1Input = document.getElementById(`pl${id}_x1`); + const y1Input = document.getElementById(`pl${id}_y1`); + const x2Input = document.getElementById(`pl${id}_x2`); + const y2Input = document.getElementById(`pl${id}_y2`); + + if (pl.points[0] && x1Input && y1Input) { + x1Input.value = this.round(pl.points[0].coordinate[0], 2); + y1Input.value = this.round(pl.points[0].coordinate[1], 2); + } else if (x1Input && y1Input) { + x1Input.value = ''; + y1Input.value = ''; + } + if (pl.points[1] && x2Input && y2Input) { + x2Input.value = this.round(pl.points[1].coordinate[0], 2); + y2Input.value = this.round(pl.points[1].coordinate[1], 2); + } else if (x2Input && y2Input) { + x2Input.value = ''; + y2Input.value = ''; + } + } + + clearPLTableRowData(id) { + const x1 = document.getElementById(`pl${id}_x1`); + const y1 = document.getElementById(`pl${id}_y1`); + const x2 = document.getElementById(`pl${id}_x2`); + const y2 = document.getElementById(`pl${id}_y2`); + if (x1) x1.value = ''; + if (y1) y1.value = ''; + if (x2) x2.value = ''; + if (y2) y2.value = ''; + } + + removePLTableRow(id) { + if (id <= this.num()) { + this.array[id - 1].remove(); + this.remove((id - 1) * 2); // delete first point's ID to trigger remove of the whole PLLine + return; + } + + let plInputTable = document.getElementById('plInputTable'); + if (this.tablePLRowCount() > MIN_PL_ROWS) { + plInputTable.deleteRow(id); + for (let i = id; i <= this.tablePLRowCount(); i++) { + this.renumberPLTableRow(i); + this.bindPLTableRow(i); + } + } else { + this.clearPLTableRowData(id); + this.bindPLTableRow(id); + } + } + + renumberPLTableRow(id) { + let plInputTable = document.getElementById('plInputTable'); + const row = plInputTable.rows[id]; + if (!row) return; + + row.cells[0].innerHTML = id; + row.querySelectorAll('[id]').forEach((element) => { + element.id = element.id.replace(/^pl\d+/, `pl${id}`); + }); + const removeButton = row.querySelector('.removePlRow'); + if (removeButton) removeButton.dataset.plId = id; + } + + clear() { + let plInputTable = document.getElementById('plInputTable'); + while (plInputTable.rows.length - 1 > MIN_PL_ROWS) { + plInputTable.deleteRow(plInputTable.rows.length - 1); + } + for (let i = 1; i <= this.tablePLRowCount(); i++) { + this.clearPLTableRowData(i); + const x1Input = document.getElementById(`pl${i}_x1`); + const y1Input = document.getElementById(`pl${i}_y1`); + const x2Input = document.getElementById(`pl${i}_x2`); + const y2Input = document.getElementById(`pl${i}_y2`); + if (x1Input) x1Input.oninput = null; + if (y1Input) y1Input.oninput = null; + if (x2Input) x2Input.oninput = null; + if (y2Input) y2Input.oninput = null; + } + for (let i of this.array) { + i.remove(); + } + this.array = []; + for (let i = 1; i <= this.tablePLRowCount(); i++) { + this.bindPLTableRow(i); + } this.interactPhoto.updateCalculButton(); } } diff --git a/src/interface/functions/CeleCoord.js b/src/interface/functions/CeleCoord.js index eeee803..e1e6498 100644 --- a/src/interface/functions/CeleCoord.js +++ b/src/interface/functions/CeleCoord.js @@ -1,5 +1,6 @@ -import { BACKEND_API } from '../../config.js'; -import { post, setHADE } from '../utils.js'; +import { MakeTime, Observer } from 'astronomy-engine'; +import { setHADE } from '../utils.js'; +import { getRaDecByName, getHaDecByRaDec, getHaDecInSolar } from '../AstroService.js'; // 计算天体坐标功能类 class CeleCoord { @@ -18,32 +19,41 @@ class CeleCoord { return -1; } - // 开始计算 - let [results, detail] = await post( - `${BACKEND_API}/astrocoord`, - { - starNames: starNames, - timestamp: timestamp, - }, - 'json' - ); - if (results === null || detail !== 'success') { - this.interactPhoto.tips.innerHTML = `自动计算天体坐标失败:${detail}`; - return -1; - } + this.interactPhoto.tips.innerHTML = `正在本地计算天体坐标...`; + + const astTime = MakeTime(new Date(timestamp * 1000)); + const observer = new Observer(0, 0, 0); + + try { + for (let i = 0; i < starNames.length; i++) { + const name = starNames[i]; + const info = await getRaDecByName(name); + if (!info) { + this.interactPhoto.tips.innerHTML = `无法自动计算 ${name} 坐标,请检查天体名称是否正确或网络是否连接`; + return -1; + } + + let ha, dec; + if (info.isSolar) { + [ha, dec] = getHaDecInSolar(info.name, astTime, observer); + } else { + [ha, dec] = getHaDecByRaDec(info.ra, info.dec, astTime, observer); + } + + if (!Number.isFinite(ha) || !Number.isFinite(dec)) { + this.interactPhoto.tips.innerHTML = `无法自动计算 ${name} 坐标,数值异常`; + return -1; + } - this.interactPhoto.tips.innerHTML = `自动计算天体坐标成功`; - for (let i = 0; i < starNames.length; i++) { - let [ha, dec] = results['haDecs'][starNames[i]]; - if (!Number.isFinite(ha) || !Number.isFinite(dec)) { - this.interactPhoto.tips.innerHTML = `无法自动计算${starNames[i]}坐标,请检查天体名称是否正确`; - return -1; - } else { setHADE(i + 1, 360 - ha * 15, dec); // 时角变参考时角 } + this.interactPhoto.tips.innerHTML = `自动计算天体坐标成功`; + return 0; + } catch (e) { + const errorMsg = e.message || e; + this.interactPhoto.tips.innerHTML = `自动计算天体坐标失败:${errorMsg}`; + return -1; } - - return 0; } getStarNames() { diff --git a/src/interface/functions/MoonTime.js b/src/interface/functions/MoonTime.js index 017c380..dba449f 100644 --- a/src/interface/functions/MoonTime.js +++ b/src/interface/functions/MoonTime.js @@ -1,5 +1,6 @@ import { DefaultbuttonFunctioner } from './Default.js'; import { getOriginalStars, getGlobalPLPointsCoord, post } from '../utils.js'; +import { getRaDecByName, optimizeMoonTime, vectorAngle } from '../AstroService.js'; import { BACKEND_API } from '../../config.js'; class MoonTime extends DefaultbuttonFunctioner { @@ -49,7 +50,7 @@ class MoonTime extends DefaultbuttonFunctioner { } // 计算拍摄时间 - calc(stars, globalPLsPointsCoord) { + async calc(stars, globalPLsPointsCoord) { // 开始计算 this.interactPhoto.buttonFunctioner = this; this.interactPhoto.tips.innerHTML = `计算中...`; @@ -60,52 +61,98 @@ class MoonTime extends DefaultbuttonFunctioner { let approxTimestamp = this.interactPhoto.getTimestamp(); let scopeDays = parseFloat(document.getElementById('setTimeScope').value); - // 计算拍摄时间 - post( - `${BACKEND_API}/moontime`, - { - photo: { - stars: stars, - lines: globalPLsPointsCoord, + // 找到月亮索引并提取非月亮的标星数据 + const moonIdx = stars.findIndex((s) => ['月', '月亮', '月球', 'moon'].includes(s.name.trim().toLowerCase())); + if (moonIdx === -1) { + this.interactPhoto.tips.innerHTML = `没有找到月亮数据`; + this.interactPhoto.resetbuttonFunctioner(); + return; + } + const subStars = stars.filter((_, idx) => idx !== moonIdx); + + try { + // 1. 通过向后端的 positioning 发送不含月亮的星体坐标来估算地理位置与焦距 + const [geoEstimate, detail] = await post( + `${BACKEND_API}/positioning`, + { + photo: { + stars: subStars, + lines: globalPLsPointsCoord, + }, + isFixRefraction: isFixRefraction, + isFixGravity: isFixGravity, }, - approxTimestamp: approxTimestamp, - scopeDays: scopeDays, - isFixRefraction: isFixRefraction, - isFixGravity: isFixGravity, - }, - 'json' - ).then(([results, detail]) => { - if (detail === 'success') { - // 显示结果 - this.interactPhoto.setDatebyTime(results['time']); - // 使用新时间重新计算天体坐标并显示 - this.celeCoord.calc().then(() => { - this.interactPhoto.tips.innerHTML = '计算拍摄时间成功'; - }); - } else { - this.interactPhoto.tips.innerHTML = `计算拍摄时间失败:${detail}`; + 'json' + ); + + if (detail !== 'success' || !geoEstimate) { + this.interactPhoto.tips.innerHTML = `定位估算失败:${detail}`; + this.interactPhoto.resetbuttonFunctioner(); + return; + } + + // 2. 计算月亮与其他星体的图上像素夹角作为 targetAngles + const z = geoEstimate.z; + const points3D = stars.map((s) => [s.x, s.y, z]); + const targetAngles = points3D.map((p) => vectorAngle(points3D[moonIdx], p)); + + // 3. 提前并行获取所有非太阳系天体的赤经赤纬 J2000 数据 + const preFetchedRaDecs = {}; + const starNames = stars.map((s) => s.name); + + this.interactPhoto.tips.innerHTML = `正在检索星体坐标...`; + await Promise.all( + starNames.map(async (name) => { + preFetchedRaDecs[name] = await getRaDecByName(name); + }) + ); + + // 检查星体坐标是否检索成功 + let missingStars = []; + for (let i = 0; i < starNames.length; i++) { + if (i === moonIdx) continue; + if (!preFetchedRaDecs[starNames[i]]) { + missingStars.push(starNames[i]); + } + } + if (missingStars.length > 0) { + this.interactPhoto.tips.innerHTML = `未能获取天体 '${missingStars.join(', ')}' 的坐标信息,请检查拼写或网络`; + this.interactPhoto.resetbuttonFunctioner(); + return; } - }); - // 结束计算 - this.interactPhoto.resetbuttonFunctioner(); + // 4. 执行本地高精度迭代时间优化 + this.interactPhoto.tips.innerHTML = `正在推算精确拍摄时间...`; + const bestTimestamp = await optimizeMoonTime( + approxTimestamp, + scopeDays, + starNames, + geoEstimate, + moonIdx, + targetAngles, + preFetchedRaDecs + ); + + // 5. 显示结果并更新图上所有天体坐标 + this.interactPhoto.setDatebyTime(bestTimestamp); + await this.celeCoord.calc(); + this.interactPhoto.tips.innerHTML = '计算拍摄时间成功'; + } catch (error) { + this.interactPhoto.tips.innerHTML = `计算拍摄时间失败:${error.message}`; + } finally { + this.interactPhoto.resetbuttonFunctioner(); + } } // 检查originalStars数组每个子项的名称是否完整 checkStars(originalStars) { - let isComplete = true; - originalStars.forEach((originalStar) => { - if (originalStar[0] === '') { - isComplete = false; - } - }); - return isComplete; + return originalStars.every((originalStar) => originalStar.name && originalStar.name.trim() !== ''); } // 检查月亮 hasMoon(originalStars) { return originalStars.some( - (star) => star['name'] === '月' || star['name'] === '月亮' || star['name'] === 'moon' + (star) => star.name && ['月', '月亮', '月球', 'moon'].includes(star.name.trim().toLowerCase()) ); } } diff --git a/src/interface/functions/PickPL.js b/src/interface/functions/PickPL.js index e4d6ff9..a736d32 100644 --- a/src/interface/functions/PickPL.js +++ b/src/interface/functions/PickPL.js @@ -23,11 +23,7 @@ class PickPL extends DefaultbuttonFunctioner { clearData() { this.isPickingPL = false; - for (let pl of this.interactPhoto.PLArray.array) { - pl.remove(); - } - this.interactPhoto.PLArray.array = []; - this.interactPhoto.updateCalculButton(); + this.interactPhoto.PLArray.clear(); } handleMouseUp(e) { diff --git a/src/interface/init.js b/src/interface/init.js index 74f2870..1d0a70f 100644 --- a/src/interface/init.js +++ b/src/interface/init.js @@ -108,9 +108,13 @@ function initializeEvents(eventManager) { document.getElementById('addCeleRow').addEventListener('click', () => { interactPhoto.CeleArray.addEmptyRow(); }); + document.getElementById('addPlRow').addEventListener('click', () => { + interactPhoto.PLArray.addEmptyRow(); + }); //为星体名称输入框启用自动补全 interactPhoto.CeleArray.initializeTableRows(); + interactPhoto.PLArray.initializeTableRows(); } export { initializeElements, initializeCanvas, initializeEvents }; diff --git a/src/interface/utils.js b/src/interface/utils.js index 134045c..239e251 100644 --- a/src/interface/utils.js +++ b/src/interface/utils.js @@ -21,13 +21,18 @@ function getOriginalStars(interactPhoto) { // 获取铅垂线端点坐标 function getGlobalPLPointsCoord(interactPhoto) { let globalPLPointsCoord = []; - for (let i = 0; i < interactPhoto.PLArray.num(); i++) { - let pl = interactPhoto.PLArray.array[i]; - let points = []; - for (let j = 0; j < pl.points.length; j++) { - points.push(pl.points[j].coordinate); + const totalRows = interactPhoto.PLArray.tablePLRowCount(); + for (let i = 1; i <= totalRows; i++) { + const x1 = parseFloat(document.getElementById(`pl${i}_x1`)?.value); + const y1 = parseFloat(document.getElementById(`pl${i}_y1`)?.value); + const x2 = parseFloat(document.getElementById(`pl${i}_x2`)?.value); + const y2 = parseFloat(document.getElementById(`pl${i}_y2`)?.value); + if (Number.isFinite(x1) && Number.isFinite(y1) && Number.isFinite(x2) && Number.isFinite(y2)) { + globalPLPointsCoord.push([ + [x1, y1], + [x2, y2], + ]); } - globalPLPointsCoord.push(points); } return globalPLPointsCoord; }