Class: SpreeCmCommissioner::Integrations::BookMeBusV1::Resources::SeatLayout

Inherits:
Object
  • Object
show all
Defined in:
app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/seat_layout.rb

Overview

SeatLayout resource wraps BookMeBus seat layout API response

This class provides methods to extract and transform seat layout data from the BookMeBus API into a format suitable for BookMe+ internal models.

BookMeBus API Structure:

layouts: Hash keyed by stop_id (e.g., "233")
  Each stop contains layers (e.g., "BUS", "UPPER_DECK")
    Each layer is a 2D array [rows][columns]
      Each cell is either a seat object or null (aisle)

Example API Response:

{
  "id" => "2adea8ac4cf6a75db037f698a9126914",
  "attributes" => {
    "from_stop_id" => 233,
    "to_stop_id" => 237,
    "layouts" => {
      "233" => {
        "BUS" => [
          [{"label" => "1", "seat_type" => 0, "status" => 0}, {"label" => "2"}, nil, {"label" => "3"}],
          [{"label" => "5"}, {"label" => "6"}, nil, {"label" => "7"}]
        ]
      }
    }
  }
}

Visual Layout Example (2 rows × 4 columns with aisle):

Row 0: [1] [2] [aisle] [3] [4]
Row 1: [5] [6] [aisle] [7] [8]

Constant Summary collapse

BLOCK_WIDTH =

Layout rendering constants

50
BLOCK_HEIGHT =
50
BLOCK_GAP =
10
LAYER_Y_OFFSET =
1000
DEFAULT_CANVAS_WIDTH =
400
DEFAULT_CANVAS_HEIGHT =
400

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(data) ⇒ SeatLayout

Initialize seat layout from BookMeBus API response

Example:

data = {
  'id' => '2adea8ac4cf6a75db037f698a9126914',
  'attributes' => {
    'from_stop_id' => 233,
    'layouts' => {"233" => {"BUS" => [[...]]}}
  }
}
seat_layout = SeatLayout.new(data)

Parameters:

  • data (Hash)

    API response data with ‘id’ and ‘attributes’ keys



65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/seat_layout.rb', line 65

def initialize(data)
  attributes = data['attributes'] || data

  @id = data['id']
  @from_stop_id = attributes['from_stop_id']
  @to_stop_id = attributes['to_stop_id']
  @on_date = attributes['on_date']
  @duration = attributes['duration']
  @min_number_of_seats_remaining = attributes['min_number_of_seats_remaining']
  @number_of_seats_remaining = attributes['number_of_seats_remaining'] || {}
  @layouts = attributes['layouts'] || {}
  @stop_times = attributes['stop_times'] || []
  @trip = attributes['trip'] || {}
end

Instance Attribute Details

#durationObject (readonly)

Returns the value of attribute duration.



47
48
49
# File 'app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/seat_layout.rb', line 47

def duration
  @duration
end

#from_stop_idObject (readonly)

Returns the value of attribute from_stop_id.



47
48
49
# File 'app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/seat_layout.rb', line 47

def from_stop_id
  @from_stop_id
end

#idObject (readonly)

Returns the value of attribute id.



47
48
49
# File 'app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/seat_layout.rb', line 47

def id
  @id
end

#layoutsObject (readonly)

Returns the value of attribute layouts.



47
48
49
# File 'app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/seat_layout.rb', line 47

def layouts
  @layouts
end

#min_number_of_seats_remainingObject (readonly)

Returns the value of attribute min_number_of_seats_remaining.



47
48
49
# File 'app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/seat_layout.rb', line 47

def min_number_of_seats_remaining
  @min_number_of_seats_remaining
end

#number_of_seats_remainingObject (readonly)

Returns the value of attribute number_of_seats_remaining.



47
48
49
# File 'app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/seat_layout.rb', line 47

def number_of_seats_remaining
  @number_of_seats_remaining
end

#on_dateObject (readonly)

Returns the value of attribute on_date.



47
48
49
# File 'app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/seat_layout.rb', line 47

def on_date
  @on_date
end

#stop_timesObject (readonly)

Returns the value of attribute stop_times.



47
48
49
# File 'app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/seat_layout.rb', line 47

def stop_times
  @stop_times
end

#to_stop_idObject (readonly)

Returns the value of attribute to_stop_id.



47
48
49
# File 'app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/seat_layout.rb', line 47

def to_stop_id
  @to_stop_id
end

#tripObject (readonly)

Returns the value of attribute trip.



47
48
49
# File 'app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/seat_layout.rb', line 47

def trip
  @trip
end

Class Method Details

.from_json_api_single(data) ⇒ Object



80
81
82
83
84
# File 'app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/seat_layout.rb', line 80

def self.from_json_api_single(data)
  return nil unless data.is_a?(Hash) && data['data'].is_a?(Hash)

  new(data['data'])
end

Instance Method Details

#available_seats_countInteger

Get count of available seats (status = 0)

Example:

# 45 total seats, 5 booked (status=1)
seat_layout.available_seats_count  # => 40

# All seats available
seat_layout.available_seats_count  # => 45

Returns:

  • (Integer)

    Number of seats with status 0 (available)



216
217
218
# File 'app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/seat_layout.rb', line 216

def available_seats_count
  seats.count(&:available?)
end

#column_countInteger

Get the number of columns in the layout (including aisles)

Example:

# Layout: [seat, seat, aisle, seat, seat]
seat_layout.column_count  # => 5

# Layout: [seat, seat, aisle, seat]
seat_layout.column_count  # => 4

Returns:

  • (Integer)

    Number of columns in the first row



200
201
202
203
# File 'app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/seat_layout.rb', line 200

def column_count
  first_row = primary_layout.values.first&.first
  first_row&.size || 0
end

#primary_layoutHash

Get the primary layout (from first stop)

Example:

seat_layout.layouts
# => {"233" => {"BUS" => [[seat1, seat2, nil, seat3], [seat5, seat6, nil, seat7]]}}

seat_layout.primary_layout
# => {"BUS" => [[seat1, seat2, nil, seat3], [seat5, seat6, nil, seat7]]}

Visual:

BUS Layer:
Row 0: [1] [2] [aisle] [3] [4]
Row 1: [5] [6] [aisle] [7] [8]

Returns:

  • (Hash)

    Layout keyed by layer name (e.g., “BUS”, “UPPER_DECK”)



102
103
104
105
106
# File 'app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/seat_layout.rb', line 102

def primary_layout
  return {} if layouts.empty?

  layouts[layouts.keys.first] || {}
end

#row_countInteger

Get the number of rows in the layout

Example:

# Layout with 12 rows:
# Row 0:  [1] [2] [aisle] [3] [4]
# Row 1:  [5] [6] [aisle] [7] [8]
# ...
# Row 11: [45] [46] [aisle] [47] [48]

seat_layout.row_count  # => 12

Returns:

  • (Integer)

    Number of rows in the primary layout



185
186
187
# File 'app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/seat_layout.rb', line 185

def row_count
  primary_layout.values.first&.size || 0
end

#seat_typesArray<Integer>

Get unique seat types present in the layout

Seat Types:

0 = Regular seat
1 = VIP/Sleeper seat
2 = Premium seat

Example:

# Layout with mixed seat types
seat_layout.seat_types  # => [0, 1, 2]

# Layout with only regular seats
seat_layout.seat_types  # => [0]

Returns:

  • (Array<Integer>)

    Sorted array of unique seat type IDs



147
148
149
# File 'app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/seat_layout.rb', line 147

def seat_types
  seats.map(&:seat_type).uniq.sort
end

#seatsArray<Seat>

Get all seats from the primary layout as a flat array

Extracts all seat objects from the 2D layout, skipping aisles (nil values). Each seat is enriched with row and column position.

Example:

seat_layout.seats
# => [
#   #<Seat label="1", row=0, column=0, seat_type=0, status=0>,
#   #<Seat label="2", row=0, column=1, seat_type=0, status=0>,
#   #<Seat label="3", row=0, column=3, seat_type=0, status=0>,  # column 2 was aisle
#   #<Seat label="5", row=1, column=0, seat_type=0, status=0>,
#   ...
# ]

seat_layout.seats.count  # => 45 (total seats, excluding aisles)

Returns:

  • (Array<Seat>)

    Array of seat objects with position data



127
128
129
# File 'app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/seat_layout.rb', line 127

def seats
  @seats ||= extract_seats_from_layout(primary_layout)
end

#seats_by_typeHash<Integer, Array<Seat>>

Get seats grouped by their seat_type

Useful for processing different seat types separately or counting seats per type.

Example:

seat_layout.seats_by_type
# => {
#   0 => [seat1, seat2, seat3, ...],  # 40 regular seats
#   1 => [seat41, seat42, ...]         # 5 VIP seats
# }

# Count seats per type
seat_layout.seats_by_type.transform_values(&:count)
# => {0 => 40, 1 => 5}

Returns:

  • (Hash<Integer, Array<Seat>>)

    Hash with seat_type as key, array of seats as value



168
169
170
# File 'app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/seat_layout.rb', line 168

def seats_by_type
  seats.group_by(&:seat_type)
end

#to_internal_blocks_attributes(seat_layout) ⇒ Array<Hash>

Convert BookMeBus seat layout to BookMe+ Block attributes

Transforms the 2D seat layout into an array of block hashes ready for creating SpreeCmCommissioner::Block records. Each seat becomes a block with calculated x,y coordinates based on its row/column position.

Coordinate Calculation:

x = column_index * (block_width + gap)
y = row_index * (block_height + gap)

Example:

internal_layout = SpreeCmCommissioner::SeatLayout.create!(
  name: "VIP Bus Layout",
  layoutable: vehicle_type
)

blocks_attrs = external_seat_layout.to_internal_blocks_attributes(internal_layout)
# => [
#   {
#     label: "1",
#     block_type: :seat,
#     width: 50, height: 50,
#     x: 0, y: 0,  # Row 0, Column 0
#     rotation: 0,
#     seat_layout: internal_layout
#   },
#   {
#     label: "2",
#     block_type: :seat,
#     width: 50, height: 50,
#     x: 60, y: 0,  # Row 0, Column 1 (50 + 10 gap)
#     rotation: 0,
#     seat_layout: internal_layout
#   },
#   # Column 2 (aisle) is skipped
#   {
#     label: "3",
#     block_type: :seat,
#     width: 50, height: 50,
#     x: 180, y: 0,  # Row 0, Column 3
#     rotation: 0,
#     seat_layout: internal_layout
#   }
# ]

# Create blocks
blocks_attrs.each { |attrs| SpreeCmCommissioner::Block.create!(attrs) }

Visual Layout:

(0,0)     (60,0)    (180,0)   (240,0)
  [1]       [2]    [aisle]     [3]       [4]

(0,60)    (60,60)  (180,60)  (240,60)
  [5]       [6]    [aisle]     [7]       [8]

Parameters:

Returns:

  • (Array<Hash>)

    Array of block attribute hashes



278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
# File 'app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/seat_layout.rb', line 278

def to_internal_blocks_attributes(seat_layout)
  blocks = []
  offsets = calculate_centering_offsets(seat_layout)
  current_layer_offset = 0

  primary_layout.each do |layer_name, rows|
    process_layer_rows(rows, layer_name, blocks, offsets, current_layer_offset, seat_layout)

    # Offset next layer by 1000px to prevent overlap
    # This allows double-decker buses to have separate visual layers
    current_layer_offset += LAYER_Y_OFFSET
  end

  blocks
end