cossincalc 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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