Class: RoomPlan

Inherits:
ApplicationRecord show all
Defined in:
app/models/room_plan.rb

Overview

== Schema Information

Table name: room_plans
Database name: primary

id :integer not null, primary key
heated_area :jsonb
heated_area_offset :decimal(, )
heated_sq_ft :decimal(, )
parent_room :integer
points :jsonb
room_area :decimal(, )
room_plan_fixtures_count :integer default(0), not null
step :integer
units :string
created_at :datetime not null
updated_at :datetime not null
party_id :integer

Indexes

index_room_plans_on_party_id (party_id) USING hash

Foreign Keys

fk_rails_... (party_id => parties.id) ON DELETE => cascade

Constant Summary collapse

DEFAULT_POINTS =
[
  { x: 243.84296513045595, y: 243.84296513045595 },
  { x: 731.5288953913679, y: 243.84296513045595 },
  { x: 731.5288953913679, y: 731.5288953913679 },
  { x: 243.84296513045595, y: 731.5288953913679 }
]
SCREEN_UNITS_IN_A_METER =
200
FEET_IN_A_METER =
3.2808
SCREEN_UNITS_IN_A_FOOT =

60.9607412826

SCREEN_UNITS_IN_A_METER / FEET_IN_A_METER
SCREEN_UNITS_IN_AN_INCH =

5.08006177355

SCREEN_UNITS_IN_A_FOOT / 12
DEFAULT_HEATED_AREA_WALL_OFFSET =
4.0

Belongs to collapse

Has many collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from ApplicationRecord

ransackable_associations, ransackable_attributes, ransortable_attributes, #to_relation

Methods included from Models::EventPublishable

#publish_event

Class Method Details

.get_nominal_wall_length(d) ⇒ Object



224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'app/models/room_plan.rb', line 224

def self.get_nominal_wall_length(d)
  d_in_ft = (d / SCREEN_UNITS_IN_A_FOOT).round(12)
  ft = d_in_ft.floor
  inches = ((d_in_ft - ft) * 12).round(2)

  if inches % 1 == 0
    inches = inches.to_i
  end

  if inches == 0
    inches = ''
  end
  "#{ft}' #{inches}#{inches != '' ? '"' : ''}"
end

.has_fixturesActiveRecord::Relation<RoomPlan>

A relation of RoomPlans that are has fixtures. Active Record Scope

Returns:

See Also:



52
# File 'app/models/room_plan.rb', line 52

scope :has_fixtures, ->(val = 1) { where(RoomPlan[:room_plan_fixtures_count].gteq(val)) }

.migrate_legacy_fixturesObject

This is a destructive operation, beware



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'app/models/room_plan.rb', line 77

def self.migrate_legacy_fixtures
  RoomPlanFixture.delete_all
  dt_key_map = DesignToolFixture.all.each_with_object({}) {|dtf, hsh| hsh[dtf['class_key']] = dtf.id}
  puts "Design Tool Fixture map initializex with #{dt_key_map.size} entries"
  RoomPlanFixture.transaction do
    RoomPlan.find_each do |rp|
      puts "Processing room plan #{rp.id}"
      (rp.legacy_fixtures || {}).each do |f|
        if class_key = f['key']
          puts " - Converting fixture #{class_key}"
          dtf_id = dt_key_map[class_key]
          rp.room_plan_fixtures.create!(
            design_tool_fixture_id: dtf_id,
            fixture_data: f
          )
        else
          puts " !! fixture class_key: #{class_key} is missing"
        end
      end
    end
  end
end

.options_for_select(ev = nil) ⇒ Object



63
64
65
66
# File 'app/models/room_plan.rb', line 63

def self.options_for_select(ev=nil)
  res = all
  res.map {|rp| [rp.selection_name, rp.id]}
end

.ransackable_scopes(auth_object = nil) ⇒ Object



72
73
74
# File 'app/models/room_plan.rb', line 72

def self.ransackable_scopes(auth_object = nil)
  [:has_fixtures]
end

Instance Method Details

#build_heated_areaObject



146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'app/models/room_plan.rb', line 146

def build_heated_area
  heated_area&.reduce('') do |pathAcc, area| # Take all areas
    i = 0
    pathAcc << area.reduce('') do |haAcc, shape| # Every area has 0+ shapes (arrays of xy point arrays--GEOJson)
      if shape.length === 1 and shape[0].kind_of? Array
        shape = shape[0]
        if shape.length === 1 and shape[0].kind_of? Array
          shape = shape[0]
        end
      end
      points = i.positive? ? shape.reverse : shape # Need to reverse if we are making holes
      i += 1
      # Reduce points to a SVG path string
      points.reduce("#{haAcc} M #{points[0][0]},#{points[0][1]}") do |acc, (x, y)|
        "#{acc} L #{x.to_f},#{y.to_f}"
      end + 'z ' # Close out shape
    end
  end
end

#build_wallsObject



166
167
168
169
170
171
# File 'app/models/room_plan.rb', line 166

def build_walls
  walls = points.map { |wall| [wall['x'], wall['y']] }
  walls.reduce("M #{walls[0][0]}, #{walls[0][1]} ") do |acc, (x, y)|
    "#{acc} L #{x} #{y}"
  end + ' z'
end

#clone(party_for_clone = nil) ⇒ Object



273
274
275
276
277
278
# File 'app/models/room_plan.rb', line 273

def clone(party_for_clone=nil)
  my_clone = self.deep_dup
  my_clone.party = party_for_clone if party_for_clone.present?
  my_clone.save
  my_clone
end

#deep_dupObject



57
58
59
60
61
# File 'app/models/room_plan.rb', line 57

def deep_dup
  deep_clone(include: :room_plan_fixtures) do |original, copy|
    copy.parent_room = original.id if copy.is_a?(RoomPlan)
  end
end

#determine_system_type_for_room_planObject



239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'app/models/room_plan.rb', line 239

def determine_system_type_for_room_plan
  # Get all room configurations associated with this room plan
  rcs = room_configurations.includes(:room_type)

  if rcs.empty?
    # Default to floor-heating if no room configurations
    'floor-heating'
  else
    # Get unique environments from room types
    environments = rcs.map { |rc| rc.room_type&.environment }.compact.uniq

    if environments.include?('Outdoor')
      # If any room is outdoor, use snow-melting
      'snow-melting'
    else
      # Default to floor-heating for indoor or mixed environments
      'floor-heating'
    end
  end
end

#editing_locked?Boolean

Returns:

  • (Boolean)


265
266
267
# File 'app/models/room_plan.rb', line 265

def editing_locked?
  room_configurations.any? { |rc| rc.room_plan_editing_locked? }
end

#fixturesObject



206
207
208
# File 'app/models/room_plan.rb', line 206

def fixtures
  room_plan_fixtures.map(&:fixture_data)
end

#get_distance_between_points(a, b) ⇒ Object



198
199
200
# File 'app/models/room_plan.rb', line 198

def get_distance_between_points(a, b)
  Math.sqrt((a['x'] - b['x']) ** 2 + (a['y'] - b['y']) ** 2);
end

#get_next_point(index) ⇒ Object



202
203
204
# File 'app/models/room_plan.rb', line 202

def get_next_point(index)
  points[modulo(index + 1, points.length)]
end

#get_viewboxObject



181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'app/models/room_plan.rb', line 181

def get_viewbox
  all_x, all_y = points.reduce([[], []]) { |(acc_x, acc_y), h| [[*acc_x, h['x']], [*acc_y, h['y']]] }
  x_min = all_x.min
  y_min = all_y.min
  x_size = all_x.max - x_min
  y_size = all_y.max - y_min
  padding = 100
  padding_x = padding #* (y_size / x_size)
  padding_y = padding #* (y_size / x_size)
  [
    x_min - padding_x,
    y_min - padding_y,
    x_size + padding_x * 2,
    y_size + padding_y * 2
  ].join(' ')
end

#hydrated_fixtures(include_visual: true) ⇒ Object



210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'app/models/room_plan.rb', line 210

def hydrated_fixtures(include_visual: true)
  preloaded_room_plan_fixtures.map do |rpf|
    hsh = {
      **rpf.fixture_data.symbolize_keys,
      width: rpf.design_tool_fixture['size_x'] * SCREEN_UNITS_IN_AN_INCH,
      height: rpf.design_tool_fixture['size_y'] * SCREEN_UNITS_IN_AN_INCH
    }
    if include_visual
      hsh[:visual] = rpf.design_tool_fixture['visual']
    end
    hsh
  end
end

#is_designable?Boolean

Returns:

  • (Boolean)


177
178
179
# File 'app/models/room_plan.rb', line 177

def is_designable?
  is_valid? && room_area.present? && fixtures.any? { |f| f['key'] == 'thermostat' }
end

#is_valid?Boolean

Returns:

  • (Boolean)


173
174
175
# File 'app/models/room_plan.rb', line 173

def is_valid?
  points.present? && fixtures.present?
end

#modulo(n, m) ⇒ Object



269
270
271
# File 'app/models/room_plan.rb', line 269

def modulo(n, m)
  ((n % m) + m) % m;
end

#ok_to_delete?Boolean

Returns:

  • (Boolean)


261
262
263
# File 'app/models/room_plan.rb', line 261

def ok_to_delete?
  room_configurations.empty?
end

#owned_or_cloned(customer) ⇒ Object



280
281
282
283
284
285
286
# File 'app/models/room_plan.rb', line 280

def owned_or_cloned(customer)
  if party_id == customer.id
    self
  else
    clone(customer)
  end
end

#partyParty

Returns:

See Also:



46
# File 'app/models/room_plan.rb', line 46

belongs_to :party, inverse_of: :room_plans, optional: true

#preloaded_room_plan_fixturesActiveRecord::Relation<RoomPlanFixture>

Returns:

See Also:



50
# File 'app/models/room_plan.rb', line 50

has_many :preloaded_room_plan_fixtures, -> { eager_load(:design_tool_fixture) }, class_name: 'RoomPlanFixture'

#room_configurationsActiveRecord::Relation<RoomConfiguration>

Returns:

  • (ActiveRecord::Relation<RoomConfiguration>)

See Also:



48
# File 'app/models/room_plan.rb', line 48

has_many :room_configurations, inverse_of: :room_plan, :dependent => :nullify

#room_plan_fixturesActiveRecord::Relation<RoomPlanFixture>

Returns:

See Also:



49
# File 'app/models/room_plan.rb', line 49

has_many :room_plan_fixtures, dependent: :destroy, autosave: true

#save_state(state) ⇒ Object



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'app/models/room_plan.rb', line 126

def save_state(state)

  self.step = state['step']
  self.units = state['units']
  self.points = state['points']
  self.room_area = state['roomArea']
  self.heated_area = state['heatedArea']
  self.heated_sq_ft = state['rawHeatedAreaSqft']
  self.heated_area_offset = [2, state['heatedAreaOffset'].to_f]&.max
  # TODO: Since room plan fixtures don't have an id in the json we have to always erase and rebuild
  # if the room_plan_fixture.id was passed back we could be better with this
  self.room_plan_fixtures.destroy_all
  (state['fixtures'] || []).each do |fixture_data|
    if design_tool_fixture = DesignToolFixture.find_by(class_key: fixture_data['key'])
      self.room_plan_fixtures.build(design_tool_fixture_id: design_tool_fixture.id, fixture_data: fixture_data)
    end
  end
  save
end

#selection_nameObject



68
69
70
# File 'app/models/room_plan.rb', line 68

def selection_name
  "##{id}#{' by ' + party&.to_s if party}"
end

#serialize_dataObject



100
101
102
103
104
105
106
107
108
109
110
# File 'app/models/room_plan.rb', line 100

def serialize_data
  {
    fixtures: hydrated_fixtures(include_visual: false),
    points: points.map { |p| [p['x'], p['y']] },
    units: units || 'ftin',
    roomArea: room_area&.to_f,
    heatedArea: heated_area,
    rawHeatedAreaSqft: heated_sq_ft&.to_f,
    heatedAreaOffset: heated_area_offset&.to_f || DEFAULT_HEATED_AREA_WALL_OFFSET
  }
end

#serialize_stateObject



112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'app/models/room_plan.rb', line 112

def serialize_state
  {
    step: step || 1,
    id: self.id,
    units: units || 'ftin',
    points: points || DEFAULT_POINTS,
    fixtures: fixtures,
    roomArea: room_area&.to_f,
    heatedArea: heated_area,
    rawHeatedAreaSqft: heated_sq_ft&.to_f,
    heatedAreaOffset: heated_area_offset&.to_f || DEFAULT_HEATED_AREA_WALL_OFFSET
  }
end