besselian2shape 0.1.0__py3-none-any.whl
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.
- besselian2shape/__init__.py +16 -0
- besselian2shape/cache.py +51 -0
- besselian2shape/cli.py +127 -0
- besselian2shape/elements.py +138 -0
- besselian2shape/export.py +178 -0
- besselian2shape/geometry.py +612 -0
- besselian2shape/kml.py +210 -0
- besselian2shape/raster.py +467 -0
- besselian2shape/shapefiles.py +136 -0
- besselian2shape-0.1.0.dist-info/METADATA +146 -0
- besselian2shape-0.1.0.dist-info/RECORD +14 -0
- besselian2shape-0.1.0.dist-info/WHEEL +4 -0
- besselian2shape-0.1.0.dist-info/entry_points.txt +3 -0
- besselian2shape-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from .cache import download_besselian_csv, get_cache_dir, get_cached_csv_path
|
|
2
|
+
from .cli import main
|
|
3
|
+
from .elements import BesselianElements, find_by_date, load_all
|
|
4
|
+
from .export import generate_eclipse_kml, generate_eclipse_shapefiles
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"BesselianElements",
|
|
8
|
+
"download_besselian_csv",
|
|
9
|
+
"find_by_date",
|
|
10
|
+
"generate_eclipse_kml",
|
|
11
|
+
"generate_eclipse_shapefiles",
|
|
12
|
+
"get_cache_dir",
|
|
13
|
+
"get_cached_csv_path",
|
|
14
|
+
"load_all",
|
|
15
|
+
"main",
|
|
16
|
+
]
|
besselian2shape/cache.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Download and local caching of NASA's Besselian elements dataset."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
from platformdirs import user_cache_dir
|
|
10
|
+
|
|
11
|
+
BESSELIAN_ELEMENTS_URL = (
|
|
12
|
+
"https://eclipse.gsfc.nasa.gov/eclipse_besselian_from_mysqldump2.csv"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
APP_NAME = "besselian2shape"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_cache_dir() -> Path:
|
|
19
|
+
"""Return the local cache directory, creating it if necessary."""
|
|
20
|
+
cache_dir = Path(user_cache_dir(APP_NAME))
|
|
21
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
return cache_dir
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_cached_csv_path() -> Path:
|
|
26
|
+
"""Return the path where the Besselian elements CSV is (or will be) cached."""
|
|
27
|
+
return get_cache_dir() / "eclipse_besselian_from_mysqldump2.csv"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def download_besselian_csv(force: bool = False) -> Path:
|
|
31
|
+
"""Ensure the NASA Besselian elements CSV is cached locally, downloading it
|
|
32
|
+
if it is not already present (or if `force` is True).
|
|
33
|
+
|
|
34
|
+
Returns the path to the cached file.
|
|
35
|
+
"""
|
|
36
|
+
dest = get_cached_csv_path()
|
|
37
|
+
if dest.exists() and not force:
|
|
38
|
+
return dest
|
|
39
|
+
|
|
40
|
+
response = requests.get(BESSELIAN_ELEMENTS_URL, stream=True, timeout=60)
|
|
41
|
+
response.raise_for_status()
|
|
42
|
+
|
|
43
|
+
tmp_path = dest.with_suffix(dest.suffix + ".part")
|
|
44
|
+
try:
|
|
45
|
+
with open(tmp_path, "wb") as f:
|
|
46
|
+
shutil.copyfileobj(response.raw, f)
|
|
47
|
+
tmp_path.replace(dest)
|
|
48
|
+
finally:
|
|
49
|
+
tmp_path.unlink(missing_ok=True)
|
|
50
|
+
|
|
51
|
+
return dest
|
besselian2shape/cli.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Command-line interface for besselian2shape."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .cache import download_besselian_csv
|
|
10
|
+
from .elements import find_by_date
|
|
11
|
+
from .export import generate_eclipse_kml, generate_eclipse_shapefiles
|
|
12
|
+
|
|
13
|
+
_ALL_FORMATS = ("shp", "kml", "kmz")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _parse_args(argv: list[str] | None) -> argparse.Namespace:
|
|
17
|
+
parser = argparse.ArgumentParser(
|
|
18
|
+
prog="besselian2shape",
|
|
19
|
+
description="Generate ESRI shapefiles and/or KML/KMZ files for a solar eclipse's path.",
|
|
20
|
+
)
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"year",
|
|
23
|
+
type=int,
|
|
24
|
+
help="eclipse year, astronomical numbering (1 BCE = 0, 2 BCE = -1, ...)",
|
|
25
|
+
)
|
|
26
|
+
parser.add_argument("month", type=int, help="eclipse month (1-12)")
|
|
27
|
+
parser.add_argument("day", type=int, help="eclipse day of month")
|
|
28
|
+
parser.add_argument(
|
|
29
|
+
"-o",
|
|
30
|
+
"--output",
|
|
31
|
+
type=Path,
|
|
32
|
+
default=None,
|
|
33
|
+
metavar="DIR",
|
|
34
|
+
help="output directory (default: ./eclipse_YYYY-MM-DD)",
|
|
35
|
+
)
|
|
36
|
+
parser.add_argument(
|
|
37
|
+
"-f",
|
|
38
|
+
"--format",
|
|
39
|
+
dest="formats",
|
|
40
|
+
action="append",
|
|
41
|
+
choices=(*_ALL_FORMATS, "all"),
|
|
42
|
+
metavar="{shp,kml,kmz,all}",
|
|
43
|
+
help="output format to generate; may be repeated (default: shp)",
|
|
44
|
+
)
|
|
45
|
+
parser.add_argument(
|
|
46
|
+
"--step-minutes",
|
|
47
|
+
type=float,
|
|
48
|
+
default=0.5,
|
|
49
|
+
metavar="MINUTES",
|
|
50
|
+
help="time resolution used to sample the eclipse path (default: 0.5)",
|
|
51
|
+
)
|
|
52
|
+
parser.add_argument(
|
|
53
|
+
"--penumbral-resolution",
|
|
54
|
+
type=float,
|
|
55
|
+
default=0.25,
|
|
56
|
+
metavar="DEGREES",
|
|
57
|
+
help="grid resolution for the penumbral visibility boundary (default: 0.25)",
|
|
58
|
+
)
|
|
59
|
+
parser.add_argument(
|
|
60
|
+
"--penumbral-step-minutes",
|
|
61
|
+
type=float,
|
|
62
|
+
default=1.5,
|
|
63
|
+
metavar="MINUTES",
|
|
64
|
+
help="time step for the penumbral visibility raster scan (default: 1.5)",
|
|
65
|
+
)
|
|
66
|
+
parser.add_argument(
|
|
67
|
+
"--refresh-cache",
|
|
68
|
+
action="store_true",
|
|
69
|
+
help="re-download the Besselian elements dataset even if already cached",
|
|
70
|
+
)
|
|
71
|
+
return parser.parse_args(argv)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def main(argv: list[str] | None = None) -> int:
|
|
75
|
+
args = _parse_args(argv)
|
|
76
|
+
|
|
77
|
+
formats = list(dict.fromkeys(args.formats or ["shp"])) # de-dupe, preserve order
|
|
78
|
+
if "all" in formats:
|
|
79
|
+
formats = list(_ALL_FORMATS)
|
|
80
|
+
|
|
81
|
+
if args.refresh_cache:
|
|
82
|
+
print("Refreshing cached Besselian elements dataset...", file=sys.stderr)
|
|
83
|
+
download_besselian_csv(force=True)
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
elements = find_by_date(args.year, args.month, args.day)
|
|
87
|
+
except LookupError as exc:
|
|
88
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
89
|
+
return 1
|
|
90
|
+
|
|
91
|
+
output_dir = args.output or Path(f"eclipse_{args.year:04d}-{args.month:02d}-{args.day:02d}")
|
|
92
|
+
|
|
93
|
+
if "shp" in formats:
|
|
94
|
+
written = generate_eclipse_shapefiles(
|
|
95
|
+
args.year,
|
|
96
|
+
args.month,
|
|
97
|
+
args.day,
|
|
98
|
+
output_dir,
|
|
99
|
+
step_minutes=args.step_minutes,
|
|
100
|
+
elements=elements,
|
|
101
|
+
penumbral_resolution_deg=args.penumbral_resolution,
|
|
102
|
+
penumbral_step_minutes=args.penumbral_step_minutes,
|
|
103
|
+
)
|
|
104
|
+
for name, path in written.items():
|
|
105
|
+
print(f"wrote {name}: {path}")
|
|
106
|
+
|
|
107
|
+
for fmt in ("kml", "kmz"):
|
|
108
|
+
if fmt not in formats:
|
|
109
|
+
continue
|
|
110
|
+
out_path = output_dir / f"eclipse.{fmt}"
|
|
111
|
+
written_path = generate_eclipse_kml(
|
|
112
|
+
args.year,
|
|
113
|
+
args.month,
|
|
114
|
+
args.day,
|
|
115
|
+
out_path,
|
|
116
|
+
step_minutes=args.step_minutes,
|
|
117
|
+
elements=elements,
|
|
118
|
+
penumbral_resolution_deg=args.penumbral_resolution,
|
|
119
|
+
penumbral_step_minutes=args.penumbral_step_minutes,
|
|
120
|
+
)
|
|
121
|
+
print(f"wrote {fmt}: {written_path}")
|
|
122
|
+
|
|
123
|
+
return 0
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
if __name__ == "__main__":
|
|
127
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Parsing of NASA's Besselian elements CSV and lookup by date."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import csv
|
|
6
|
+
from dataclasses import dataclass, fields
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .cache import download_besselian_csv
|
|
10
|
+
|
|
11
|
+
_INT_FIELDS = {
|
|
12
|
+
"year", "month", "day", "luna_num", "saros", "cat_no", "canon_plate",
|
|
13
|
+
"etype", "PNS", "UNS", "NCN", "nSer", "nSeq", "nJLE",
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
_FLOAT_FIELDS = {
|
|
17
|
+
"dt", "gamma", "magnitude", "lat_dd_ge", "lng_dd_ge", "sun_alt", "sun_azm",
|
|
18
|
+
"path_width", "duration_secs", "julian_date", "t0",
|
|
19
|
+
"x0", "x1", "x2", "x3", "y0", "y1", "y2", "y3", "d0", "d1", "d2",
|
|
20
|
+
"mu0", "mu1", "mu2", "l10", "l11", "l12", "l20", "l21", "l22",
|
|
21
|
+
"tan_f1", "tan_f2", "tmin", "tmax",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True, slots=True)
|
|
26
|
+
class BesselianElements:
|
|
27
|
+
"""Besselian elements and metadata for a single solar eclipse.
|
|
28
|
+
|
|
29
|
+
Field names and units follow NASA's Five Millennium Canon of Solar
|
|
30
|
+
Eclipses mysqldump export: x/y are the coordinates of the shadow axis
|
|
31
|
+
on the fundamental plane, d/mu describe the axis's declination and
|
|
32
|
+
hour angle, l1/l2 are the penumbral/umbral radii, and tan_f1/tan_f2
|
|
33
|
+
are the penumbral/umbral cone angles. All are polynomials in time (in
|
|
34
|
+
hours) measured from t0 (TDT), valid over [tmin, tmax].
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
year: int
|
|
38
|
+
month: int
|
|
39
|
+
day: int
|
|
40
|
+
td_ge: str
|
|
41
|
+
dt: float
|
|
42
|
+
luna_num: int
|
|
43
|
+
saros: int
|
|
44
|
+
eclipse_type: str
|
|
45
|
+
gamma: float
|
|
46
|
+
magnitude: float
|
|
47
|
+
lat_ge: str
|
|
48
|
+
lng_ge: str
|
|
49
|
+
lat_dd_ge: float
|
|
50
|
+
lng_dd_ge: float
|
|
51
|
+
sun_alt: float
|
|
52
|
+
sun_azm: float
|
|
53
|
+
path_width: float
|
|
54
|
+
central_duration: str
|
|
55
|
+
duration_secs: float
|
|
56
|
+
cat_no: int
|
|
57
|
+
canon_plate: int
|
|
58
|
+
julian_date: float
|
|
59
|
+
t0: float
|
|
60
|
+
x0: float
|
|
61
|
+
x1: float
|
|
62
|
+
x2: float
|
|
63
|
+
x3: float
|
|
64
|
+
y0: float
|
|
65
|
+
y1: float
|
|
66
|
+
y2: float
|
|
67
|
+
y3: float
|
|
68
|
+
d0: float
|
|
69
|
+
d1: float
|
|
70
|
+
d2: float
|
|
71
|
+
mu0: float
|
|
72
|
+
mu1: float
|
|
73
|
+
mu2: float
|
|
74
|
+
l10: float
|
|
75
|
+
l11: float
|
|
76
|
+
l12: float
|
|
77
|
+
l20: float
|
|
78
|
+
l21: float
|
|
79
|
+
l22: float
|
|
80
|
+
tan_f1: float
|
|
81
|
+
tan_f2: float
|
|
82
|
+
tmin: float
|
|
83
|
+
tmax: float
|
|
84
|
+
etype: int
|
|
85
|
+
PNS: int
|
|
86
|
+
UNS: int
|
|
87
|
+
NCN: int
|
|
88
|
+
nSer: int
|
|
89
|
+
nSeq: int
|
|
90
|
+
nJLE: int
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def from_row(cls, row: dict[str, str]) -> "BesselianElements":
|
|
94
|
+
kwargs = {}
|
|
95
|
+
for f in fields(cls):
|
|
96
|
+
raw = row[f.name]
|
|
97
|
+
if f.name in _INT_FIELDS:
|
|
98
|
+
kwargs[f.name] = int(float(raw))
|
|
99
|
+
elif f.name in _FLOAT_FIELDS:
|
|
100
|
+
kwargs[f.name] = float(raw)
|
|
101
|
+
else:
|
|
102
|
+
kwargs[f.name] = raw
|
|
103
|
+
return cls(**kwargs)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def load_all(csv_path: Path | None = None) -> list[BesselianElements]:
|
|
107
|
+
"""Load every eclipse's Besselian elements from the (cached) CSV.
|
|
108
|
+
|
|
109
|
+
Downloads and caches the CSV first if `csv_path` is not given and no
|
|
110
|
+
cached copy exists yet.
|
|
111
|
+
"""
|
|
112
|
+
path = csv_path if csv_path is not None else download_besselian_csv()
|
|
113
|
+
with open(path, newline="", encoding="utf-8") as f:
|
|
114
|
+
reader = csv.DictReader(f)
|
|
115
|
+
return [BesselianElements.from_row(row) for row in reader]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def find_by_date(
|
|
119
|
+
year: int, month: int, day: int, csv_path: Path | None = None
|
|
120
|
+
) -> BesselianElements:
|
|
121
|
+
"""Return the Besselian elements for the solar eclipse on the given
|
|
122
|
+
(proleptic Gregorian, astronomical-numbered) calendar date.
|
|
123
|
+
|
|
124
|
+
Use astronomical year numbering for BCE dates (e.g. 1 BCE is year 0,
|
|
125
|
+
2 BCE is year -1). Raises LookupError if no eclipse occurred on that
|
|
126
|
+
date in the dataset.
|
|
127
|
+
"""
|
|
128
|
+
matches = [
|
|
129
|
+
e for e in load_all(csv_path)
|
|
130
|
+
if e.year == year and e.month == month and e.day == day
|
|
131
|
+
]
|
|
132
|
+
if not matches:
|
|
133
|
+
raise LookupError(f"No solar eclipse found for {year:04d}-{month:02d}-{day:02d}")
|
|
134
|
+
if len(matches) > 1:
|
|
135
|
+
raise LookupError(
|
|
136
|
+
f"Multiple eclipses found for {year:04d}-{month:02d}-{day:02d}: {matches}"
|
|
137
|
+
)
|
|
138
|
+
return matches[0]
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Top-level API: generate ESRI shapefiles or KML/KMZ for a solar eclipse
|
|
2
|
+
by date.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from . import kml as kml_writer
|
|
10
|
+
from .elements import BesselianElements, find_by_date
|
|
11
|
+
from .geometry import central_line, is_central, limits_to_ring, umbral_limits
|
|
12
|
+
from .raster import penumbral_coverage_rings
|
|
13
|
+
from .shapefiles import (
|
|
14
|
+
write_polygon_shapefile,
|
|
15
|
+
write_polygon_shapefile_with_holes,
|
|
16
|
+
write_polyline_shapefile,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# Grid resolution and time step for the penumbral visibility raster scan
|
|
20
|
+
# (see raster.py). The path/umbral computation's own `step_minutes` isn't
|
|
21
|
+
# reused here -- it controls sampling density along a 1-D analytic curve,
|
|
22
|
+
# a different kind of parameter from a 2-D grid's resolution.
|
|
23
|
+
_PENUMBRAL_RESOLUTION_DEG = 0.25
|
|
24
|
+
_PENUMBRAL_STEP_MINUTES = 1.5
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class _Layers:
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
e: BesselianElements,
|
|
31
|
+
step_minutes: float,
|
|
32
|
+
penumbral_resolution_deg: float,
|
|
33
|
+
penumbral_step_minutes: float,
|
|
34
|
+
):
|
|
35
|
+
self.penumbral_polygons = penumbral_coverage_rings(
|
|
36
|
+
e,
|
|
37
|
+
lambda s: (s.l1, s.l1p),
|
|
38
|
+
resolution_deg=penumbral_resolution_deg,
|
|
39
|
+
step_minutes=penumbral_step_minutes,
|
|
40
|
+
)
|
|
41
|
+
self.central_line: list[tuple[float, float]] = []
|
|
42
|
+
self.umbral_ring: list[tuple[float, float]] = []
|
|
43
|
+
if is_central(e):
|
|
44
|
+
self.central_line = central_line(e, step_minutes)
|
|
45
|
+
self.umbral_ring = limits_to_ring(umbral_limits(e, step_minutes))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _load_layers(
|
|
49
|
+
year: int,
|
|
50
|
+
month: int,
|
|
51
|
+
day: int,
|
|
52
|
+
csv_path: Path | None,
|
|
53
|
+
step_minutes: float,
|
|
54
|
+
elements: BesselianElements | None,
|
|
55
|
+
penumbral_resolution_deg: float,
|
|
56
|
+
penumbral_step_minutes: float,
|
|
57
|
+
) -> tuple[BesselianElements, _Layers]:
|
|
58
|
+
e = elements if elements is not None else find_by_date(year, month, day, csv_path=csv_path)
|
|
59
|
+
layers = _Layers(e, step_minutes, penumbral_resolution_deg, penumbral_step_minutes)
|
|
60
|
+
if not layers.penumbral_polygons:
|
|
61
|
+
raise RuntimeError(
|
|
62
|
+
f"Could not compute a penumbral path for {year:04d}-{month:02d}-{day:02d}"
|
|
63
|
+
)
|
|
64
|
+
return e, layers
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def generate_eclipse_shapefiles(
|
|
68
|
+
year: int,
|
|
69
|
+
month: int,
|
|
70
|
+
day: int,
|
|
71
|
+
output_dir: Path | str,
|
|
72
|
+
csv_path: Path | None = None,
|
|
73
|
+
step_minutes: float = 0.5,
|
|
74
|
+
elements: BesselianElements | None = None,
|
|
75
|
+
penumbral_resolution_deg: float = _PENUMBRAL_RESOLUTION_DEG,
|
|
76
|
+
penumbral_step_minutes: float = _PENUMBRAL_STEP_MINUTES,
|
|
77
|
+
) -> dict[str, Path]:
|
|
78
|
+
"""Generate ESRI shapefiles for the solar eclipse on the given
|
|
79
|
+
(proleptic Gregorian, astronomical-numbered) calendar date.
|
|
80
|
+
|
|
81
|
+
Writes into `output_dir`:
|
|
82
|
+
- "penumbral_path.shp": polygon (possibly multi-part, e.g. a main
|
|
83
|
+
region plus separate disjoint loops for a shadow path that passes
|
|
84
|
+
close to a pole) of the region on Earth from which at least a
|
|
85
|
+
partial eclipse is visible (all eclipse types).
|
|
86
|
+
- "central_line.shp": polyline of the path of totality/annularity's
|
|
87
|
+
central line (total/annular/hybrid eclipses only).
|
|
88
|
+
- "umbral_path.shp": polygon of the path of totality/annularity
|
|
89
|
+
(total/annular/hybrid eclipses only).
|
|
90
|
+
|
|
91
|
+
Each shapefile is written as a single-feature .shp/.shx/.dbf/.prj set
|
|
92
|
+
in WGS84 geographic coordinates. `step_minutes` controls how finely
|
|
93
|
+
the umbral/central-line path is sampled along its length.
|
|
94
|
+
`penumbral_resolution_deg`/`penumbral_step_minutes` control the
|
|
95
|
+
coverage-grid resolution and time step used to compute the penumbral
|
|
96
|
+
boundary (see `raster.penumbral_coverage_rings`); the defaults are a
|
|
97
|
+
reasonable accuracy/runtime balance, coarsened here mainly for fast
|
|
98
|
+
tests.
|
|
99
|
+
|
|
100
|
+
If `elements` is given, it is used directly instead of looking `year`,
|
|
101
|
+
`month`, `day` up in the (cached) CSV -- useful when generating more
|
|
102
|
+
than one output format for the same eclipse, to avoid re-parsing the
|
|
103
|
+
dataset each time.
|
|
104
|
+
|
|
105
|
+
Raises LookupError if no eclipse occurred on that date. Returns a dict
|
|
106
|
+
mapping each layer name written to its .shp path.
|
|
107
|
+
"""
|
|
108
|
+
e, layers = _load_layers(
|
|
109
|
+
year, month, day, csv_path, step_minutes, elements,
|
|
110
|
+
penumbral_resolution_deg, penumbral_step_minutes,
|
|
111
|
+
)
|
|
112
|
+
output_dir = Path(output_dir)
|
|
113
|
+
|
|
114
|
+
written: dict[str, Path] = {
|
|
115
|
+
"penumbral_path": write_polygon_shapefile_with_holes(
|
|
116
|
+
output_dir / "penumbral_path.shp", layers.penumbral_polygons, e
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if layers.central_line or layers.umbral_ring:
|
|
121
|
+
path_fields = [("duration", "C", 8, 0), ("path_km", "F", 10, 2)]
|
|
122
|
+
path_record = {"duration": e.central_duration, "path_km": e.path_width}
|
|
123
|
+
|
|
124
|
+
if layers.central_line:
|
|
125
|
+
written["central_line"] = write_polyline_shapefile(
|
|
126
|
+
output_dir / "central_line.shp",
|
|
127
|
+
layers.central_line,
|
|
128
|
+
e,
|
|
129
|
+
extra_fields=path_fields,
|
|
130
|
+
extra_record=path_record,
|
|
131
|
+
)
|
|
132
|
+
if layers.umbral_ring:
|
|
133
|
+
written["umbral_path"] = write_polygon_shapefile(
|
|
134
|
+
output_dir / "umbral_path.shp",
|
|
135
|
+
layers.umbral_ring,
|
|
136
|
+
e,
|
|
137
|
+
extra_fields=path_fields,
|
|
138
|
+
extra_record=path_record,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
return written
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def generate_eclipse_kml(
|
|
145
|
+
year: int,
|
|
146
|
+
month: int,
|
|
147
|
+
day: int,
|
|
148
|
+
output_path: Path | str,
|
|
149
|
+
csv_path: Path | None = None,
|
|
150
|
+
step_minutes: float = 0.5,
|
|
151
|
+
elements: BesselianElements | None = None,
|
|
152
|
+
penumbral_resolution_deg: float = _PENUMBRAL_RESOLUTION_DEG,
|
|
153
|
+
penumbral_step_minutes: float = _PENUMBRAL_STEP_MINUTES,
|
|
154
|
+
) -> Path:
|
|
155
|
+
"""Generate a single KML (or, if `output_path` ends in .kmz, KMZ) file
|
|
156
|
+
for the solar eclipse on the given date, containing the same layers as
|
|
157
|
+
`generate_eclipse_shapefiles` as separate Placemarks in one Document:
|
|
158
|
+
the penumbral path (all eclipse types), and, for total/annular/hybrid
|
|
159
|
+
eclipses, the central line and umbral path.
|
|
160
|
+
|
|
161
|
+
See `generate_eclipse_shapefiles` for the other parameters.
|
|
162
|
+
|
|
163
|
+
Raises LookupError if no eclipse occurred on that date.
|
|
164
|
+
"""
|
|
165
|
+
e, layers = _load_layers(
|
|
166
|
+
year, month, day, csv_path, step_minutes, elements,
|
|
167
|
+
penumbral_resolution_deg, penumbral_step_minutes,
|
|
168
|
+
)
|
|
169
|
+
output_path = Path(output_path)
|
|
170
|
+
|
|
171
|
+
doc = kml_writer.build_document(e)
|
|
172
|
+
kml_writer.add_penumbral_path(doc, e, layers.penumbral_polygons)
|
|
173
|
+
if layers.central_line:
|
|
174
|
+
kml_writer.add_central_line(doc, e, layers.central_line)
|
|
175
|
+
if layers.umbral_ring:
|
|
176
|
+
kml_writer.add_umbral_path(doc, e, layers.umbral_ring)
|
|
177
|
+
|
|
178
|
+
return kml_writer.write_kml(doc, output_path)
|