skymap 0.0.1 → 0.1.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.
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skymap
4
+ Canvas = Data.define(:size, :padding) do
5
+ def center
6
+ Point.new(x: size / 2.0, y: size / 2.0)
7
+ end
8
+
9
+ def radius
10
+ size / 2.0 - padding
11
+ end
12
+ end
13
+ 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.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
@@ -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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skymap
4
+ ConstellationLine = Data.define(:constellation, :from, :to, :weight)
5
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skymap
4
+ class Context
5
+ attr_reader :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
+ coordinates = star.equatorial_coordinates
17
+ key = [
18
+ coordinates.right_ascension,
19
+ coordinates.declination,
20
+ coordinates.epoch
21
+ ]
22
+
23
+ @horizontal[key] ||= Astronoby::DeepSkyObject
24
+ .new(equatorial_coordinates: coordinates)
25
+ .at(@instant)
26
+ .observed_by(@observer)
27
+ .horizontal
28
+ end
29
+
30
+ def position(star)
31
+ horizontal = horizontal(star)
32
+ project(altitude: horizontal.altitude, azimuth: horizontal.azimuth)
33
+ end
34
+
35
+ def project(altitude:, azimuth:, radius: @canvas.radius)
36
+ projection(radius).project(altitude: altitude, azimuth: azimuth)
37
+ end
38
+
39
+ private
40
+
41
+ def projection(radius)
42
+ @projections[radius] ||= Projection::Stereographic.new(
43
+ center: @canvas.center,
44
+ radius: radius
45
+ )
46
+ end
47
+ end
48
+ 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,71 @@
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
+ direction(end_stars.map { |star| context.horizontal(star) })
22
+ end
23
+ next if directions.all? { |direction| direction.last.negative? }
24
+
25
+ from, to = ends.each_with_index.map do |end_stars, index|
26
+ if directions[index].last.negative?
27
+ project_direction(
28
+ context,
29
+ horizon_crossing(directions[1 - index], directions[index])
30
+ )
31
+ else
32
+ Point.midpoint(end_stars.map { |star| context.position(star) })
33
+ end
34
+ end
35
+ Renderer::Line.new(
36
+ from: from,
37
+ to: to,
38
+ width: LINE_WIDTH_RATIOS.fetch(line.weight) * context.canvas.radius
39
+ )
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def direction(horizontals)
46
+ vectors = horizontals.map do |horizontal|
47
+ [
48
+ horizontal.altitude.cos * horizontal.azimuth.cos,
49
+ horizontal.altitude.cos * horizontal.azimuth.sin,
50
+ horizontal.altitude.sin
51
+ ]
52
+ end
53
+ vectors.transpose.map(&:sum)
54
+ end
55
+
56
+ def horizon_crossing(above, below)
57
+ above.zip(below).map do |from, to|
58
+ above.last * to - below.last * from
59
+ end
60
+ end
61
+
62
+ def project_direction(context, direction)
63
+ x, y, = direction
64
+ context.project(
65
+ altitude: Astronoby::Angle.zero,
66
+ azimuth: Astronoby::Angle.from_radians(Math.atan2(y, x))
67
+ )
68
+ end
69
+ end
70
+ end
71
+ 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
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skymap
4
+ Point = Data.define(:x, :y) do
5
+ def self.midpoint(points)
6
+ new(
7
+ x: points.sum(&:x).fdiv(points.size),
8
+ y: points.sum(&:y).fdiv(points.size)
9
+ )
10
+ end
11
+ end
12
+ end
@@ -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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skymap
4
+ module Renderer
5
+ Dot = Data.define(:center, :radius)
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skymap
4
+ module Renderer
5
+ Label = Data.define(:position, :text, :size)
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skymap
4
+ module Renderer
5
+ Line = Data.define(:from, :to, :width)
6
+ end
7
+ end
@@ -0,0 +1,85 @@
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
+ LABEL_COLOR = HORIZON_COLOR
14
+ LABEL_FONT = "sans-serif"
15
+
16
+ def initialize(canvas:)
17
+ @canvas = canvas
18
+ end
19
+
20
+ def render(elements)
21
+ svg = Victor::SVG.new(
22
+ viewBox: "0 0 #{@canvas.size} #{@canvas.size}",
23
+ width: @canvas.size,
24
+ height: @canvas.size
25
+ )
26
+
27
+ svg.circle(
28
+ cx: @canvas.center.x.round(2),
29
+ cy: @canvas.center.y.round(2),
30
+ r: @canvas.radius.round(2),
31
+ fill: SKY_COLOR,
32
+ stroke: HORIZON_COLOR,
33
+ stroke_width: HORIZON_WIDTH
34
+ )
35
+
36
+ elements.each do |element|
37
+ case element
38
+ when Line then draw_line(svg, element)
39
+ when Dot then draw_dot(svg, element)
40
+ when Label then draw_label(svg, element)
41
+ else raise ArgumentError, "unknown element: #{element.class}"
42
+ end
43
+ end
44
+
45
+ svg.render
46
+ end
47
+
48
+ private
49
+
50
+ def draw_line(svg, line)
51
+ svg.line(
52
+ x1: line.from.x.round(2),
53
+ y1: line.from.y.round(2),
54
+ x2: line.to.x.round(2),
55
+ y2: line.to.y.round(2),
56
+ stroke: LINE_COLOR,
57
+ stroke_width: line.width.round(2),
58
+ stroke_linecap: "round"
59
+ )
60
+ end
61
+
62
+ def draw_dot(svg, dot)
63
+ svg.circle(
64
+ cx: dot.center.x.round(2),
65
+ cy: dot.center.y.round(2),
66
+ r: dot.radius.round(2),
67
+ fill: STAR_COLOR
68
+ )
69
+ end
70
+
71
+ def draw_label(svg, label)
72
+ svg.text(
73
+ label.text,
74
+ x: label.position.x.round(2),
75
+ y: label.position.y.round(2),
76
+ font_size: label.size.round(2),
77
+ font_family: LABEL_FONT,
78
+ text_anchor: "middle",
79
+ dominant_baseline: "central",
80
+ fill: LABEL_COLOR
81
+ )
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skymap
4
+ Star = Data.define(:hr, :equatorial_coordinates, :magnitude)
5
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skymap
4
+ class StarSize
5
+ def initialize(
6
+ brightest_magnitude:,
7
+ faintest_magnitude:,
8
+ min_radius:,
9
+ max_radius:,
10
+ exponent:
11
+ )
12
+ @brightest_magnitude = brightest_magnitude
13
+ @faintest_magnitude = faintest_magnitude
14
+ @min_radius = min_radius
15
+ @max_radius = max_radius
16
+ @exponent = exponent
17
+ end
18
+
19
+ def radius(magnitude)
20
+ brightness = (@faintest_magnitude - magnitude)
21
+ .fdiv(@faintest_magnitude - @brightest_magnitude)
22
+ .clamp(0, 1)
23
+
24
+ @min_radius + brightness**@exponent * (@max_radius - @min_radius)
25
+ end
26
+ end
27
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Skymap
4
- VERSION = "0.0.1"
4
+ VERSION = "0.1.0"
5
5
  end
data/lib/skymap.rb CHANGED
@@ -1,5 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "astronoby"
4
+
5
+ require_relative "skymap/canvas"
6
+ require_relative "skymap/catalog/constellation_lines"
7
+ require_relative "skymap/catalog/stars"
8
+ require_relative "skymap/chart"
9
+ require_relative "skymap/constellation_line"
10
+ require_relative "skymap/context"
11
+ require_relative "skymap/layers/cardinal_directions"
12
+ require_relative "skymap/layers/constellation_lines"
13
+ require_relative "skymap/layers/stars"
14
+ require_relative "skymap/point"
15
+ require_relative "skymap/projection/stereographic"
16
+ require_relative "skymap/renderer/dot"
17
+ require_relative "skymap/renderer/label"
18
+ require_relative "skymap/renderer/line"
19
+ require_relative "skymap/renderer/svg"
20
+ require_relative "skymap/star"
21
+ require_relative "skymap/star_size"
3
22
  require_relative "skymap/version"
4
23
 
5
24
  module Skymap
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: skymap
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rémy Hannequin
@@ -9,6 +9,48 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: astronoby
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.10'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.10'
26
+ - !ruby/object:Gem::Dependency
27
+ name: csv
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '3.3'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '3.3'
40
+ - !ruby/object:Gem::Dependency
41
+ name: victor
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.5'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '0.5'
12
54
  - !ruby/object:Gem::Dependency
13
55
  name: irb
14
56
  requirement: !ruby/object:Gem::Requirement
@@ -37,6 +79,20 @@ dependencies:
37
79
  - - "~>"
38
80
  - !ruby/object:Gem::Version
39
81
  version: '13.0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: rexml
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '3.4'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '3.4'
40
96
  - !ruby/object:Gem::Dependency
41
97
  name: rspec
42
98
  requirement: !ruby/object:Gem::Requirement
@@ -89,7 +145,26 @@ files:
89
145
  - CHANGELOG.md
90
146
  - LICENSE.txt
91
147
  - README.md
148
+ - data/constellations/iau.csv
149
+ - data/stars.csv
92
150
  - lib/skymap.rb
151
+ - lib/skymap/canvas.rb
152
+ - lib/skymap/catalog/constellation_lines.rb
153
+ - lib/skymap/catalog/stars.rb
154
+ - lib/skymap/chart.rb
155
+ - lib/skymap/constellation_line.rb
156
+ - lib/skymap/context.rb
157
+ - lib/skymap/layers/cardinal_directions.rb
158
+ - lib/skymap/layers/constellation_lines.rb
159
+ - lib/skymap/layers/stars.rb
160
+ - lib/skymap/point.rb
161
+ - lib/skymap/projection/stereographic.rb
162
+ - lib/skymap/renderer/dot.rb
163
+ - lib/skymap/renderer/label.rb
164
+ - lib/skymap/renderer/line.rb
165
+ - lib/skymap/renderer/svg.rb
166
+ - lib/skymap/star.rb
167
+ - lib/skymap/star_size.rb
93
168
  - lib/skymap/version.rb
94
169
  homepage: https://github.com/rhannequin/skymap
95
170
  licenses: