cossincalc 1.0.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.
data/lib/core_ext.rb ADDED
@@ -0,0 +1,18 @@
1
+ # Taken from Active Support (copyright (c) 2005-2008 David Heinemeier Hansson),
2
+ # which is realeased under the MIT license as part of the Ruby on Rails framework.
3
+ # http://github.com/rails/rails/blob/babbc1580da9e4a23921ab68d47c7c0d2e8447da/activesupport/lib/active_support/core_ext/symbol.rb
4
+
5
+ unless :to_proc.respond_to?(:to_proc)
6
+ class Symbol
7
+ # Turns the symbol into a simple proc, which is especially useful for enumerations. Examples:
8
+ #
9
+ # # The same as people.collect { |p| p.name }
10
+ # people.collect(&:name)
11
+ #
12
+ # # The same as people.select { |p| p.manager? }.collect { |p| p.salary }
13
+ # people.select(&:manager?).collect(&:salary)
14
+ def to_proc
15
+ Proc.new { |*args| args.shift.__send__(self, *args) }
16
+ end
17
+ end
18
+ end
data/lib/cossincalc.rb ADDED
@@ -0,0 +1,13 @@
1
+ require 'core_ext'
2
+ require 'cossincalc/version'
3
+ require 'cossincalc/triangle/calculator'
4
+ require 'cossincalc/triangle/drawing/svg'
5
+ require 'cossincalc/triangle/drawing'
6
+ require 'cossincalc/triangle/formatter'
7
+ require 'cossincalc/triangle/formatter/latex'
8
+ require 'cossincalc/triangle/validator'
9
+ require 'cossincalc/triangle/variable_hash'
10
+ require 'cossincalc/triangle'
11
+
12
+ module CosSinCalc
13
+ end
@@ -0,0 +1,124 @@
1
+ module CosSinCalc
2
+ class Triangle
3
+ include Calculator
4
+
5
+ VARIABLES = [:a, :b, :c]
6
+
7
+ attr_reader :alt # Reference to alternative triangle at ambiguous case.
8
+ attr_reader :equations # Steps performed to calculate the result.
9
+
10
+ # Initializes a triangle object with the given sides and angles and an optional reference to an alternative triangle.
11
+ #
12
+ # The sides and angles may be given either as a VariableHash object or an ordinary hash.
13
+ # The angle unit may be specified inside the given_angles hash (using the key :unit and value either :degree, :gon or :radian).
14
+ # If no angle unit is given it defaults to :degree.
15
+ # If a hash is used then value parsing and conversion will only occur if the values are provided as strings.
16
+ # Float angle values are expected to be radians no matter the given angle unit.
17
+ def initialize(given_sides, given_angles, alternative = nil)
18
+ initialize_variables
19
+
20
+ given_sides.each { |v, value| side[v] = Formatter.parse(value) }
21
+
22
+ angles.unit = (given_angles.respond_to?(:unit) ? given_angles.unit : given_angles.delete(:unit)) || :degree
23
+ given_angles.each { |v, value| angle[v] = Formatter.parse_angle(value, angles.unit) }
24
+
25
+ @alt = alternative
26
+ end
27
+
28
+ # Calculates the unknown variables in the triangle.
29
+ def calculate!
30
+ Validator.new(self).validate
31
+ calculate_variables
32
+ Validator.new(self).validate_calculation
33
+ @calculated = true
34
+ rescue Errno::EDOM
35
+ Validator::ValidationError.new([Validator::INVALID_TRIANGLE])
36
+ rescue Validator::ValidationError => exception
37
+ exception
38
+ end
39
+
40
+ def humanize(precision = 2)
41
+ Formatter.new(self, precision)
42
+ end
43
+
44
+ def angle(v = nil)
45
+ v.nil? ? @angles : @angles[v]
46
+ end
47
+ alias_method :angles, :angle
48
+
49
+ def side(v = nil)
50
+ v.nil? ? @sides : @sides[v]
51
+ end
52
+ alias_method :sides, :side
53
+
54
+ # Returns the length of the line which starts at the corner and is perpendicular with the opposite side.
55
+ def altitude(v)
56
+ require_calculation
57
+ r = rest(v)
58
+ Math.sin(angle(r[0])) * side(r[1])
59
+ end
60
+
61
+ # Returns the length of the line going from the corner to the middle of the opposite side.
62
+ def median(v)
63
+ require_calculation
64
+ Math.sqrt((2 * sq(sides(rest(v))).inject(&:+) - sq(side(v))) / 4)
65
+ end
66
+
67
+ # Returns the length of the line between a corner and the opposite side which bisects the angle at the corner.
68
+ def angle_bisector(v)
69
+ require_calculation
70
+ r = rest(v)
71
+ Math.sin(angle(r[0])) * side(r[1]) / Math.sin(angle(r[1]) + angle(v) / 2)
72
+ end
73
+
74
+ # Returns the area of the triangle.
75
+ def area
76
+ require_calculation
77
+ side(VARIABLES[0]) * side(VARIABLES[1]) * Math.sin(angle(VARIABLES[2])) / 2
78
+ end
79
+
80
+ # Returns the circumference of the triangle.
81
+ def circumference
82
+ require_calculation
83
+ sides(VARIABLES).inject(&:+)
84
+ end
85
+
86
+ # Executes the given block for each variable symbol.
87
+ # If an array of variable names is given, only those variables will be iterated through.
88
+ def each(array = VARIABLES, &block)
89
+ array.each { |v| block.arity == 2 ? yield(v, rest(v)) : yield(v) }
90
+ end
91
+
92
+ # Returns all the variable symbols except those given.
93
+ def rest(*vars)
94
+ VARIABLES - vars
95
+ end
96
+
97
+ # Returns whether the missing values have been successfully calculated.
98
+ def calculated?
99
+ @calculated
100
+ end
101
+
102
+ # Returns whether the given value is acute or not.
103
+ def acute?(value)
104
+ value < Math::PI / 2
105
+ end
106
+
107
+ # Returns whether the given value is obtuse or not.
108
+ def obtuse?(value)
109
+ value > Math::PI / 2
110
+ end
111
+
112
+ private
113
+ # Reset the sides, angles etc. to their default values.
114
+ def initialize_variables
115
+ @sides = VariableHash.new
116
+ @angles = VariableHash.new
117
+ end
118
+
119
+ # Calculate the missing values of the triangle if not already done.
120
+ def require_calculation
121
+ calculate! unless calculated?
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,118 @@
1
+ module CosSinCalc
2
+ class Triangle
3
+ module Calculator
4
+ include Math
5
+
6
+ def calculate_variables
7
+ case sides.amount
8
+ when 3 then calculate_three_angles
9
+ when 2 then calculate_two_angles
10
+ when 1 then calculate_two_sides
11
+ end
12
+ end
13
+
14
+ # Calculates the last unknown angle and side.
15
+ # This function is public so it is derectly callable (used with ambiguous case).
16
+ def calculate_side_and_angle
17
+ calculate_two_sides
18
+ end
19
+
20
+ # Calculates the value of an angle when all the sides are known.
21
+ def calculate_angle_by_sides(v, r)
22
+ acos (sq(sides(r)).inject(&:+) - sq(side(v))) / (2 * sides(r).inject(&:*))
23
+ end
24
+
25
+ # Add a calculation step to the list of equations performed.
26
+ def equation(latex, *variables)
27
+ @equations ||= []
28
+ @equations << [latex, variables]
29
+ end
30
+
31
+ private
32
+ def each(*args, &block)
33
+ @triangle.each(*args, &block)
34
+ end
35
+
36
+ def sq(value)
37
+ value.is_a?(Array) ? value.map { |n| n * n } : (value * value)
38
+ end
39
+
40
+ # Calculates all three angles when all three sides are known.
41
+ def calculate_three_angles
42
+ each do |v, r|
43
+ unless angle(v)
44
+ angle[v] = calculate_angle_by_sides(v, r)
45
+ equation('@1=\arccos\left(\frac{$2^2+$3^2-$1^2}{2 * $2 * $3}\right)', v, *r)
46
+ end
47
+ end
48
+ end
49
+
50
+ # Calculates two unknown angles when two sides and one angle are known.
51
+ def calculate_two_angles
52
+ each do |v, r|
53
+ if angle(v)
54
+ unless side(v)
55
+ side[v] = sqrt sq(sides(r)).inject(&:+) -
56
+ 2 * sides(r).inject(&:*) * cos(angle(v))
57
+ equation('$1=\sqrt{$2^2+$3^2-2 * $2 * $3 * \cos(@1)}', v, *r)
58
+ calculate_three_angles
59
+ break
60
+ end
61
+
62
+ each(r) do |v2|
63
+ if side(v2)
64
+ angle[v2] = asin sin(angle(v)) * side(v2) / side(v)
65
+ equation('@2=\arcsin\left(\frac{\sin(@1) * $2}{$1}\right)', v, v2)
66
+
67
+ if ambiguous_case?(v, v2)
68
+ @alt = CosSinCalc::Triangle.new(sides, angles, self)
69
+ @alt.angle[v2] = PI - angle(v2)
70
+ @alt.equation('@2=@pi-\arcsin\left(\frac{\sin(@1) * $2}{$1}\right)', v, v2)
71
+ @alt.calculate_side_and_angle
72
+ end
73
+
74
+ calculate_two_sides
75
+ break
76
+ end
77
+ end
78
+ break
79
+ end
80
+ end
81
+ end
82
+
83
+ # Calculates up to two unknown sides when at least one side and two angles are known.
84
+ def calculate_two_sides
85
+ calculate_last_angle
86
+
87
+ each do |v, r|
88
+ if side(v)
89
+ each(r) do |v2|
90
+ unless side(v2)
91
+ side[v2] = sin(angle(v2)) * side(v) / sin(angle(v))
92
+ equation('$2=\frac{\sin(@2) * $1}{\sin(@1)}', v, v2)
93
+ end
94
+ end
95
+ break
96
+ end
97
+ end
98
+ end
99
+
100
+ # Calculates the last unknown angle.
101
+ def calculate_last_angle
102
+ each do |v, r|
103
+ unless angle(v)
104
+ angle[v] = PI - angles(r).inject(&:+)
105
+ equation('@1=@pi-@2-@3', v, *r)
106
+ break
107
+ end
108
+ end
109
+ end
110
+
111
+ # Calculates and returns whether the triangle has multiple solutions.
112
+ # See http://en.wikipedia.org/wiki/Law_of_sines#The_ambiguous_case
113
+ def ambiguous_case?(v1, v2)
114
+ acute?(angle(v1)) && side(v1) < side(v2) && side(v1) > side(v2) * sin(angle(v1))
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,76 @@
1
+ module CosSinCalc
2
+ class Triangle
3
+ class Drawing
4
+ include Svg
5
+
6
+ # Initializes the drawing object of the given formatter's triangle with the provided maximum size and border padding.
7
+ def initialize(formatter, size = 500, padding = 50)
8
+ @formatter, @size, @padding = formatter, size, padding
9
+ @triangle = @formatter.triangle
10
+ @coords = {}
11
+ end
12
+
13
+ private
14
+ # Calculates the coordinates of the verticies of the triangle and scales it to maximum allowed size.
15
+ def draw
16
+ calculate_coords
17
+ resize
18
+ apply_padding
19
+ end
20
+
21
+ # Shorthand-method referencing the associated triangle object.
22
+ def t; @triangle end
23
+
24
+ # Shorthand-method referencing the associated triangle formatter object.
25
+ def f; @formatter end
26
+
27
+ # Calculates the coordinates for the corners of the triangle.
28
+ def calculate_coords
29
+ @coords[:a] = [ 0, 0 ]
30
+ @coords[:b] = [ t.side(:c) * Math.cos(t.angle(:a)), t.altitude(:b) ]
31
+ @coords[:c] = [ t.side(:b), 0 ]
32
+
33
+ if t.obtuse?(t.angle(:a))
34
+ move_coords(-@coords[:b][0])
35
+ end
36
+ end
37
+
38
+ # Moves the corners of the triangle with the given distance to the right.
39
+ # Pass a negative value to move them to the left.
40
+ def move_coords(distance)
41
+ t.each { |v| @coords[v][0] += distance }
42
+ end
43
+
44
+ # Scales the coordiantes to fit the size of the canvas.
45
+ def resize
46
+ scale_coords @size / [@coords[:b][0], @coords[:c][0], @coords[:b][1]].max
47
+ end
48
+
49
+ # Scales the coordinates with the given amount.
50
+ def scale_coords(scalar)
51
+ t.each do |v|
52
+ @coords[v][0] *= scalar
53
+ @coords[v][1] *= scalar
54
+ end
55
+ end
56
+
57
+ # Switches between bottom-left and top-left origin of the coordiante system.
58
+ def invert_coords
59
+ t.each { |v| @coords[v][1] = canvas_size - @coords[v][1] }
60
+ end
61
+
62
+ # Adds a padding around the triangle.
63
+ def apply_padding
64
+ t.each do |v|
65
+ @coords[v][0] += @padding
66
+ @coords[v][1] += @padding
67
+ end
68
+ end
69
+
70
+ # Returns the total size of the canvas, ie. the sum of the size of the triangle and the padding on both sides of it.
71
+ def canvas_size
72
+ @size + @padding * 2
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,127 @@
1
+ module CosSinCalc
2
+ class Triangle
3
+ class Drawing
4
+ module Svg
5
+ VERTEX_LABEL_MARGIN = 10 # The distance between the middle of the vertex label and the vertex itself.
6
+ VERTEX_VALUE_MARGIN = 55 # The distance between the middle of the vertex value and the vertex itself.
7
+ FONT_SIZE = 12 # The font size of the labels.
8
+ ARC_RADIUS = 25 # The radius of the circular arcs at the vertices.
9
+ NEXT_VARIABLE = { :a => :b, :b => :c, :c => :a } # The association between a variable and the next.
10
+
11
+ # Returns a drawing of the triangle in SVG (Scalable Vector Graphics) format.
12
+ def to_svg
13
+ draw
14
+ invert_coords
15
+
16
+ polygon = @coords.values.map { |c| c.join(',') }.join(' ')
17
+ labels = ''
18
+
19
+ t.each { |v| labels << vertex_label(v) << vertex_arc(v) << vertex_value(v) }
20
+ t.each { |v| labels << edge_label(v) } # Needs to be drawn last in order to make ImageMagick render it correctly.
21
+
22
+ <<-EOT
23
+ <?xml version="1.0" encoding="utf-8" standalone="no"?>
24
+ <svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="#{canvas_size}" height="#{canvas_size}">
25
+ <polygon fill="#f5eae5" stroke="#993300" stroke-width="1" points="#{polygon}"/>
26
+ #{labels}</svg>
27
+ EOT
28
+ end
29
+
30
+ # Saves a drawing of the triangle as an PNG file.
31
+ # The filename should be provided without the .png extension.
32
+ def save_png(filename)
33
+ save_svg(filename)
34
+ system "convert \"#{filename}.svg\" \"#{filename}.png\""
35
+ end
36
+
37
+ # Saves a drawing of the triangle as an SVG file.
38
+ # The filename should be provided without the .svg extension.
39
+ def save_svg(filename)
40
+ File.open("#{filename}.svg", 'w') { |f| f.write(to_svg) }
41
+ end
42
+
43
+ private
44
+ # Returns the SVG code for a label containing the given text at the given position.
45
+ def label(x, y, text, attributes = nil)
46
+ %[<text font-size="#{FONT_SIZE}" font-family="Verdana" fill="#333333" text-anchor="middle" x="#{x}" y="#{y + FONT_SIZE / 2}"#{' ' + attributes if attributes}>#{text}</text>\n]
47
+ end
48
+
49
+ # Returns the equivalent cartesian coordiantes (x and y) of the given polar coordiantes (angle and distance).
50
+ def polar_to_cartesian(angle, distance)
51
+ [ Math.cos(angle), Math.sin(angle) ].map { |val| distance * val }
52
+ end
53
+
54
+ # Returns the coordiantes of the vertex label of the given variable.
55
+ def vertex_label_coords(v)
56
+ x, y = *polar_to_cartesian(bisector_angle_to_x(v), VERTEX_LABEL_MARGIN)
57
+ [ @coords[v][0] - x, @coords[v][1] + y ]
58
+ end
59
+
60
+ # Returns the SVG code for the vertex label of the given variable.
61
+ def vertex_label(v)
62
+ x, y = *vertex_label_coords(v)
63
+ label(x, y, v.to_s.upcase)
64
+ end
65
+
66
+ # Returns the angle between the first edge (the right relative to the angle) connected to the given verted and the x-axis.
67
+ def angle_to_x(v)
68
+ case v
69
+ when :a then 0.0
70
+ when :b then -(t.angle(:b) + t.angle(:c))
71
+ when :c then Math::PI - t.angle(:c)
72
+ end
73
+ end
74
+
75
+ # Returns the angle between the angle bisector of the given vertex and the x-axis.
76
+ def bisector_angle_to_x(v)
77
+ t.angle(v) / 2 + angle_to_x(v)
78
+ end
79
+
80
+ # Returns the SVG code for the circular arcs at the given vertex.
81
+ def vertex_arc(v)
82
+ x1, y1 = *polar_to_cartesian(angle_to_x(v), ARC_RADIUS)
83
+ x2, y2 = *polar_to_cartesian(angle_to_x(v) + t.angle(v), ARC_RADIUS)
84
+ %[<path d="M #{@coords[v][0] + x1},#{@coords[v][1] - y1} A #{ARC_RADIUS},#{ARC_RADIUS} 0 0 0 #{@coords[v][0] + x2},#{@coords[v][1] - y2}" stroke="#90ee90" stroke-width="2" fill="none"/>\n]
85
+ end
86
+
87
+ # Returns the coordiantes of the vertex value label of the given variable.
88
+ def vertex_value_coords(v)
89
+ x, y = *polar_to_cartesian(bisector_angle_to_x(v), VERTEX_VALUE_MARGIN)
90
+ [ @coords[v][0] + x, @coords[v][1] - y ]
91
+ end
92
+
93
+ # Returns the SVG code for the vertex label containing the value of the angle including the unit.
94
+ def vertex_value(v)
95
+ x, y = *vertex_value_coords(v)
96
+ label(x, y, format_angle(f.angle(v), t.angles.unit))
97
+ end
98
+
99
+ # Adds the appropriate unit to the angle value.
100
+ def format_angle(value, unit)
101
+ if unit == :degree
102
+ value + '&#176;'
103
+ else
104
+ value + ' gon'
105
+ end
106
+ end
107
+
108
+ # Returns the SVG code for the given edge label.
109
+ def edge_label(v)
110
+ r = t.rest(v)
111
+ x = (@coords[r[1]][0] - @coords[r[0]][0]) / 2 + @coords[r[0]][0]
112
+ y = (@coords[r[1]][1] - @coords[r[0]][1]) / 2 + @coords[r[0]][1]
113
+
114
+ v2 = NEXT_VARIABLE[v]
115
+ angle = CosSinCalc::Triangle::Formatter.convert_angle(angle_to_x(v2) + t.angle(v2), :degree, true)
116
+ text = "#{v} = #{f.side(v)}"
117
+
118
+ if angle < 90 && angle > -90
119
+ label(x, y - FONT_SIZE, text, %[transform="rotate(#{-angle} #{x},#{y})"])
120
+ else
121
+ label(x, y + FONT_SIZE, text, %[transform="rotate(#{180 - angle}, #{x},#{y})"])
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end