en14960 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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rspec_status +31 -0
- data/CHANGELOG.md +25 -0
- data/LICENSE +14 -0
- data/LICENSE.txt +21 -0
- data/README.md +195 -0
- data/Rakefile +15 -0
- data/en14960.gemspec +47 -0
- data/lib/en14960/calculators/anchor_calculator.rb +82 -0
- data/lib/en14960/calculators/slide_calculator.rb +254 -0
- data/lib/en14960/calculators/user_capacity_calculator.rb +98 -0
- data/lib/en14960/constants.rb +90 -0
- data/lib/en14960/models/calculator_response.rb +21 -0
- data/lib/en14960/validators/material_validator.rb +57 -0
- data/lib/en14960/version.rb +5 -0
- data/lib/en14960.rb +82 -0
- metadata +135 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: e392aeaeca04d9c6c7648b5517675cecdd55470c983aa834e7f09cd083cedc6c
|
4
|
+
data.tar.gz: 0a60d6ac76ca2bee3463215f0ec2e94c9947b1d881be4d2abdc83f70d07a1784
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: '0953db3f8d5c86091ff07ac92f9597299cef1ef4482d67fb877b1d3dfc08bd60e92c4fc7396827ad0946f7e4c4bcee4a28bb663d669f403af1b917c1e01b5725'
|
7
|
+
data.tar.gz: 023212ec36c17575cc234413fb42812d041dc1df8a393b0e747570e0a890d352876f178a12a6b48b7e7676222eb9b0d382748605c451d3f54fe30eef6d0c24b9
|
data/.rspec
ADDED
data/.rspec_status
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
example_id | status | run_time |
|
2
|
+
----------------------------------------------------- | ------ | --------------- |
|
3
|
+
./spec/calculators/anchor_calculator_spec.rb[1:1:1:1] | passed | 0.00004 seconds |
|
4
|
+
./spec/calculators/anchor_calculator_spec.rb[1:1:1:2] | passed | 0.00006 seconds |
|
5
|
+
./spec/calculators/anchor_calculator_spec.rb[1:1:1:3] | passed | 0.00005 seconds |
|
6
|
+
./spec/calculators/anchor_calculator_spec.rb[1:1:2:1] | passed | 0.00005 seconds |
|
7
|
+
./spec/calculators/anchor_calculator_spec.rb[1:1:2:2] | passed | 0.00004 seconds |
|
8
|
+
./spec/calculators/anchor_calculator_spec.rb[1:1:3:1] | passed | 0.001 seconds |
|
9
|
+
./spec/calculators/anchor_calculator_spec.rb[1:1:3:2] | passed | 0.00043 seconds |
|
10
|
+
./spec/calculators/anchor_calculator_spec.rb[1:1:4:1] | passed | 0.00028 seconds |
|
11
|
+
./spec/calculators/anchor_calculator_spec.rb[1:1:4:2] | passed | 0.00003 seconds |
|
12
|
+
./spec/calculators/anchor_calculator_spec.rb[1:1:4:3] | passed | 0.00002 seconds |
|
13
|
+
./spec/calculators/anchor_calculator_spec.rb[1:1:5:1] | passed | 0.00005 seconds |
|
14
|
+
./spec/calculators/anchor_calculator_spec.rb[1:1:5:2] | passed | 0.00007 seconds |
|
15
|
+
./spec/calculators/anchor_calculator_spec.rb[1:2:1:1] | passed | 0.00002 seconds |
|
16
|
+
./spec/calculators/anchor_calculator_spec.rb[1:2:1:2] | passed | 0.00003 seconds |
|
17
|
+
./spec/calculators/anchor_calculator_spec.rb[1:2:1:3] | passed | 0.00003 seconds |
|
18
|
+
./spec/calculators/anchor_calculator_spec.rb[1:2:1:4] | passed | 0.00003 seconds |
|
19
|
+
./spec/calculators/anchor_calculator_spec.rb[1:2:1:5] | passed | 0.00005 seconds |
|
20
|
+
./spec/calculators/anchor_calculator_spec.rb[1:3:1] | passed | 0.00003 seconds |
|
21
|
+
./spec/calculators/anchor_calculator_spec.rb[1:4:1] | passed | 0.00044 seconds |
|
22
|
+
./spec/en14960_spec.rb[1:1] | passed | 0.00027 seconds |
|
23
|
+
./spec/en14960_spec.rb[1:2:1:1] | passed | 0.00004 seconds |
|
24
|
+
./spec/en14960_spec.rb[1:2:2:1] | passed | 0.00022 seconds |
|
25
|
+
./spec/en14960_spec.rb[1:2:2:2] | passed | 0.00004 seconds |
|
26
|
+
./spec/en14960_spec.rb[1:2:3:1] | passed | 0.00006 seconds |
|
27
|
+
./spec/en14960_spec.rb[1:2:4:1] | passed | 0.00008 seconds |
|
28
|
+
./spec/en14960_spec.rb[1:2:4:2] | passed | 0.00005 seconds |
|
29
|
+
./spec/en14960_spec.rb[1:2:5:1] | passed | 0.00005 seconds |
|
30
|
+
./spec/en14960_spec.rb[1:2:6:1] | passed | 0.00004 seconds |
|
31
|
+
./spec/en14960_spec.rb[1:2:7:1] | passed | 0.00003 seconds |
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
All notable changes to this project will be documented in this file.
|
4
|
+
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
|
+
|
8
|
+
## [0.1.0] - 2025-01-28
|
9
|
+
|
10
|
+
### Added
|
11
|
+
- Initial release of EN14960 gem
|
12
|
+
- Anchor calculator for wind load calculations per EN 14960:2019 Annex A
|
13
|
+
- Slide calculator for runout distances and wall height requirements
|
14
|
+
- User capacity calculator based on age-appropriate space allocation
|
15
|
+
- Material validator for rope, fabric, thread, and netting specifications
|
16
|
+
- Comprehensive constants from EN 14960:2019 standard
|
17
|
+
- Full test suite with RSpec
|
18
|
+
- Detailed documentation and usage examples
|
19
|
+
|
20
|
+
### Features
|
21
|
+
- Framework-agnostic implementation (no Rails dependency)
|
22
|
+
- Clean public API with intuitive method names
|
23
|
+
- Detailed calculation breakdowns for transparency
|
24
|
+
- Support for all height categories defined in EN 14960:2019
|
25
|
+
- Configurable parameters for various scenarios
|
data/LICENSE
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
Copyright (C) 2025 Chobble.com
|
2
|
+
|
3
|
+
This program is free software: you can redistribute it and/or modify
|
4
|
+
it under the terms of the GNU Affero General Public License as published
|
5
|
+
by the Free Software Foundation, either version 3 of the License, or
|
6
|
+
(at your option) any later version.
|
7
|
+
|
8
|
+
This program is distributed in the hope that it will be useful,
|
9
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
11
|
+
GNU Affero General Public License for more details.
|
12
|
+
|
13
|
+
You should have received a copy of the GNU Affero General Public License
|
14
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2025 [Your Name]
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,195 @@
|
|
1
|
+
# EN14960
|
2
|
+
|
3
|
+
A Ruby gem providing calculators and validators for BS EN 14960:2019 - the British/European safety standard for inflatable play equipment.
|
4
|
+
|
5
|
+
## Features
|
6
|
+
|
7
|
+
- **Anchor Calculations**: Calculate required ground anchors based on dimensions and wind loads
|
8
|
+
- **Slide Safety**: Calculate runout distances and wall height requirements
|
9
|
+
- **User Capacity**: Calculate safe user capacity based on play area and user heights
|
10
|
+
- **Material Validation**: Validate material specifications against EN 14960 requirements
|
11
|
+
|
12
|
+
## Installation
|
13
|
+
|
14
|
+
Add this line to your application's Gemfile:
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
gem 'en14960'
|
18
|
+
```
|
19
|
+
|
20
|
+
And then execute:
|
21
|
+
|
22
|
+
$ bundle install
|
23
|
+
|
24
|
+
Or install it yourself as:
|
25
|
+
|
26
|
+
$ gem install en14960
|
27
|
+
|
28
|
+
## Usage
|
29
|
+
|
30
|
+
### Anchor Calculations
|
31
|
+
|
32
|
+
Calculate required anchors for inflatable play equipment:
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
result = EN14960.calculate_anchors(length: 5, width: 4, height: 3)
|
36
|
+
puts result.value # => 8
|
37
|
+
puts result.breakdown
|
38
|
+
# => [
|
39
|
+
# ["Front/back area", "4m (W) × 3m (H) = 12m²"],
|
40
|
+
# ["Sides area", "5m (L) × 3m (H) = 15m²"],
|
41
|
+
# ["Front & back anchor counts", "((12.0 × 114.0 * 1.5) ÷ 1600.0 = 2"],
|
42
|
+
# ["Left & right anchor counts", "((15.0 × 114.0 * 1.5) ÷ 1600.0 = 2"],
|
43
|
+
# ["Required anchors", "(2 + 2) × 2 = 8"]
|
44
|
+
# ]
|
45
|
+
```
|
46
|
+
|
47
|
+
### Slide Runout Calculations
|
48
|
+
|
49
|
+
Calculate minimum runout distance for slides:
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
# Basic runout calculation
|
53
|
+
result = EN14960.calculate_slide_runout(2.5)
|
54
|
+
puts result.value # => 1.25 (meters)
|
55
|
+
|
56
|
+
# With stop wall
|
57
|
+
result = EN14960.calculate_slide_runout(2.5, has_stop_wall: true)
|
58
|
+
puts result.value # => 1.75 (meters)
|
59
|
+
```
|
60
|
+
|
61
|
+
### Wall Height Requirements
|
62
|
+
|
63
|
+
Calculate containing wall requirements for different platform heights:
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
result = EN14960.calculate_wall_height(2.0, 1.5)
|
67
|
+
puts result.value # => 1.5 (meters)
|
68
|
+
puts result.breakdown
|
69
|
+
# => [
|
70
|
+
# ["Height range", "0.6m - 3.0m"],
|
71
|
+
# ["Calculation", "1.5m (user height)"]
|
72
|
+
# ]
|
73
|
+
```
|
74
|
+
|
75
|
+
### User Capacity Calculations
|
76
|
+
|
77
|
+
Calculate safe user capacity based on play area:
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
result = EN14960.calculate_user_capacity(10, 8)
|
81
|
+
puts result.value
|
82
|
+
# => {
|
83
|
+
# users_1000mm: 80, # 1.0m tall users
|
84
|
+
# users_1200mm: 60, # 1.2m tall users
|
85
|
+
# users_1500mm: 48, # 1.5m tall users
|
86
|
+
# users_1800mm: 40 # 1.8m tall users
|
87
|
+
# }
|
88
|
+
|
89
|
+
# With maximum user height restriction
|
90
|
+
result = EN14960.calculate_user_capacity(10, 8, 1.5, 0)
|
91
|
+
puts result.value[:users_1800mm] # => 0 (not allowed)
|
92
|
+
|
93
|
+
# With obstacles/negative play space
|
94
|
+
result = EN14960.calculate_user_capacity(10, 8, nil, 15)
|
95
|
+
# Reduces usable area by 15m²
|
96
|
+
```
|
97
|
+
|
98
|
+
### Material Validation
|
99
|
+
|
100
|
+
Validate materials against EN 14960 requirements:
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
# Rope diameter validation
|
104
|
+
EN14960.valid_rope_diameter?(20) # => true (within 18-45mm range)
|
105
|
+
EN14960.valid_rope_diameter?(10) # => false (too thin)
|
106
|
+
|
107
|
+
# Access material standards
|
108
|
+
standards = EN14960.material_standards
|
109
|
+
puts standards[:fabric][:min_tensile_strength] # => 1850 (Newtons)
|
110
|
+
puts standards[:rope][:min_diameter] # => 18 (mm)
|
111
|
+
```
|
112
|
+
|
113
|
+
### Direct Module Access
|
114
|
+
|
115
|
+
You can also use the calculator modules directly:
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
# Using AnchorCalculator directly
|
119
|
+
result = EN14960::Calculators::AnchorCalculator.calculate(
|
120
|
+
length: 10,
|
121
|
+
width: 8,
|
122
|
+
height: 4
|
123
|
+
)
|
124
|
+
|
125
|
+
# Using MaterialValidator directly
|
126
|
+
EN14960::Validators::MaterialValidator.valid_fabric_tensile_strength?(2000)
|
127
|
+
# => true
|
128
|
+
```
|
129
|
+
|
130
|
+
## Constants
|
131
|
+
|
132
|
+
The gem provides access to all EN 14960:2019 constants:
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
# Height categories
|
136
|
+
EN14960.height_categories
|
137
|
+
# => {
|
138
|
+
# 1000 => {label: "1.0m (Young children)", max_users: :calculate_by_area},
|
139
|
+
# 1200 => {label: "1.2m (Children)", max_users: :calculate_by_area},
|
140
|
+
# 1500 => {label: "1.5m (Adolescents)", max_users: :calculate_by_area},
|
141
|
+
# 1800 => {label: "1.8m (Adults)", max_users: :calculate_by_area}
|
142
|
+
# }
|
143
|
+
|
144
|
+
# Access all constants
|
145
|
+
EN14960::Constants::SLIDE_HEIGHT_THRESHOLDS
|
146
|
+
EN14960::Constants::MATERIAL_STANDARDS
|
147
|
+
EN14960::Constants::ANCHOR_CALCULATION_CONSTANTS
|
148
|
+
```
|
149
|
+
|
150
|
+
## EN 14960:2019 Compliance
|
151
|
+
|
152
|
+
This gem implements calculations based on BS EN 14960:2019, including:
|
153
|
+
|
154
|
+
- **Section 4.2.9**: Containment requirements (wall heights)
|
155
|
+
- **Section 4.2.11**: Slide runout requirements
|
156
|
+
- **Section 4.3**: Number of users calculations
|
157
|
+
- **Annex A**: Wind load and anchor calculations
|
158
|
+
- **Material specifications**: Fabric, thread, rope, and netting requirements
|
159
|
+
|
160
|
+
## Development
|
161
|
+
|
162
|
+
After checking out the repo, run `bundle install` to install dependencies. Then, run `bundle exec rspec` to run the tests.
|
163
|
+
|
164
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
165
|
+
|
166
|
+
## Releasing
|
167
|
+
|
168
|
+
This gem uses GitHub Actions for automated releases to RubyGems.
|
169
|
+
|
170
|
+
To release a new version:
|
171
|
+
|
172
|
+
1. Update the version number in `lib/en14960/version.rb`
|
173
|
+
2. Commit the change: `git commit -am "Bump version to x.y.z"`
|
174
|
+
3. Create a tag: `git tag -a vx.y.z -m "Release version x.y.z"`
|
175
|
+
4. Push the tag: `git push origin vx.y.z`
|
176
|
+
|
177
|
+
The GitHub Action will automatically:
|
178
|
+
- Build the gem
|
179
|
+
- Publish it to RubyGems
|
180
|
+
- Create a GitHub release with the gem file attached
|
181
|
+
|
182
|
+
**Note**: You need to set up RubyGems authentication in your GitHub repository settings. Add a repository secret named `GEM_HOST_API_KEY` with your RubyGems API key.
|
183
|
+
|
184
|
+
## Contributing
|
185
|
+
|
186
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/chobbledotcom/en14960.
|
187
|
+
|
188
|
+
## License
|
189
|
+
|
190
|
+
The gem is available as open source under the terms of the [GNU Affero General Public License v3.0](https://www.gnu.org/licenses/agpl-3.0.html).
|
191
|
+
|
192
|
+
## References
|
193
|
+
|
194
|
+
- BS EN 14960:2019 - Inflatable play equipment - Safety requirements and test methods
|
195
|
+
- EN 71-3 - Safety of toys - Migration of certain elements
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rspec/core/rake_task"
|
5
|
+
require "standard/rake"
|
6
|
+
|
7
|
+
RSpec::Core::RakeTask.new(:spec)
|
8
|
+
|
9
|
+
task default: %i[spec standard]
|
10
|
+
|
11
|
+
desc "Run tests with coverage"
|
12
|
+
task :coverage do
|
13
|
+
ENV["COVERAGE"] = "true"
|
14
|
+
Rake::Task["spec"].invoke
|
15
|
+
end
|
data/en14960.gemspec
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/en14960/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "en14960"
|
7
|
+
spec.version = EN14960::VERSION
|
8
|
+
spec.authors = ["Chobble.com"]
|
9
|
+
spec.email = ["hello@chobble.com"]
|
10
|
+
|
11
|
+
spec.summary = "BS EN 14960:2019 safety standard calculators for inflatable play equipment"
|
12
|
+
spec.description = "A Ruby gem providing calculators and validators for BS EN 14960:2019 - the safety standard for inflatable play equipment. Includes calculations for anchoring requirements, slide safety, user capacity, and material specifications."
|
13
|
+
spec.homepage = "https://github.com/chobbledotcom/en14960"
|
14
|
+
spec.license = "AGPL-3.0-or-later"
|
15
|
+
spec.required_ruby_version = ">= 3.0.0"
|
16
|
+
|
17
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
18
|
+
|
19
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
20
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
21
|
+
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
22
|
+
|
23
|
+
# Specify which files should be added to the gem when it is released.
|
24
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
25
|
+
spec.files = Dir.chdir(__dir__) do
|
26
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
27
|
+
(File.expand_path(f) == __FILE__) ||
|
28
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile]) ||
|
29
|
+
f.end_with?(".gem")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
spec.bindir = "exe"
|
33
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
34
|
+
spec.require_paths = ["lib"]
|
35
|
+
|
36
|
+
# No runtime dependencies - this gem is standalone
|
37
|
+
|
38
|
+
# Development dependencies
|
39
|
+
spec.add_development_dependency "bundler", "~> 2.0"
|
40
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
41
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
42
|
+
spec.add_development_dependency "standard", "~> 1.0"
|
43
|
+
spec.add_development_dependency "simplecov", "~> 0.21"
|
44
|
+
|
45
|
+
# For more information and examples about making a new gem, check out our
|
46
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
47
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../constants"
|
4
|
+
require_relative "../models/calculator_response"
|
5
|
+
|
6
|
+
module EN14960
|
7
|
+
module Calculators
|
8
|
+
module AnchorCalculator
|
9
|
+
extend self
|
10
|
+
|
11
|
+
def calculate_required_anchors(area_m2)
|
12
|
+
# EN 14960-1:2019 Annex A (Lines 1175-1210) - Anchor calculation formula
|
13
|
+
# Force = 0.5 × Cw × ρ × V² × A
|
14
|
+
# Where: Cw = 1.5, ρ = 1.24 kg/m³, V = 11.1 m/s (Lines 1194-1199)
|
15
|
+
# Number of anchors = Force / 1600N (Line 450 - each anchor withstands 1600N)
|
16
|
+
return 0 if area_m2.nil? || area_m2 <= 0
|
17
|
+
|
18
|
+
# Pre-calculated: 0.5 × 1.5 × 1.24 × 11.1² ≈ 114
|
19
|
+
area_coeff = Constants::ANCHOR_CALCULATION_CONSTANTS[:area_coefficient]
|
20
|
+
base_div = Constants::ANCHOR_CALCULATION_CONSTANTS[:base_divisor]
|
21
|
+
safety_mult = Constants::ANCHOR_CALCULATION_CONSTANTS[:safety_factor]
|
22
|
+
|
23
|
+
((area_m2.to_f * area_coeff * safety_mult) / base_div).ceil
|
24
|
+
end
|
25
|
+
|
26
|
+
def calculate(length:, width:, height:)
|
27
|
+
# EN 14960-1:2019 Lines 1175-1210 (Annex A) - Calculate exposed surface areas
|
28
|
+
front_area = (width * height).round(1)
|
29
|
+
sides_area = (length * height).round(1)
|
30
|
+
|
31
|
+
required_front = calculate_required_anchors(front_area)
|
32
|
+
required_sides = calculate_required_anchors(sides_area)
|
33
|
+
|
34
|
+
# EN 14960-1:2019 Line 1204 - Calculate for each side
|
35
|
+
total_required = (required_front + required_sides) * 2
|
36
|
+
|
37
|
+
# EN 14960-1:2019 Lines 441-442 - "Each inflatable shall have at least six anchorage points"
|
38
|
+
minimum = Constants::ANCHOR_CALCULATION_CONSTANTS[:minimum_anchors]
|
39
|
+
total_required = [total_required, minimum].max
|
40
|
+
|
41
|
+
area_coeff = Constants::ANCHOR_CALCULATION_CONSTANTS[:area_coefficient]
|
42
|
+
base_div = Constants::ANCHOR_CALCULATION_CONSTANTS[:base_divisor]
|
43
|
+
safety_mult = Constants::ANCHOR_CALCULATION_CONSTANTS[:safety_factor]
|
44
|
+
|
45
|
+
formula_front = "((#{front_area} × #{area_coeff} * #{safety_mult}) ÷ #{base_div} = #{required_front}"
|
46
|
+
formula_sides = "((#{sides_area} × #{area_coeff} * #{safety_mult}) ÷ #{base_div} = #{required_sides}"
|
47
|
+
|
48
|
+
calculated_total = (required_front + required_sides) * 2
|
49
|
+
|
50
|
+
breakdown = [
|
51
|
+
["Front/back area", "#{width}m (W) × #{height}m (H) = #{front_area}m²"],
|
52
|
+
["Sides area", "#{length}m (L) × #{height}m (H) = #{sides_area}m²"],
|
53
|
+
["Front & back anchor counts", formula_front],
|
54
|
+
["Left & right anchor counts", formula_sides],
|
55
|
+
["Required anchors", "(#{required_front} + #{required_sides}) × 2 = #{calculated_total}"]
|
56
|
+
]
|
57
|
+
|
58
|
+
# Add minimum requirement note if applicable
|
59
|
+
if calculated_total < minimum
|
60
|
+
breakdown << ["EN 14960 minimum", "Minimum #{minimum} anchors required, using #{minimum}"]
|
61
|
+
end
|
62
|
+
|
63
|
+
CalculatorResponse.new(
|
64
|
+
value: total_required,
|
65
|
+
value_suffix: "",
|
66
|
+
breakdown: breakdown
|
67
|
+
)
|
68
|
+
end
|
69
|
+
|
70
|
+
def anchor_formula_text
|
71
|
+
area_coeff = Constants::ANCHOR_CALCULATION_CONSTANTS[:area_coefficient]
|
72
|
+
base_div = Constants::ANCHOR_CALCULATION_CONSTANTS[:base_divisor]
|
73
|
+
safety_fact = Constants::ANCHOR_CALCULATION_CONSTANTS[:safety_factor]
|
74
|
+
"((Area × #{area_coeff} × #{safety_fact}) ÷ #{base_div})"
|
75
|
+
end
|
76
|
+
|
77
|
+
def anchor_calculation_description
|
78
|
+
"Anchors must be calculated based on the play area to ensure adequate ground restraint for wind loads."
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,254 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../constants"
|
4
|
+
require_relative "../models/calculator_response"
|
5
|
+
|
6
|
+
module EN14960
|
7
|
+
module Calculators
|
8
|
+
module SlideCalculator
|
9
|
+
extend self
|
10
|
+
|
11
|
+
# Simple calculation method that returns just the numeric value
|
12
|
+
def calculate_runout_value(platform_height, has_stop_wall: false)
|
13
|
+
return 0 if platform_height.nil? || platform_height <= 0
|
14
|
+
|
15
|
+
height_ratio = Constants::RUNOUT_CALCULATION_CONSTANTS[:platform_height_ratio]
|
16
|
+
minimum_runout = Constants::RUNOUT_CALCULATION_CONSTANTS[:minimum_runout_meters]
|
17
|
+
stop_wall_add = Constants::RUNOUT_CALCULATION_CONSTANTS[:stop_wall_addition]
|
18
|
+
|
19
|
+
calculated_runout = platform_height * height_ratio
|
20
|
+
base_runout = [calculated_runout, minimum_runout].max
|
21
|
+
|
22
|
+
has_stop_wall ? base_runout + stop_wall_add : base_runout
|
23
|
+
end
|
24
|
+
|
25
|
+
def calculate_required_runout(platform_height, has_stop_wall: false)
|
26
|
+
# EN 14960-1:2019 Section 4.2.11 (Lines 930-939) - Runout requirements
|
27
|
+
# Line 934-935: The runout distance must be at least half the height of the slide's
|
28
|
+
# highest platform (measured from ground level), with an absolute minimum of 300mm
|
29
|
+
# Line 936: If a stop-wall is installed at the runout's end, an additional
|
30
|
+
# 50cm must be added to the total runout length
|
31
|
+
return CalculatorResponse.new(value: 0, value_suffix: "m", breakdown: []) if platform_height.nil? || platform_height <= 0
|
32
|
+
|
33
|
+
# Get constants
|
34
|
+
height_ratio = Constants::RUNOUT_CALCULATION_CONSTANTS[:platform_height_ratio]
|
35
|
+
minimum_runout = Constants::RUNOUT_CALCULATION_CONSTANTS[:minimum_runout_meters]
|
36
|
+
stop_wall_add = Constants::RUNOUT_CALCULATION_CONSTANTS[:stop_wall_addition]
|
37
|
+
|
38
|
+
# Calculate values using the shared method
|
39
|
+
calculated_runout = platform_height * height_ratio
|
40
|
+
base_runout = calculate_runout_value(platform_height, has_stop_wall: false)
|
41
|
+
final_runout = calculate_runout_value(platform_height, has_stop_wall: has_stop_wall)
|
42
|
+
|
43
|
+
# Build breakdown
|
44
|
+
breakdown = [
|
45
|
+
["50% calculation", "#{platform_height}m × 0.5 = #{calculated_runout}m"],
|
46
|
+
["Minimum requirement", "#{minimum_runout}m (300mm)"],
|
47
|
+
["Base runout", "Maximum of #{calculated_runout}m and #{minimum_runout}m = #{base_runout}m"]
|
48
|
+
]
|
49
|
+
|
50
|
+
# Add stop-wall if applicable
|
51
|
+
if has_stop_wall
|
52
|
+
breakdown << ["Stop-wall addition", "#{base_runout}m + #{stop_wall_add}m = #{final_runout}m"]
|
53
|
+
end
|
54
|
+
|
55
|
+
CalculatorResponse.new(
|
56
|
+
value: final_runout,
|
57
|
+
value_suffix: "m",
|
58
|
+
breakdown: breakdown
|
59
|
+
)
|
60
|
+
end
|
61
|
+
|
62
|
+
def meets_height_requirements?(platform_height, user_height, containing_wall_height, has_permanent_roof)
|
63
|
+
# EN 14960-1:2019 Section 4.2.9 (Lines 854-887) - Containment requirements
|
64
|
+
# Lines 859-860: Containing walls become mandatory for platforms exceeding 0.6m in height
|
65
|
+
# Lines 861-862: Platforms between 0.6m and 3.0m need walls at least as tall as the maximum user height
|
66
|
+
# Lines 863-864: Platforms between 3.0m and 6.0m require walls at least 1.25 times the maximum user height OR a permanent roof
|
67
|
+
# Lines 865-866: Platforms over 6.0m must have both containing walls and a permanent roof structure
|
68
|
+
return false if platform_height.nil? || user_height.nil? || containing_wall_height.nil? || has_permanent_roof.nil?
|
69
|
+
|
70
|
+
enhanced_multiplier = Constants::WALL_HEIGHT_CONSTANTS[:enhanced_height_multiplier]
|
71
|
+
thresholds = Constants::SLIDE_HEIGHT_THRESHOLDS
|
72
|
+
|
73
|
+
case platform_height
|
74
|
+
when 0..thresholds[:no_walls_required]
|
75
|
+
true # No containing walls required
|
76
|
+
when (thresholds[:no_walls_required]..thresholds[:basic_walls])
|
77
|
+
containing_wall_height >= user_height
|
78
|
+
when (thresholds[:basic_walls]..thresholds[:enhanced_walls])
|
79
|
+
# EITHER walls at 1.25x user height OR permanent roof
|
80
|
+
has_permanent_roof || containing_wall_height >= (user_height * enhanced_multiplier)
|
81
|
+
when (thresholds[:enhanced_walls]..thresholds[:max_safe_height])
|
82
|
+
# BOTH containing walls AND permanent roof required
|
83
|
+
has_permanent_roof && containing_wall_height >= (user_height * enhanced_multiplier)
|
84
|
+
else
|
85
|
+
false # Exceeds safe height limits
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def meets_runout_requirements?(runout_length, platform_height, has_stop_wall: false)
|
90
|
+
# EN 14960-1:2019 Section 4.2.11 (Lines 930-939) - Runout requirements
|
91
|
+
# Lines 934-935: The runout area must extend at least half the platform's height
|
92
|
+
# or 300mm (whichever is greater) to allow users to decelerate safely
|
93
|
+
return false if runout_length.nil? || platform_height.nil?
|
94
|
+
|
95
|
+
required_runout = calculate_runout_value(platform_height, has_stop_wall: has_stop_wall)
|
96
|
+
runout_length >= required_runout
|
97
|
+
end
|
98
|
+
|
99
|
+
def slide_runout_formula_text
|
100
|
+
ratio_constant = Constants::RUNOUT_CALCULATION_CONSTANTS[:platform_height_ratio]
|
101
|
+
height_ratio = (ratio_constant * 100).to_i
|
102
|
+
min_constant = Constants::RUNOUT_CALCULATION_CONSTANTS[:minimum_runout_meters]
|
103
|
+
min_runout = (min_constant * 1000).to_i
|
104
|
+
"#{height_ratio}% of platform height, minimum #{min_runout}mm"
|
105
|
+
end
|
106
|
+
|
107
|
+
def calculate_wall_height_requirements(platform_height, user_height, has_permanent_roof = nil)
|
108
|
+
# EN 14960-1:2019 Section 4.2.9 (Lines 854-887) - Containment requirements
|
109
|
+
return CalculatorResponse.new(value: 0, value_suffix: "m", breakdown: []) if platform_height.nil? || user_height.nil? || platform_height <= 0 || user_height <= 0
|
110
|
+
|
111
|
+
# Get requirement details and breakdown
|
112
|
+
requirement_details = get_wall_height_requirement_details(platform_height, user_height, has_permanent_roof)
|
113
|
+
|
114
|
+
# Extract the required wall height from the details
|
115
|
+
required_height = extract_required_wall_height(platform_height, user_height)
|
116
|
+
|
117
|
+
CalculatorResponse.new(
|
118
|
+
value: required_height,
|
119
|
+
value_suffix: "m",
|
120
|
+
breakdown: requirement_details[:breakdown]
|
121
|
+
)
|
122
|
+
end
|
123
|
+
|
124
|
+
def get_wall_height_requirement_details(platform_height, user_height, has_permanent_roof)
|
125
|
+
thresholds = Constants::SLIDE_HEIGHT_THRESHOLDS
|
126
|
+
enhanced_multiplier = Constants::WALL_HEIGHT_CONSTANTS[:enhanced_height_multiplier]
|
127
|
+
|
128
|
+
case platform_height
|
129
|
+
when 0..thresholds[:no_walls_required]
|
130
|
+
{
|
131
|
+
text: "No containing walls required",
|
132
|
+
breakdown: [
|
133
|
+
["Height range", "Under 0.6m"],
|
134
|
+
["Requirement", "No containing walls required"]
|
135
|
+
]
|
136
|
+
}
|
137
|
+
when (thresholds[:no_walls_required]..thresholds[:basic_walls])
|
138
|
+
{
|
139
|
+
text: "Walls must be at least #{user_height}m (equal to user height)",
|
140
|
+
breakdown: [
|
141
|
+
["Height range", "0.6m - 3.0m"],
|
142
|
+
["Calculation", "#{user_height}m (user height)"]
|
143
|
+
]
|
144
|
+
}
|
145
|
+
when (thresholds[:basic_walls]..thresholds[:enhanced_walls])
|
146
|
+
required_height = (user_height * enhanced_multiplier).round(2)
|
147
|
+
breakdown = [
|
148
|
+
["Height range", "3.0m - 6.0m"],
|
149
|
+
["Calculation", "#{user_height}m × #{enhanced_multiplier} = #{required_height}m"],
|
150
|
+
["Alternative requirement", "Permanent roof (can replace heightened walls)"]
|
151
|
+
]
|
152
|
+
|
153
|
+
# Add roof status if known
|
154
|
+
if !has_permanent_roof.nil?
|
155
|
+
breakdown << if has_permanent_roof
|
156
|
+
["Permanent roof", "Fitted ✓"]
|
157
|
+
else
|
158
|
+
["Permanent roof", "Not fitted ✗"]
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
{
|
163
|
+
text: "Walls must be at least #{required_height}m (1.25× user height)",
|
164
|
+
breakdown: breakdown
|
165
|
+
}
|
166
|
+
when (thresholds[:enhanced_walls]..thresholds[:max_safe_height])
|
167
|
+
required_height = (user_height * enhanced_multiplier).round(2)
|
168
|
+
breakdown = [
|
169
|
+
["Height range", "Over 6.0m"],
|
170
|
+
["Calculation", "#{user_height}m × #{enhanced_multiplier} = #{required_height}m"],
|
171
|
+
["Additional requirement", "Permanent roof required"]
|
172
|
+
]
|
173
|
+
|
174
|
+
# Add roof status if known
|
175
|
+
if !has_permanent_roof.nil?
|
176
|
+
breakdown << if has_permanent_roof
|
177
|
+
["Permanent roof", "Required and fitted ✓"]
|
178
|
+
else
|
179
|
+
["Permanent roof", "Required but not fitted ✗"]
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
{
|
184
|
+
text: "Walls must be at least #{required_height}m + permanent roof required",
|
185
|
+
breakdown: breakdown
|
186
|
+
}
|
187
|
+
else
|
188
|
+
{
|
189
|
+
text: "Platform height exceeds safe limits",
|
190
|
+
breakdown: [
|
191
|
+
["Status", "Platform height exceeds safe limits"]
|
192
|
+
]
|
193
|
+
}
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def requires_permanent_roof?(platform_height)
|
198
|
+
# EN 14960-1:2019 Section 4.2.9 (Lines 865-866)
|
199
|
+
# Inflatable structures with platforms higher than 6.0m must be equipped
|
200
|
+
# with both containing walls and a permanent roof
|
201
|
+
threshold = Constants::SLIDE_HEIGHT_THRESHOLDS[:enhanced_walls]
|
202
|
+
!platform_height.nil? && platform_height > threshold
|
203
|
+
end
|
204
|
+
|
205
|
+
def wall_height_requirement
|
206
|
+
multiplier = Constants::WALL_HEIGHT_CONSTANTS[:enhanced_height_multiplier]
|
207
|
+
"Containing walls required #{multiplier} times user height"
|
208
|
+
end
|
209
|
+
|
210
|
+
def slide_calculations
|
211
|
+
# EN 14960:2019 - Comprehensive slide safety requirements
|
212
|
+
{
|
213
|
+
containing_wall_heights: {
|
214
|
+
under_600mm: "No containing walls required",
|
215
|
+
between_600_3000mm: "Containing walls required of user height",
|
216
|
+
between_3000_6000mm: wall_height_requirement,
|
217
|
+
over_6000mm: "Both containing walls AND permanent roof required"
|
218
|
+
},
|
219
|
+
runout_requirements: {
|
220
|
+
minimum_length: "50% of highest platform height",
|
221
|
+
absolute_minimum: "300mm in any case",
|
222
|
+
maximum_inclination: "Not more than 10°",
|
223
|
+
stop_wall_addition: "If fitted, adds 50cm to required run-out length",
|
224
|
+
wall_height_requirement: "50% of user height on run-out sides"
|
225
|
+
},
|
226
|
+
safety_factors: {
|
227
|
+
first_metre_gradient: "Special requirements for first metre of slope",
|
228
|
+
surface_requirements: "Non-slip surface material required",
|
229
|
+
edge_protection: "Rounded edges and smooth transitions"
|
230
|
+
}
|
231
|
+
}
|
232
|
+
end
|
233
|
+
|
234
|
+
private
|
235
|
+
|
236
|
+
def extract_required_wall_height(platform_height, user_height)
|
237
|
+
thresholds = Constants::SLIDE_HEIGHT_THRESHOLDS
|
238
|
+
enhanced_multiplier = Constants::WALL_HEIGHT_CONSTANTS[:enhanced_height_multiplier]
|
239
|
+
|
240
|
+
case platform_height
|
241
|
+
when 0..thresholds[:no_walls_required]
|
242
|
+
0 # No walls required
|
243
|
+
when (thresholds[:no_walls_required]..thresholds[:basic_walls])
|
244
|
+
user_height # Equal to user height
|
245
|
+
when (thresholds[:basic_walls]..thresholds[:enhanced_walls]),
|
246
|
+
(thresholds[:enhanced_walls]..thresholds[:max_safe_height])
|
247
|
+
(user_height * enhanced_multiplier).round(2) # 1.25× user height
|
248
|
+
else
|
249
|
+
0 # Exceeds safe limits
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../constants"
|
4
|
+
require_relative "../models/calculator_response"
|
5
|
+
|
6
|
+
module EN14960
|
7
|
+
module Calculators
|
8
|
+
module UserCapacityCalculator
|
9
|
+
extend self
|
10
|
+
|
11
|
+
def calculate(length, width, max_user_height = nil, negative_adjustment_area = 0)
|
12
|
+
return default_result if length.nil? || width.nil?
|
13
|
+
|
14
|
+
total_area = (length * width).round(2)
|
15
|
+
negative_adjustment_area = negative_adjustment_area.to_f.abs
|
16
|
+
usable_area = [total_area - negative_adjustment_area, 0].max.round(2)
|
17
|
+
|
18
|
+
breakdown = build_breakdown(length, width, total_area, negative_adjustment_area, usable_area)
|
19
|
+
capacities = calculate_capacities(usable_area, max_user_height, breakdown)
|
20
|
+
|
21
|
+
CalculatorResponse.new(
|
22
|
+
value: capacities,
|
23
|
+
value_suffix: "",
|
24
|
+
breakdown: breakdown
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def build_breakdown(length, width, total_area, negative_adjustment_area, usable_area)
|
31
|
+
breakdown = []
|
32
|
+
formatted_length = format_number(length)
|
33
|
+
formatted_width = format_number(width)
|
34
|
+
formatted_total = format_number(total_area)
|
35
|
+
formatted_usable = format_number(usable_area)
|
36
|
+
|
37
|
+
breakdown << ["Total area", "#{formatted_length}m × #{formatted_width}m = #{formatted_total}m²"]
|
38
|
+
|
39
|
+
if negative_adjustment_area > 0
|
40
|
+
formatted_adjustment = format_number(negative_adjustment_area)
|
41
|
+
breakdown << ["Obstacles/adjustments", "- #{formatted_adjustment}m²"]
|
42
|
+
end
|
43
|
+
|
44
|
+
breakdown << ["Usable area", "#{formatted_usable}m²"]
|
45
|
+
breakdown << ["Capacity calculations", "Based on usable area"]
|
46
|
+
|
47
|
+
breakdown
|
48
|
+
end
|
49
|
+
|
50
|
+
def calculate_capacities(usable_area, max_user_height, breakdown)
|
51
|
+
capacities = {}
|
52
|
+
|
53
|
+
Constants::AREA_DIVISOR.each do |height_mm, divisor|
|
54
|
+
height_m = height_mm / 1000.0
|
55
|
+
key = :"users_#{height_mm}mm"
|
56
|
+
|
57
|
+
if max_user_height.nil? || height_m <= max_user_height
|
58
|
+
capacity = (usable_area / divisor).floor
|
59
|
+
capacities[key] = capacity
|
60
|
+
formatted_area = format_number(usable_area)
|
61
|
+
formatted_divisor = format_number(divisor)
|
62
|
+
calculation = "#{formatted_area} ÷ #{formatted_divisor} = #{capacity} "
|
63
|
+
calculation += (capacity == 1) ? "user" : "users"
|
64
|
+
breakdown << ["#{format_number(height_m)}m users", calculation]
|
65
|
+
else
|
66
|
+
capacities[key] = 0
|
67
|
+
breakdown << ["#{format_number(height_m)}m users", "Not allowed (exceeds height limit)"]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
capacities
|
72
|
+
end
|
73
|
+
|
74
|
+
def default_result
|
75
|
+
CalculatorResponse.new(
|
76
|
+
value: default_capacity,
|
77
|
+
value_suffix: "",
|
78
|
+
breakdown: [["Invalid dimensions", ""]]
|
79
|
+
)
|
80
|
+
end
|
81
|
+
|
82
|
+
def default_capacity
|
83
|
+
{
|
84
|
+
users_1000mm: 0,
|
85
|
+
users_1200mm: 0,
|
86
|
+
users_1500mm: 0,
|
87
|
+
users_1800mm: 0
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
def format_number(number)
|
92
|
+
# Remove trailing zeros after decimal point
|
93
|
+
formatted = sprintf("%.1f", number)
|
94
|
+
formatted.sub(/\.0$/, "")
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module EN14960
|
4
|
+
module Constants
|
5
|
+
# Height category constants based on EN 14960:2019
|
6
|
+
HEIGHT_CATEGORIES = {
|
7
|
+
1000 => {label: "1.0m (Young children)", max_users: :calculate_by_area},
|
8
|
+
1200 => {label: "1.2m (Children)", max_users: :calculate_by_area},
|
9
|
+
1500 => {label: "1.5m (Adolescents)", max_users: :calculate_by_area},
|
10
|
+
1800 => {label: "1.8m (Adults)", max_users: :calculate_by_area}
|
11
|
+
}.freeze
|
12
|
+
|
13
|
+
# Grounding test weights by user height (EN 14960:2019)
|
14
|
+
GROUNDING_TEST_WEIGHTS = {
|
15
|
+
height_1000mm: 25, # kg test weight for 1.0m users
|
16
|
+
height_1200mm: 35, # kg test weight for 1.2m users
|
17
|
+
height_1500mm: 65, # kg test weight for 1.5m users
|
18
|
+
height_1800mm: 85 # kg test weight for 1.8m users
|
19
|
+
}.freeze
|
20
|
+
|
21
|
+
# Reinspection interval
|
22
|
+
REINSPECTION_INTERVAL_DAYS = 365 # days
|
23
|
+
|
24
|
+
# Anchor calculation constants from EN 14960-1:2019
|
25
|
+
# Line 450: Each anchor must withstand 1600N force
|
26
|
+
# Lines 441-442: Minimum 6 anchorage points required
|
27
|
+
# Lines 1194-1199: Cw=1.5, ρ=1.24 kg/m³, V=11.1 m/s
|
28
|
+
# Pre-calculated: 0.5 × 1.5 × 1.24 × 11.1² ≈ 114
|
29
|
+
ANCHOR_CALCULATION_CONSTANTS = {
|
30
|
+
area_coefficient: 114.0, # Pre-calculated wind force coefficient
|
31
|
+
base_divisor: 1600.0, # Force per anchor in Newtons (Line 450)
|
32
|
+
safety_factor: 1.5, # Safety factor multiplier
|
33
|
+
minimum_anchors: 6 # Minimum required anchors (Lines 441-442)
|
34
|
+
}.freeze
|
35
|
+
|
36
|
+
# Slide safety thresholds
|
37
|
+
SLIDE_HEIGHT_THRESHOLDS = {
|
38
|
+
no_walls_required: 0.6, # Under 600mm
|
39
|
+
basic_walls: 3.0, # 600mm - 3000mm
|
40
|
+
enhanced_walls: 6.0, # 3000mm - 6000mm
|
41
|
+
max_safe_height: 8.0 # Maximum recommended height
|
42
|
+
}.freeze
|
43
|
+
|
44
|
+
# Slide runout calculation constants (EN 14960:2019)
|
45
|
+
RUNOUT_CALCULATION_CONSTANTS = {
|
46
|
+
platform_height_ratio: 0.5, # 50% of platform height
|
47
|
+
minimum_runout_meters: 0.3, # Absolute minimum 300mm (0.3m)
|
48
|
+
stop_wall_addition: 0.5 # 50cm addition when stop-wall fitted (Line 936)
|
49
|
+
}.freeze
|
50
|
+
|
51
|
+
# Wall height calculation constants (EN 14960:2019)
|
52
|
+
WALL_HEIGHT_CONSTANTS = {
|
53
|
+
enhanced_height_multiplier: 1.25 # 1.25× multiplier for enhanced walls
|
54
|
+
}.freeze
|
55
|
+
|
56
|
+
# EN 14960-1:2019 Section 4.3 (Lines 940-961) - Number of users
|
57
|
+
# Note: EN 14960 doesn't specify exact calculation formulas, only factors to consider:
|
58
|
+
# - Height of user (Line 946)
|
59
|
+
# - Size of playing area (Line 954)
|
60
|
+
# - Type of activity (Line 956)
|
61
|
+
# The calculation below is industry standard practice, not from EN 14960
|
62
|
+
AREA_DIVISOR = {
|
63
|
+
1000 => 1.0, # 1 user per m² for 1.0m height
|
64
|
+
1200 => 1.33, # 0.75 users per m² for 1.2m height
|
65
|
+
1500 => 1.66, # 0.60 users per m² for 1.5m height
|
66
|
+
1800 => 2.0 # 0.5 users per m² for 1.8m height
|
67
|
+
}.freeze
|
68
|
+
|
69
|
+
# Material safety standards (EN 14960:2019 & EN 71-3)
|
70
|
+
MATERIAL_STANDARDS = {
|
71
|
+
fabric: {
|
72
|
+
min_tensile_strength: 1850, # Newtons minimum
|
73
|
+
min_tear_strength: 350, # Newtons minimum
|
74
|
+
fire_standard: "EN 71-3" # Fire retardancy standard
|
75
|
+
},
|
76
|
+
thread: {
|
77
|
+
min_tensile_strength: 88 # Newtons minimum
|
78
|
+
},
|
79
|
+
rope: {
|
80
|
+
min_diameter: 18, # mm minimum
|
81
|
+
max_diameter: 45, # mm maximum
|
82
|
+
max_swing_percentage: 20 # % maximum swing
|
83
|
+
},
|
84
|
+
netting: {
|
85
|
+
max_vertical_mesh: 30, # mm maximum for >1m height
|
86
|
+
max_roof_mesh: 8 # mm maximum
|
87
|
+
}
|
88
|
+
}.freeze
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module EN14960
|
4
|
+
# Data class for calculator responses
|
5
|
+
# Provides a consistent structure for all calculator results
|
6
|
+
class CalculatorResponse < Struct.new(:value, :value_suffix, :breakdown, keyword_init: true)
|
7
|
+
def initialize(value:, value_suffix: "", breakdown: [])
|
8
|
+
super
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_h
|
12
|
+
{
|
13
|
+
value: value,
|
14
|
+
value_suffix: value_suffix,
|
15
|
+
breakdown: breakdown
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
alias_method :as_json, :to_h
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../constants"
|
4
|
+
|
5
|
+
module EN14960
|
6
|
+
module Validators
|
7
|
+
module MaterialValidator
|
8
|
+
extend self
|
9
|
+
|
10
|
+
def valid_rope_diameter?(diameter_mm)
|
11
|
+
# EN 14960:2019 - Rope diameter range prevents finger entrapment while
|
12
|
+
# ensuring adequate grip and structural strength
|
13
|
+
return false if diameter_mm.nil?
|
14
|
+
|
15
|
+
min_diameter = Constants::MATERIAL_STANDARDS[:rope][:min_diameter]
|
16
|
+
max_diameter = Constants::MATERIAL_STANDARDS[:rope][:max_diameter]
|
17
|
+
diameter_mm.between?(min_diameter, max_diameter)
|
18
|
+
end
|
19
|
+
|
20
|
+
def fabric_tensile_requirement
|
21
|
+
fabric_standards = Constants::MATERIAL_STANDARDS[:fabric]
|
22
|
+
"#{fabric_standards[:min_tensile_strength]} Newtons minimum"
|
23
|
+
end
|
24
|
+
|
25
|
+
def fabric_tear_requirement
|
26
|
+
fabric_standards = Constants::MATERIAL_STANDARDS[:fabric]
|
27
|
+
"#{fabric_standards[:min_tear_strength]} Newtons minimum"
|
28
|
+
end
|
29
|
+
|
30
|
+
# Additional validation methods
|
31
|
+
def valid_fabric_tensile_strength?(strength_n)
|
32
|
+
return false if strength_n.nil?
|
33
|
+
strength_n >= Constants::MATERIAL_STANDARDS[:fabric][:min_tensile_strength]
|
34
|
+
end
|
35
|
+
|
36
|
+
def valid_fabric_tear_strength?(strength_n)
|
37
|
+
return false if strength_n.nil?
|
38
|
+
strength_n >= Constants::MATERIAL_STANDARDS[:fabric][:min_tear_strength]
|
39
|
+
end
|
40
|
+
|
41
|
+
def valid_thread_tensile_strength?(strength_n)
|
42
|
+
return false if strength_n.nil?
|
43
|
+
strength_n >= Constants::MATERIAL_STANDARDS[:thread][:min_tensile_strength]
|
44
|
+
end
|
45
|
+
|
46
|
+
def valid_netting_mesh?(mesh_mm, is_roof: false)
|
47
|
+
return false if mesh_mm.nil?
|
48
|
+
|
49
|
+
max_mesh = is_roof ?
|
50
|
+
Constants::MATERIAL_STANDARDS[:netting][:max_roof_mesh] :
|
51
|
+
Constants::MATERIAL_STANDARDS[:netting][:max_vertical_mesh]
|
52
|
+
|
53
|
+
mesh_mm <= max_mesh
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/lib/en14960.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "en14960/version"
|
4
|
+
require_relative "en14960/models/calculator_response"
|
5
|
+
require_relative "en14960/constants"
|
6
|
+
require_relative "en14960/calculators/anchor_calculator"
|
7
|
+
require_relative "en14960/calculators/slide_calculator"
|
8
|
+
require_relative "en14960/calculators/user_capacity_calculator"
|
9
|
+
require_relative "en14960/validators/material_validator"
|
10
|
+
|
11
|
+
# EN14960 provides calculators and validators for BS EN 14960:2019
|
12
|
+
# the safety standard for inflatable play equipment
|
13
|
+
module EN14960
|
14
|
+
class Error < StandardError; end
|
15
|
+
|
16
|
+
# Public API methods for easy access to calculators
|
17
|
+
class << self
|
18
|
+
# Calculate required anchors for inflatable play equipment
|
19
|
+
# @param length [Float] Length in meters
|
20
|
+
# @param width [Float] Width in meters
|
21
|
+
# @param height [Float] Height in meters
|
22
|
+
# @return [CalculatorResponse] Response with anchor count and breakdown
|
23
|
+
def calculate_anchors(length:, width:, height:)
|
24
|
+
Calculators::AnchorCalculator.calculate(length: length, width: width, height: height)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Calculate required slide runout distance
|
28
|
+
# @param platform_height [Float] Platform height in meters
|
29
|
+
# @param has_stop_wall [Boolean] Whether a stop wall is fitted
|
30
|
+
# @return [CalculatorResponse] Response with runout distance and breakdown
|
31
|
+
def calculate_slide_runout(platform_height, has_stop_wall: false)
|
32
|
+
Calculators::SlideCalculator.calculate_required_runout(platform_height, has_stop_wall: has_stop_wall)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Calculate wall height requirements for slides
|
36
|
+
# @param platform_height [Float] Platform height in meters
|
37
|
+
# @param user_height [Float] Maximum user height in meters
|
38
|
+
# @param has_permanent_roof [Boolean] Whether unit has permanent roof
|
39
|
+
# @return [CalculatorResponse] Response with wall height requirements
|
40
|
+
def calculate_wall_height(platform_height, user_height, has_permanent_roof = nil)
|
41
|
+
Calculators::SlideCalculator.calculate_wall_height_requirements(
|
42
|
+
platform_height,
|
43
|
+
user_height,
|
44
|
+
has_permanent_roof
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Calculate user capacity based on play area
|
49
|
+
# @param length [Float] Length in meters
|
50
|
+
# @param width [Float] Width in meters
|
51
|
+
# @param max_user_height [Float, nil] Maximum allowed user height
|
52
|
+
# @param negative_adjustment_area [Float] Area to subtract for obstacles
|
53
|
+
# @return [CalculatorResponse] Response with capacity by user height
|
54
|
+
def calculate_user_capacity(length, width, max_user_height = nil, negative_adjustment_area = 0)
|
55
|
+
Calculators::UserCapacityCalculator.calculate(
|
56
|
+
length,
|
57
|
+
width,
|
58
|
+
max_user_height,
|
59
|
+
negative_adjustment_area
|
60
|
+
)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Check if rope diameter meets safety requirements
|
64
|
+
# @param diameter_mm [Float] Rope diameter in millimeters
|
65
|
+
# @return [Boolean] Whether diameter is within safe range
|
66
|
+
def valid_rope_diameter?(diameter_mm)
|
67
|
+
Validators::MaterialValidator.valid_rope_diameter?(diameter_mm)
|
68
|
+
end
|
69
|
+
|
70
|
+
# Get height categories defined by EN 14960:2019
|
71
|
+
# @return [Hash] Height categories with labels and requirements
|
72
|
+
def height_categories
|
73
|
+
Constants::HEIGHT_CATEGORIES
|
74
|
+
end
|
75
|
+
|
76
|
+
# Get material standards defined by EN 14960:2019
|
77
|
+
# @return [Hash] Material requirements for fabrics, threads, ropes, and netting
|
78
|
+
def material_standards
|
79
|
+
Constants::MATERIAL_STANDARDS
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
metadata
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: en14960
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Chobble.com
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-07-28 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '13.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '13.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: standard
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: simplecov
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.21'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.21'
|
83
|
+
description: A Ruby gem providing calculators and validators for BS EN 14960:2019
|
84
|
+
- the safety standard for inflatable play equipment. Includes calculations for anchoring
|
85
|
+
requirements, slide safety, user capacity, and material specifications.
|
86
|
+
email:
|
87
|
+
- hello@chobble.com
|
88
|
+
executables: []
|
89
|
+
extensions: []
|
90
|
+
extra_rdoc_files: []
|
91
|
+
files:
|
92
|
+
- ".rspec"
|
93
|
+
- ".rspec_status"
|
94
|
+
- CHANGELOG.md
|
95
|
+
- LICENSE
|
96
|
+
- LICENSE.txt
|
97
|
+
- README.md
|
98
|
+
- Rakefile
|
99
|
+
- en14960.gemspec
|
100
|
+
- lib/en14960.rb
|
101
|
+
- lib/en14960/calculators/anchor_calculator.rb
|
102
|
+
- lib/en14960/calculators/slide_calculator.rb
|
103
|
+
- lib/en14960/calculators/user_capacity_calculator.rb
|
104
|
+
- lib/en14960/constants.rb
|
105
|
+
- lib/en14960/models/calculator_response.rb
|
106
|
+
- lib/en14960/validators/material_validator.rb
|
107
|
+
- lib/en14960/version.rb
|
108
|
+
homepage: https://github.com/chobbledotcom/en14960
|
109
|
+
licenses:
|
110
|
+
- AGPL-3.0-or-later
|
111
|
+
metadata:
|
112
|
+
allowed_push_host: https://rubygems.org
|
113
|
+
homepage_uri: https://github.com/chobbledotcom/en14960
|
114
|
+
source_code_uri: https://github.com/chobbledotcom/en14960
|
115
|
+
changelog_uri: https://github.com/chobbledotcom/en14960/blob/main/CHANGELOG.md
|
116
|
+
post_install_message:
|
117
|
+
rdoc_options: []
|
118
|
+
require_paths:
|
119
|
+
- lib
|
120
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: 3.0.0
|
125
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
126
|
+
requirements:
|
127
|
+
- - ">="
|
128
|
+
- !ruby/object:Gem::Version
|
129
|
+
version: '0'
|
130
|
+
requirements: []
|
131
|
+
rubygems_version: 3.4.19
|
132
|
+
signing_key:
|
133
|
+
specification_version: 4
|
134
|
+
summary: BS EN 14960:2019 safety standard calculators for inflatable play equipment
|
135
|
+
test_files: []
|