Otapp Bus Seat Map
A flexible bus seat map widget for Flutter. Designed for bus booking apps using Otapp Services API or manual configuration.
Features
- Works directly with Otapp Services API response format
- Manual setup option for custom implementations
- 100% customizable widgets - bring your own seat, aisle, and special element designs
- Automatic aisle detection
- Support for special elements (doors, toilets, stairs)
- VIP/category support
- Seat status management (available, booked, selected, processing)
- InteractiveViewer support for zoom/pan
Installation
dependencies:
otapp_bus_seat_map: ^0.1.2
Or run:
flutter pub add otapp_bus_seat_map
Option 1: Using Otapp Services API
If you're using the Otapp Services API, the seat map response works directly with this package.
API Response Format
The Otapp API returns seat data in this format:
{
"lower_seat_map": [
{"seat_row1": "L-1-1-1,L-1-1-2,0,L-1-1-3,L-1-1-4"},
{"seat_row2": "L-1-2-5,L-1-2-6,0,L-1-2-7,L-1-2-8"},
{"seat_row3": "@,0,0,L-1-3-9,L-1-3-10"},
{"seat_row4": "*,0,0,L-1-4-11,L-1-4-12"}
],
"available_seats": "L-1-1-1,L-1-1-2,L-1-1-3,L-1-2-5,L-1-2-7",
"process_seats": "L-1-2-8",
"is_right_hand_drive": 1,
"seat_types": [
{"seat_type_name": "VIP", "seats": "L-1-1-1,L-1-1-2", "fare": [{"fare": "35000"}]},
{"seat_type_name": "Standard", "seats": "L-1-1-3,L-1-1-4", "fare": [{"fare": "25000"}]}
]
}
Using with Otapp API
import 'package:otapp_bus_seat_map/otapp_bus_seat_map.dart';
// Parse directly from API response
final layout = SeatLayout.fromJson(
apiResponse['lower_seat_map'],
config: SeatLayoutConfig.bus(),
);
// Apply seat statuses from API
final layoutWithStatus = SeatLayout.fromCsvRowsWithStatus(
layout.rawRows,
config: SeatLayoutConfig.bus(),
availableSeats: apiResponse['available_seats'],
bookedSeats: apiResponse['booked_seats'] ?? '',
processingSeats: apiResponse['process_seats'] ?? '',
);
// Use the widget
SeatMapWidget(
layout: layoutWithStatus,
selectedSeats: selectedSeats,
onSeatTap: (seat) {
// Handle seat selection
},
)
Option 2: Manual Setup
You can also define seat layouts manually without an API.
CSV Format
Each row is a comma-separated string. Use markers for special elements:
| Code | Element | Description |
|---|---|---|
0 |
Empty/Aisle | Empty space or walkway |
@ |
Door | Entry/exit door |
* |
Toilet | WC/restroom |
# |
Stairs | For double-decker buses |
| Any other | Seat | Regular bookable seat |
Manual Layout Example
import 'package:otapp_bus_seat_map/otapp_bus_seat_map.dart';
// Define your seat layout manually
final rows = [
'1A,1B,0,1C,1D', // Row 1: 2 seats, aisle, 2 seats
'2A,2B,0,2C,2D', // Row 2: same pattern
'@,0,0,3C,3D', // Row 3: door on left, 2 seats on right
'4A,4B,0,4C,4D', // Row 4: normal row
'*,0,0,5C,5D', // Row 5: toilet on left, 2 seats on right
'6A,6B,6C,6D,6E', // Back row: 5 seats, no aisle
];
// Parse the layout
final layout = SeatLayout.fromCsvRows(
rows,
config: SeatLayoutConfig.bus(defaultPrice: 25000),
);
// Use the widget
SeatMapWidget(
layout: layout,
selectedSeats: selectedSeats,
onSeatTap: (seat) => handleSelection(seat),
)
With Custom Pricing & Categories
final vipSeats = {'1A', '1B', '1C', '1D'};
final layout = SeatLayout.fromCsvRows(
rows,
config: SeatLayoutConfig.bus(
defaultPrice: 25000,
categoryResolver: (code, metadata) {
if (vipSeats.contains(code)) return 'VIP';
return 'Standard';
},
priceResolver: (code, category, metadata) {
if (category == 'VIP') return 35000;
return 25000;
},
),
);
Full Widget Customization
Every widget is customizable. Use the default widgets or bring your own designs.
Available Builders
| Builder | Purpose | Receives |
|---|---|---|
seatBuilder |
Custom seat design | context, seat, isSelected |
aisleBuilder |
Custom aisle/empty space | context, element |
specialBuilder |
Custom door/toilet/stairs | context, element |
rowLabelBuilder |
Custom row labels | context, rowIndex, row |
Example: Fully Custom Seat Map
SeatMapWidget(
layout: layout,
selectedSeats: selectedSeats,
onSeatTap: (seat) => handleSelection(seat),
// Your custom seat widget
seatBuilder: (context, seat, isSelected) {
return Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: _getSeatColor(seat, isSelected),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade300),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (seat.category == 'VIP') Icon(Icons.star, size: 12),
Text(seat.label ?? '', style: TextStyle(fontWeight: FontWeight.bold)),
if (seat.price != null) Text('${seat.price}', style: TextStyle(fontSize: 8)),
],
),
);
},
// Your custom aisle widget
aisleBuilder: (context, element) {
return Container(
width: 50,
height: 50,
child: Center(
child: RotatedBox(
quarterTurns: 1,
child: Text('AISLE', style: TextStyle(color: Colors.grey, fontSize: 8)),
),
),
);
},
// Your custom special elements
specialBuilder: (context, element) {
switch (element.type) {
case SeatElementType.door:
return MyCustomDoorWidget();
case SeatElementType.toilet:
return MyCustomToiletWidget();
case SeatElementType.stairs:
return MyCustomStairsWidget();
default:
return DefaultSpecialWidget(element: element);
}
},
// Your custom row labels
rowLabelBuilder: (context, rowIndex, row) {
return Container(
width: 30,
child: Center(child: Text('R${rowIndex + 1}')),
);
},
)
Using Default Widgets (Optional)
If you don't provide custom builders, the package uses sensible defaults:
// Minimal setup - uses all default widgets
SeatMapWidget(
layout: layout,
selectedSeats: selectedSeats,
onSeatTap: (seat) => handleSelection(seat),
)
With Selection Controller
SeatMapController(
layout: layout,
maxSelection: 4,
onSelectionChanged: (seats) {
print('Selected: ${seats.map((s) => s.label).join(", ")}');
print('Total: ${seats.fold(0.0, (sum, s) => sum + s.price)}');
},
builder: (context, layout, selectedSeats, onSeatTap) {
return SeatMapWidget(
layout: layout,
selectedSeats: selectedSeats,
onSeatTap: onSeatTap,
// Add your custom builders here too
);
},
)
Configuration Options
SeatLayoutConfig
| Property | Default | Description |
|---|---|---|
emptyMarkers |
{'0', ''} |
Codes that represent empty spaces |
doorMarker |
'@' |
Code for door elements |
toiletMarker |
'*' |
Code for toilet elements |
stairsMarker |
'#' |
Code for stairs |
autoDetectAisle |
true |
Auto-detect aisle columns |
delimiter |
',' |
Separator for row strings |
SeatMapWidget
| Property | Default | Description |
|---|---|---|
seatSize |
50 |
Width/height of each seat |
seatSpacing |
4 |
Horizontal spacing between seats |
rowSpacing |
4 |
Vertical spacing between rows |
showRowLabels |
false |
Show row numbers on left |
enableZoom |
false |
Enable InteractiveViewer |
minScale |
0.5 |
Minimum zoom scale |
maxScale |
3.0 |
Maximum zoom scale |
About Otapp Services
This package is designed to work seamlessly with the Otapp Services bus booking API. For API access and documentation, contact Otapp Services.
License
MIT License
Libraries
- otapp_bus_seat_map
- A flexible, customizable seat map widget for Flutter.