skymap 0.0.1 → 0.2.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +40 -1
- data/README.md +229 -1
- data/data/constellations/iau/boundaries.csv +5110 -0
- data/data/constellations/iau/lines.csv +753 -0
- data/data/stars.csv +9097 -0
- data/lib/skymap/canvas.rb +13 -0
- data/lib/skymap/catalog/constellation_boundaries.rb +43 -0
- data/lib/skymap/catalog/constellation_lines.rb +37 -0
- data/lib/skymap/catalog/stars.rb +35 -0
- data/lib/skymap/chart.rb +22 -0
- data/lib/skymap/constellation_boundary.rb +5 -0
- data/lib/skymap/constellation_line.rb +5 -0
- data/lib/skymap/context.rb +51 -0
- data/lib/skymap/horizon.rb +32 -0
- data/lib/skymap/layers/cardinal_directions.rb +27 -0
- data/lib/skymap/layers/constellation_boundaries.rb +67 -0
- data/lib/skymap/layers/constellation_lines.rb +46 -0
- data/lib/skymap/layers/moon.rb +40 -0
- data/lib/skymap/layers/stars.rb +39 -0
- data/lib/skymap/point.rb +12 -0
- data/lib/skymap/projection/stereographic.rb +21 -0
- data/lib/skymap/renderer/boundary.rb +7 -0
- data/lib/skymap/renderer/dot.rb +7 -0
- data/lib/skymap/renderer/label.rb +7 -0
- data/lib/skymap/renderer/line.rb +7 -0
- data/lib/skymap/renderer/moon.rb +7 -0
- data/lib/skymap/renderer/svg.rb +132 -0
- data/lib/skymap/star.rb +5 -0
- data/lib/skymap/star_size.rb +27 -0
- data/lib/skymap/version.rb +1 -1
- data/lib/skymap.rb +26 -0
- metadata +84 -1
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "csv"
|
|
4
|
+
|
|
5
|
+
module Skymap
|
|
6
|
+
module Catalog
|
|
7
|
+
class ConstellationBoundaries
|
|
8
|
+
include Enumerable
|
|
9
|
+
|
|
10
|
+
PATH = File.expand_path(
|
|
11
|
+
"../../../data/constellations/iau/boundaries.csv",
|
|
12
|
+
__dir__
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
def each
|
|
16
|
+
CSV.foreach(PATH, headers: true).chunk_while do |previous, row|
|
|
17
|
+
previous["border"] == row["border"]
|
|
18
|
+
end.each do |rows|
|
|
19
|
+
yield boundary_from(rows)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def boundary_from(rows)
|
|
26
|
+
ConstellationBoundary.new(
|
|
27
|
+
constellations: rows.first["constellations"].split,
|
|
28
|
+
points: rows.map { |row| coordinates_from(row) }
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def coordinates_from(row)
|
|
33
|
+
Astronoby::Coordinates::Equatorial.new(
|
|
34
|
+
right_ascension: Astronoby::Angle.from_hours(
|
|
35
|
+
row["right_ascension"].to_f
|
|
36
|
+
),
|
|
37
|
+
declination: Astronoby::Angle.from_degrees(row["declination"].to_f),
|
|
38
|
+
epoch: Astronoby::JulianDate::J2000
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "csv"
|
|
4
|
+
|
|
5
|
+
module Skymap
|
|
6
|
+
module Catalog
|
|
7
|
+
class ConstellationLines
|
|
8
|
+
include Enumerable
|
|
9
|
+
|
|
10
|
+
PATH = File.expand_path(
|
|
11
|
+
"../../../data/constellations/iau/lines.csv",
|
|
12
|
+
__dir__
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
def each
|
|
16
|
+
CSV.foreach(PATH, headers: true) do |row|
|
|
17
|
+
yield line_from(row)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def line_from(row)
|
|
24
|
+
ConstellationLine.new(
|
|
25
|
+
constellation: row["constellation"],
|
|
26
|
+
from: hr_numbers(row["from_hr"]),
|
|
27
|
+
to: hr_numbers(row["to_hr"]),
|
|
28
|
+
weight: row["weight"].to_sym
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def hr_numbers(value)
|
|
33
|
+
value.split.map(&:to_i)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "csv"
|
|
4
|
+
|
|
5
|
+
module Skymap
|
|
6
|
+
module Catalog
|
|
7
|
+
class Stars
|
|
8
|
+
include Enumerable
|
|
9
|
+
|
|
10
|
+
PATH = File.expand_path("../../../data/stars.csv", __dir__)
|
|
11
|
+
|
|
12
|
+
def each
|
|
13
|
+
CSV.foreach(PATH, headers: true) do |row|
|
|
14
|
+
yield star_from(row)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def star_from(row)
|
|
21
|
+
Star.new(
|
|
22
|
+
hr: row["hr"].to_i,
|
|
23
|
+
equatorial_coordinates: Astronoby::Coordinates::Equatorial.new(
|
|
24
|
+
right_ascension: Astronoby::Angle.from_hours(
|
|
25
|
+
row["right_ascension"].to_f
|
|
26
|
+
),
|
|
27
|
+
declination: Astronoby::Angle.from_degrees(row["declination"].to_f),
|
|
28
|
+
epoch: Astronoby::JulianDate::J2000
|
|
29
|
+
),
|
|
30
|
+
magnitude: row["magnitude"].to_f
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/skymap/chart.rb
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Skymap
|
|
4
|
+
class Chart
|
|
5
|
+
def initialize(observer:, instant:, canvas:)
|
|
6
|
+
@observer = observer
|
|
7
|
+
@instant = instant
|
|
8
|
+
@canvas = canvas
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def render(*layers)
|
|
12
|
+
context = Context.new(
|
|
13
|
+
observer: @observer,
|
|
14
|
+
instant: @instant,
|
|
15
|
+
canvas: @canvas
|
|
16
|
+
)
|
|
17
|
+
elements = layers.flat_map { |layer| layer.elements(context) }
|
|
18
|
+
|
|
19
|
+
Renderer::SVG.new(canvas: @canvas).render(elements)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Skymap
|
|
4
|
+
class Context
|
|
5
|
+
attr_reader :observer, :instant, :canvas
|
|
6
|
+
|
|
7
|
+
def initialize(observer:, instant:, canvas:)
|
|
8
|
+
@observer = observer
|
|
9
|
+
@instant = instant
|
|
10
|
+
@canvas = canvas
|
|
11
|
+
@horizontal = {}
|
|
12
|
+
@projections = {}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def horizontal(star)
|
|
16
|
+
horizontal_coordinates(star.equatorial_coordinates)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def horizontal_coordinates(coordinates)
|
|
20
|
+
key = [
|
|
21
|
+
coordinates.right_ascension,
|
|
22
|
+
coordinates.declination,
|
|
23
|
+
coordinates.epoch
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
@horizontal[key] ||= Astronoby::DeepSkyObject
|
|
27
|
+
.new(equatorial_coordinates: coordinates)
|
|
28
|
+
.at(@instant)
|
|
29
|
+
.observed_by(@observer)
|
|
30
|
+
.horizontal
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def position(star)
|
|
34
|
+
horizontal = horizontal(star)
|
|
35
|
+
project(altitude: horizontal.altitude, azimuth: horizontal.azimuth)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def project(altitude:, azimuth:, radius: @canvas.radius)
|
|
39
|
+
projection(radius).project(altitude: altitude, azimuth: azimuth)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def projection(radius)
|
|
45
|
+
@projections[radius] ||= Projection::Stereographic.new(
|
|
46
|
+
center: @canvas.center,
|
|
47
|
+
radius: radius
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Skymap
|
|
4
|
+
module Horizon
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def direction(horizontals)
|
|
8
|
+
vectors = horizontals.map do |horizontal|
|
|
9
|
+
[
|
|
10
|
+
horizontal.altitude.cos * horizontal.azimuth.cos,
|
|
11
|
+
horizontal.altitude.cos * horizontal.azimuth.sin,
|
|
12
|
+
horizontal.altitude.sin
|
|
13
|
+
]
|
|
14
|
+
end
|
|
15
|
+
vectors.transpose.map(&:sum)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def crossing(above, below)
|
|
19
|
+
above.zip(below).map do |from, to|
|
|
20
|
+
above.last * to - below.last * from
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def project(context, direction)
|
|
25
|
+
x, y, = direction
|
|
26
|
+
context.project(
|
|
27
|
+
altitude: Astronoby::Angle.zero,
|
|
28
|
+
azimuth: Astronoby::Angle.from_radians(Math.atan2(y, x))
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Skymap
|
|
4
|
+
module Layers
|
|
5
|
+
class CardinalDirections
|
|
6
|
+
CARDINAL_DIRECTIONS = {"N" => 0, "E" => 90, "S" => 180, "W" => 270}
|
|
7
|
+
CARDINAL_SIZE_RATIO = 0.6
|
|
8
|
+
|
|
9
|
+
def elements(context)
|
|
10
|
+
canvas = context.canvas
|
|
11
|
+
|
|
12
|
+
CARDINAL_DIRECTIONS.map do |text, azimuth|
|
|
13
|
+
position = context.project(
|
|
14
|
+
altitude: Astronoby::Angle.zero,
|
|
15
|
+
azimuth: Astronoby::Angle.from_degrees(azimuth),
|
|
16
|
+
radius: canvas.radius + canvas.padding / 2.0
|
|
17
|
+
)
|
|
18
|
+
Renderer::Label.new(
|
|
19
|
+
position: position,
|
|
20
|
+
text: text,
|
|
21
|
+
size: canvas.padding * CARDINAL_SIZE_RATIO
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Skymap
|
|
4
|
+
module Layers
|
|
5
|
+
class ConstellationBoundaries
|
|
6
|
+
LINE_WIDTH_RATIO = 0.0017
|
|
7
|
+
|
|
8
|
+
def initialize(boundaries:)
|
|
9
|
+
@boundaries = boundaries
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def elements(context)
|
|
13
|
+
width = LINE_WIDTH_RATIO * context.canvas.radius
|
|
14
|
+
|
|
15
|
+
@boundaries.flat_map do |boundary|
|
|
16
|
+
visible_runs(context, boundary.points).map do |points|
|
|
17
|
+
Renderer::Boundary.new(points: points, width: width)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def visible_runs(context, coordinates)
|
|
25
|
+
horizontals = coordinates.map do |point|
|
|
26
|
+
context.horizontal_coordinates(point)
|
|
27
|
+
end
|
|
28
|
+
path = [position(context, horizontals.first)] +
|
|
29
|
+
horizontals.each_cons(2).flat_map do |from, to|
|
|
30
|
+
crossing(context, from, to) + [position(context, to)]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
path.chunk { |point| !point.nil? }.filter_map do |visible, points|
|
|
34
|
+
points if visible && points.size > 1
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def position(context, horizontal)
|
|
39
|
+
return if below?(horizontal)
|
|
40
|
+
|
|
41
|
+
context.project(
|
|
42
|
+
altitude: horizontal.altitude,
|
|
43
|
+
azimuth: horizontal.azimuth
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def crossing(context, from, to)
|
|
48
|
+
return [] if below?(from) == below?(to)
|
|
49
|
+
|
|
50
|
+
above, below = below?(from) ? [to, from] : [from, to]
|
|
51
|
+
[
|
|
52
|
+
Horizon.project(
|
|
53
|
+
context,
|
|
54
|
+
Horizon.crossing(
|
|
55
|
+
Horizon.direction([above]),
|
|
56
|
+
Horizon.direction([below])
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def below?(horizontal)
|
|
63
|
+
horizontal.altitude.negative?
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Skymap
|
|
4
|
+
module Layers
|
|
5
|
+
class ConstellationLines
|
|
6
|
+
LINE_WIDTH_RATIOS = {bold: 0.0044, normal: 0.003, thin: 0.0017}
|
|
7
|
+
|
|
8
|
+
def initialize(lines:, stars:)
|
|
9
|
+
@lines = lines
|
|
10
|
+
@stars = stars
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def elements(context)
|
|
14
|
+
stars_by_hr = @stars.to_h { |star| [star.hr, star] }
|
|
15
|
+
|
|
16
|
+
@lines.filter_map do |line|
|
|
17
|
+
ends = [line.from, line.to].map { |hrs| stars_by_hr.values_at(*hrs) }
|
|
18
|
+
next if ends.flatten.include?(nil)
|
|
19
|
+
|
|
20
|
+
directions = ends.map do |end_stars|
|
|
21
|
+
Horizon.direction(
|
|
22
|
+
end_stars.map { |star| context.horizontal(star) }
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
next if directions.all? { |direction| direction.last.negative? }
|
|
26
|
+
|
|
27
|
+
from, to = ends.each_with_index.map do |end_stars, index|
|
|
28
|
+
if directions[index].last.negative?
|
|
29
|
+
Horizon.project(
|
|
30
|
+
context,
|
|
31
|
+
Horizon.crossing(directions[1 - index], directions[index])
|
|
32
|
+
)
|
|
33
|
+
else
|
|
34
|
+
Point.midpoint(end_stars.map { |star| context.position(star) })
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
Renderer::Line.new(
|
|
38
|
+
from: from,
|
|
39
|
+
to: to,
|
|
40
|
+
width: LINE_WIDTH_RATIOS.fetch(line.weight) * context.canvas.radius
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Skymap
|
|
4
|
+
module Layers
|
|
5
|
+
class Moon
|
|
6
|
+
MOON_RADIUS_RATIO = 0.03
|
|
7
|
+
|
|
8
|
+
def initialize(ephem:)
|
|
9
|
+
@ephem = ephem
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def elements(context)
|
|
13
|
+
moon = Astronoby::Moon.at(context.instant, ephem: @ephem)
|
|
14
|
+
horizontal = moon.observed_by(context.observer).horizontal
|
|
15
|
+
return [] if horizontal.altitude.negative?
|
|
16
|
+
|
|
17
|
+
[
|
|
18
|
+
Renderer::Moon.new(
|
|
19
|
+
center: context.project(
|
|
20
|
+
altitude: horizontal.altitude,
|
|
21
|
+
azimuth: horizontal.azimuth
|
|
22
|
+
),
|
|
23
|
+
radius: MOON_RADIUS_RATIO * context.canvas.radius,
|
|
24
|
+
illuminated_fraction: moon.illuminated_fraction,
|
|
25
|
+
rotation: rotation(moon, context.observer)
|
|
26
|
+
)
|
|
27
|
+
]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def rotation(moon, observer)
|
|
33
|
+
from_zenith = moon.bright_limb_position_angle.degrees -
|
|
34
|
+
moon.parallactic_angle(observer: observer).degrees
|
|
35
|
+
|
|
36
|
+
-90 - from_zenith
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Skymap
|
|
4
|
+
module Layers
|
|
5
|
+
class Stars
|
|
6
|
+
DEFAULT_MAGNITUDE_LIMIT = 5
|
|
7
|
+
MAX_RADIUS_MAGNITUDE = -1.5
|
|
8
|
+
MIN_STAR_RADIUS_RATIO = 0.0025
|
|
9
|
+
MAX_STAR_RADIUS_RATIO = 0.02
|
|
10
|
+
STAR_SIZE_EXPONENT = 1.5
|
|
11
|
+
|
|
12
|
+
def initialize(stars:, magnitude_limit: DEFAULT_MAGNITUDE_LIMIT)
|
|
13
|
+
@stars = stars
|
|
14
|
+
@magnitude_limit = magnitude_limit
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def elements(context)
|
|
18
|
+
star_size = StarSize.new(
|
|
19
|
+
brightest_magnitude: MAX_RADIUS_MAGNITUDE,
|
|
20
|
+
faintest_magnitude: @magnitude_limit,
|
|
21
|
+
min_radius: MIN_STAR_RADIUS_RATIO * context.canvas.radius,
|
|
22
|
+
max_radius: MAX_STAR_RADIUS_RATIO * context.canvas.radius,
|
|
23
|
+
exponent: STAR_SIZE_EXPONENT
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
@stars.filter_map do |star|
|
|
27
|
+
next if star.magnitude > @magnitude_limit
|
|
28
|
+
|
|
29
|
+
next if context.horizontal(star).altitude.negative?
|
|
30
|
+
|
|
31
|
+
Renderer::Dot.new(
|
|
32
|
+
center: context.position(star),
|
|
33
|
+
radius: star_size.radius(star.magnitude)
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/skymap/point.rb
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Skymap
|
|
4
|
+
module Projection
|
|
5
|
+
class Stereographic
|
|
6
|
+
def initialize(center:, radius:)
|
|
7
|
+
@center = center
|
|
8
|
+
@radius = radius
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def project(altitude:, azimuth:)
|
|
12
|
+
distance = @radius * altitude.cos / (1 + altitude.sin)
|
|
13
|
+
|
|
14
|
+
Point.new(
|
|
15
|
+
x: @center.x - distance * azimuth.sin,
|
|
16
|
+
y: @center.y - distance * azimuth.cos
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "victor"
|
|
4
|
+
|
|
5
|
+
module Skymap
|
|
6
|
+
module Renderer
|
|
7
|
+
class SVG
|
|
8
|
+
SKY_COLOR = "#000000"
|
|
9
|
+
HORIZON_COLOR = "#3262a8"
|
|
10
|
+
HORIZON_WIDTH = 3
|
|
11
|
+
STAR_COLOR = "#ffffff"
|
|
12
|
+
LINE_COLOR = "#46689c"
|
|
13
|
+
BOUNDARY_COLOR = "#2b3f5e"
|
|
14
|
+
BOUNDARY_DASH_RATIO = 4
|
|
15
|
+
MOON_COLOR = "#f1ecd6"
|
|
16
|
+
MOON_SHADOW_COLOR = "#232a35"
|
|
17
|
+
MOON_SHADOW_OPACITY = 0.6
|
|
18
|
+
LABEL_COLOR = HORIZON_COLOR
|
|
19
|
+
LABEL_FONT = "sans-serif"
|
|
20
|
+
|
|
21
|
+
def initialize(canvas:)
|
|
22
|
+
@canvas = canvas
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def render(elements)
|
|
26
|
+
svg = Victor::SVG.new(
|
|
27
|
+
viewBox: "0 0 #{@canvas.size} #{@canvas.size}",
|
|
28
|
+
width: @canvas.size,
|
|
29
|
+
height: @canvas.size
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
svg.circle(
|
|
33
|
+
cx: @canvas.center.x.round(2),
|
|
34
|
+
cy: @canvas.center.y.round(2),
|
|
35
|
+
r: @canvas.radius.round(2),
|
|
36
|
+
fill: SKY_COLOR,
|
|
37
|
+
stroke: HORIZON_COLOR,
|
|
38
|
+
stroke_width: HORIZON_WIDTH
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
elements.each do |element|
|
|
42
|
+
case element
|
|
43
|
+
when Boundary then draw_boundary(svg, element)
|
|
44
|
+
when Line then draw_line(svg, element)
|
|
45
|
+
when Dot then draw_dot(svg, element)
|
|
46
|
+
when Label then draw_label(svg, element)
|
|
47
|
+
when Moon then draw_moon(svg, element)
|
|
48
|
+
else raise ArgumentError, "unknown element: #{element.class}"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
svg.render
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def draw_line(svg, line)
|
|
58
|
+
svg.line(
|
|
59
|
+
x1: line.from.x.round(2),
|
|
60
|
+
y1: line.from.y.round(2),
|
|
61
|
+
x2: line.to.x.round(2),
|
|
62
|
+
y2: line.to.y.round(2),
|
|
63
|
+
stroke: LINE_COLOR,
|
|
64
|
+
stroke_width: line.width.round(2),
|
|
65
|
+
stroke_linecap: "round"
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def draw_boundary(svg, boundary)
|
|
70
|
+
dash = (boundary.width * BOUNDARY_DASH_RATIO).round(2)
|
|
71
|
+
|
|
72
|
+
svg.polyline(
|
|
73
|
+
points: boundary.points.map { |point|
|
|
74
|
+
"#{point.x.round(2)},#{point.y.round(2)}"
|
|
75
|
+
}.join(" "),
|
|
76
|
+
fill: "none",
|
|
77
|
+
stroke: BOUNDARY_COLOR,
|
|
78
|
+
stroke_width: boundary.width.round(2),
|
|
79
|
+
stroke_dasharray: "#{dash} #{dash}"
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def draw_dot(svg, dot)
|
|
84
|
+
svg.circle(
|
|
85
|
+
cx: dot.center.x.round(2),
|
|
86
|
+
cy: dot.center.y.round(2),
|
|
87
|
+
r: dot.radius.round(2),
|
|
88
|
+
fill: STAR_COLOR
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def draw_moon(svg, moon)
|
|
93
|
+
x = moon.center.x.round(2)
|
|
94
|
+
y = moon.center.y.round(2)
|
|
95
|
+
radius = moon.radius.round(2)
|
|
96
|
+
fraction = moon.illuminated_fraction
|
|
97
|
+
terminator = (radius * (1 - 2 * fraction).abs).round(2)
|
|
98
|
+
bulge = (fraction > 0.5) ? 1 : 0
|
|
99
|
+
top = "#{x},#{(y - radius).round(2)}"
|
|
100
|
+
bottom = "#{x},#{(y + radius).round(2)}"
|
|
101
|
+
lit = "M #{top} A #{radius},#{radius} 0 0 1 #{bottom} " \
|
|
102
|
+
"A #{terminator},#{radius} 0 0 #{bulge} #{top} Z"
|
|
103
|
+
|
|
104
|
+
svg.build do
|
|
105
|
+
g(transform: "rotate(#{moon.rotation.round(2)} #{x} #{y})") do
|
|
106
|
+
circle(
|
|
107
|
+
cx: x,
|
|
108
|
+
cy: y,
|
|
109
|
+
r: radius,
|
|
110
|
+
fill: MOON_SHADOW_COLOR,
|
|
111
|
+
fill_opacity: MOON_SHADOW_OPACITY
|
|
112
|
+
)
|
|
113
|
+
path(d: lit, fill: MOON_COLOR)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def draw_label(svg, label)
|
|
119
|
+
svg.text(
|
|
120
|
+
label.text,
|
|
121
|
+
x: label.position.x.round(2),
|
|
122
|
+
y: label.position.y.round(2),
|
|
123
|
+
font_size: label.size.round(2),
|
|
124
|
+
font_family: LABEL_FONT,
|
|
125
|
+
text_anchor: "middle",
|
|
126
|
+
dominant_baseline: "central",
|
|
127
|
+
fill: LABEL_COLOR
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|