Install the required package:
npm i btrz-seatmaps
This will install the code required to draw a seatmap and communicate the changes on seats in real-time if required. If the site where you want to plug this is bundling or minifying script files you can simply include node_modules/btrz-seatmaps/lib/seatmap-section.js into the process and SeatmapSection class will be able to be called from there. Opening the browser console and running new SeatmapSection("seatmap", {}).draw(); where "seatmap" is the id of any div container, will help to easily check the code needed is in place and accessible.
Or it's possible to import and run it this way:
// Example
<!DOCTYPE html>
<html>
<head>
<script src="node_modules/btrz-seatmaps/lib/seatmap-section.js" type="text/javascript"></script>
<link rel="stylesheet" href="btrz-jds.css" />
</head>
<body>
<div id="seatmap" class="side-panel seatmap pointer-events-none user-select-none"></div>
<script>
new SeatmapSection("seatmap", {}).draw();
</script>
</body>
</html>
It will draw a seatmap section with a default fixed set of available seats and no facilities, into a "seatmap" div container. Next sections will show how to load it properly with real data.
Get the seatmap configuration and its capacity
Use the seatmaps availability api call to get the seatmap sections and seats information in order to draw the seatmap. This call will retrieve a list of sections per seatmap, and information about the number of seats, the way of labelling them, seat classes, seat fees, the different status of each seat, etc.
//Example (client is an axios instance)
return client({
url: "https://api.betterez.com/inventory/seatmaps/{seatmapId}/available-seats/{routeId}/{scheduleId}/{manifestDate}",
method: "get",
params: {
newdesign: true,
originId: originStationId
destinationId: destinationStationId,
legFromIndex,
legToIndex
}
headers: {
'x-api-key': "{{x-api-key-value}}",
'authorization': `Bearer ${jwtToken}`
}
});
Notice a "newdesign" query parameter is REQUIRED to call the mentioned api in order to work with the "new" design seatmaps. This is a boolean flag and if not present or false we'll keep working with "old" design seatmaps.
Replace the x-api-key-value
and the jwtToken
with proper values for your account. see here
This call will retrieve a json object similar to
// Example seatmap results:
{
"seatmap": {
"_id": "65b85d6ccdefd805046f7c08",
"name": "MB PDD MIX 49",
"capacity": 49,
"sections": [
{
"_id": "65b85a9ca1a794050af6c043",
"rowsEnumNoGaps": false,
"seatsPerRowLeft": 2,
"seatsPerRowRight": 2,
"customSeats": [
{
"row": 5,
"col": 1,
"status": "available",
"label": "12"
},
{
"row": 3,
"col": 5,
"status": "available",
"label": "7",
"seatClass": "6555a94bdef8f0051a52b37b"
},
],
"facilities": [
{
"type": "door",
"row": 4,
"col": 5,
"height": 2,
"width": 1,
"alignment": {
"key": 2,
"value": "Right"
}
},
{
"type": "stairway",
"row": 7,
"col": 4,
"height": 1,
"width": 2,
"label": "",
"orientation": {
"key": 1,
"value": "Horizontal"
}
},
{
"type": "driver",
"row": 1,
"col": 1,
"height": 1,
"width": 5,
"label": "",
"alignment": {
"key": 1,
"value": "Left"
}
},
{
"type": "gap",
"row": 3,
"col": 4,
"height": 1,
"width": 1
}
],
"availableRows": "3",
"availableCols": 5,
"seatsWithStatus": [],
"enumType": 1,
"enumDir": 1,
"startingSeatLabel": 1,
"rowLabelType": 1,
"seatLabelType": 1,
"startingRowLabel": "-1",
"showRowLabels": false,
"lastRowNoGap": false,
"name": "PISO 1",
"capacity": 7,
"rowLabelRange": ""
}
],
},
"customSeats": [
{
"row": 3,
"col": 5,
"status": "unavailable",
"female": true,
"label": "7",
"seatClass": "6555a94bdef8f0051a52b37b"
},
{
"row": 2,
"col": 5,
"suggested": true,
"label": "S",
"seatClass": "6555a94bdef8f0051a52b37b",
"sectionId": "65b85a9ca1a794050af6c043"
},
]
}
This endpoint will return information about every single seatmap section in order to draw the seats and facilities. Under customSeats array we'll find a list of booked seats (having "unavailable" status), seats marked as suggested or seats with particular gender information ("female" seats). This information has to be included into the customSeats property of the related section (notice those seats will have a sectionId property).
Create and identify the container element
Now that we have access to the seatmap code and the information to draw a section, we need to set the element where the seatmap will be rendered into
<div id="seatmap"></div>
Remember the wrapper id
since it will be our entry point to render the seatmap section as a required parameter.
Let's draw a seatmap section
To draw a seatmap section we need to create an instance of a SeatmapSection class first, providing following parameters to the constructor:
containerId
(String) The Id of the seatmap container. REQUIRED.
section
(Object) An object containing information about the seat section. REQUIRED.
settings
(Object) Additional settings for the seatmap. OPTIONAL.
// Example
const seatmap = new SeatmapSection(
containerId: "seatmap",
section,
settings: {
fees,
seatClasses,
labels,
socketEvents,
events,
}
);
seatmap.draw();
Once we have the instance created properly we can call the draw
method in order to get our section up and running.
settings.fees
The list of seat fees which are associated to the seats of this particular seatmap section we are about to load. Here we have a sample of the way of getting them:
//Example (client is an axios instance)
return client({
url: "https://api.betterez.com/inventory/seat-fees",
method: "get",
params: {
providerId: account_id,
disabled: false,
currency
}
headers: {
'x-api-key': "{{x-api-key-value}}",
'authorization': `Bearer ${jwtToken}`
}
});
We'll use the retrieved data to fill the settings.fees property with and Array similar to this one:
[
{
"_id":"6552f5365fac28050ca386ae",
"shortName":"SPL",
"type":"%",
"value":"25",
"name":"SPL",
"bgcolor":"#d4eaf0",
"valueToDisplay":"25"
},
{
"_id":"6552ec3a2e880a0512a27219",
"shortName":"VIP",
"type":"%",
"value":"20",
"name":"VIP",
"bgcolor":"#CF9D9B",
"valueToDisplay":"20"
},
{
"_id":"652825f8a6babd05144d778d",
"shortName":"bas",
"type":"$",
"value":"7.00",
"name":"bas",
"bgcolor":"#F24C40",
"valueToDisplay":"7.00"
}
]
Just _id, name and bgcolor are required.
settings.seatClasses
The list of seat classes which are associated to the seats of this particular seatmap section we are about to load. Here we have a sample of the way of getting them:
//Example (client is an axios instance)
return client({
url: "https://api.betterez.com/inventory/seat-classes",
method: "get",
params: {
providerId: account_id,
disabled: false
}
headers: {
'x-api-key': "{{x-api-key-value}}",
'authorization': `Bearer ${jwtToken}`
}
});
We'll use the retrieved data to fill the settings.seatClasses property with and Array similar to this one:
[
{
"_id":"64a524df44e060052291d4f7",
"name":"BATHROOM SEAT",
"description":"BATHROOM SEAT",
"bgcolor":"#40F28D",
"color":"#232970",
"value":"BATHROOM SEAT"
},
{
"_id":"6555a94bdef8f0051a52b37b",
"name":"VIP",
"description":"VIP",
"bgcolor":"#34def4",
"color":"#F9DEB7",
"value":"VIP"
},
{
"_id":"6555b54d9aa8b005205aeadc",
"name":"NORMAL",
"description":"NORMAL",
"bgcolor":"#40F28D",
"value":"NORMAL"
},
{
"_id":"65b889f2a1a794050af6c05f",
"name":"BATHROOM SEAT 2",
"description":"BATHROOM SEAT 2",
"bgcolor":"#1b0391",
"value":"BATHROOM SEAT 2"
}
]
Just _id, name and bgcolor are required.
settings.labels
The list of lexicon values so the seatmap can manage different languages to show titles, labels, etc. We need to provide an object with following fixed properties, setting their values in the required language:
{
"section":"Section",
"row":"Row",
"seat":"Seat",
"status":"Status",
"seatClass":"Seat class",
"fee":"Fee",
"female":"Female",
"suggested":"Suggested",
"accessible":"Accessible"
}
Live seatmaps configuration and code sample
Live seatmaps allow you to display in real time when users in other browsers select a seat in the same seatmap you are working with. To set a live seatmap we need to fill the socketEvents property of the settings parameter when calling the seatmap section constructor.
settings.socketEvents
An object that defines the socket communication settings and attaches to events for real-time updates of the seatmap.
scheduleId
(String) The schedule Id.tripId
(String) The trip Id.callbacks
(Object) Functions which will be triggered for various seat-related or broadcast events.
socketUrl
(String) The socket URL.ttlSec
(Number) Seat life time in seconds.idForLiveSeatmap
(String) The Id for live seatmap: ${ROUTE_ID}${MANIFEST_ID}${MANIFEST_DATE_YYY-MM-DD}
.legFrom
(Number)legTo
(Number)accessTicket
(String) Access token for the socket.
settings.socketEvents.callbacks
Here is the list of the events which can be attached
settings.socketEvents.callbacks.seatClicked
it will be called when a user in this particular seatmap clicks or hits Enter over an available seat or over an already selected one. This callback should be used to select and/or unselect seats.
settings.socketEvents.callbacks.seatClicked = (seat) => {
const seatId = `section-${seat.sectionId}-row-${seat.rowLabel}-seat-${seat.label}`;
if (seatWasAlreadySelected(seat)) {
//Relase the seat
SeatmapSection.changeSeatDataProp({col: seat.col, row: seat.row, sectionId: seat.sectionId}, "selected", "false");
//Broadcast the event
SeatmapSocket.pushEvent("seat:unselected", {col: seat.col, row: seat.row, sectionId: seat.sectionId}, seatId);
} else {
//Mark the seat as selected
SeatmapSection.changeSeatDataProp({col: seat.col, row: seat.row, sectionId: seat.sectionId}, "selected", "true");
//Broadcast the event
SeatmapSocket.pushEvent("seat:selected", {col: seat.col, row: seat.row, sectionId: seat.sectionId}, seatId);
}
};
settings.socketEvents.callbacks.seatOver
it will be called after the mouseover event over the seat in this particular seatmap.
settings.socketEvents.callbacks.seatOut
it will be called after the mouseover event out the seat in this particular seatmap.
settings.socketEvents.callbacks.seatExpired
this one will be called when a "sync:seats" event arrives from broadcasting to the live seatmap, announcing a seat concludes its time to live
settings.socketEvents.callbacks.seatExpired = (seatExpired) => {
if (ifExpiredSeatWasSelectedInThisSeatmap(seatExpired)) {
//Release the seat
SeatmapSection.changeSeatDataProp({col: seatExpired.col, row: seatExpired.row, sectionId: seatExpired.sectionId}, "selected", "false");
}
//Mark the expired seat as available and ready to navigate through keyboard
SeatmapSection.changeSeatStatus({col: seatExpired.col, row: seatExpired.row, sectionId: seatExpired.sectionId}, "available");
SeatmapSection.changeSeatDataProp({col: seatExpired.col, row: seatExpired.row, sectionId: seatExpired.sectionId}, "keynav", "true");
};
settings.socketEvents.callbacks.seatmapJoin
it will be called after a "sync:join" event arrives when the live seatmap successfully plugs into the broadcast socket telling which seats were already blocked in a per section basis
settings.socketEvents.callbacks.seatmapJoin = (seats, sectionId, customSeats) => {
seats.forEach((s) => {
if (ifSeatBelongsToThisSeatmapSection(s)) {
SeatmapSection.changeSeatStatus({col: s.seat.col, row: s.seat.row, sectionId: s.seat.sectionId}, "blocked");
}
}
});
};
settings.socketEvents.callbacks.seatmapSeatSelected
this one will be called when a "seat:selected" event arrives from broadcasting when a user in another browser selects a seat
settings.socketEvents.callbacks.seatmapSeatSelected = (seat) => {
if (ifSeatBelongsToThisSeatmapSection(s)) {
SeatmapSection.changeSeatStatus({col: seat.col, row: seat.row, sectionId: seat.sectionId}, "blocked");
}
};
settings.socketEvents.callbacks.seatmapSeatUnSelected
this one will be called when a "seat:unselected" event arrives from broadcasting when a user in another browser unselects a seat
settings.socketEvents.callbacks.seatmapSeatUnSelected = (seat) => {
if (ifSeatBelongsToThisSeatmapSection(s)) {
SeatmapSection.changeSeatStatus({col: seat.col, row: seat.row, sectionId: seat.sectionId}, "available");
SeatmapSection.changeSeatDataProp({col: seat.col, row: seat.row, sectionId: seat.sectionId}, "keynav", "true");
}
};
settings.socketEvents.accessTicket
In order to get the access ticket do this on the server side so not to expose your jwtToken. Replace the x-api-key-value
and the jwtToken
with proper values for your account. see here
//client is an axios instance
return client({
url: "https://api.betterez.com/seatmaps/access-ticket",
method: "post",
headers: {
'x-api-key': "{{x-api-key-value}}",
'authorization': `Bearer ${jwtToken}`
}
});
Here we have a sample of the way of building the socketEvents object when we want to get the live seatmap up and running:
// Example
const socketEvents = {
scheduleId: "a-schedule-id-123",
tripId: "a-trip-id-123",
callbacks: {
seatClicked: window.onSeatClicked,
seatExpired: window.onSeatExpired,
seatmapJoin: window.onSeatmapJoin,
seatmapSeatSelected: window.onSeatmapSeatSelected,
seatmapSeatUnSelected: window.onSeatmapSeatUnSelected
},
socketUrl: "ws://{{domain}}/seatmaps/socket",
ttlSec: 180,
accessTicket: "a-access-ticket-123",
idForLiveSeatmap: `routeId_scheduleId_manifestDate`,
legFrom: 6,
legTo: 5
}
Broadcast events
We can call a static method to broadcast expected events to tell everyone listening that a seat was selected or unselected:
// Example
SeatmapSocket.pushEvent("seat:unselected", seat, seatId);
SeatmapSocket.pushEvent("seat:selected", seat, seatId);
seat
(Object) Having col, row and sectionId as REQUIRED properties
seatId
(String) The unique seat identifier: section-${seat.sectionId}-row-${seat.rowLabel}-seat-${seat.label}
Events:
In case you don't need to work with a live seatmap you can still define a different set of events which will be called by the seatmap on a per seat basis.
settings.events:
These array of events can be defined using the following properties on each of them:elementType
(String) type of element to which the event applies to, meaning seats or a specific facility (gap, door, table, etc). If this one is not present an elementStatus is REQUIRED
elementStatus
(Array) Status of the seat element to which the event will be applied to (available, unavailable, blocked, reserved). If this one is not present an elementType is REQUIREDtype
(String) REQUIRED Type of event (click, blur, focus, mouseover, mouseout etc).
cb
(Function) REQUIRED Callback function to be triggered when the event occurs.
// Example
settings.events = [
{
elementType: "seat",
elementStatus: ["available"],
type: "click",
cb: function (evt, e, elem) {
window.onSeatClicked(elem);
}
}
];
window.onSeatClicked = (elem) => {
const seatId = `section-${seat.sectionId}-row-${seat.rowLabel}-seat-${seat.label}`;
if (seatWasAlreadySelected(seat)) {
//Mark the seat as selected
SeatmapSection.changeSeatDataProp(seat, "selected", "false");
//Broadcast the event
SeatmapSocket.pushEvent("seat:unselected", seat, seatId);
} else {
//Mark the seat as selected
SeatmapSection.changeSeatDataProp(seat, "selected", "true");
//Broadcast the event
SeatmapSocket.pushEvent("seat:selected", seat, seatId);
}
};
SeatmapSection useful methods
Here you have an extra set of exposed methods and can be useful to work with the seatmap sections:
clearFocus
Remove focus of an element within the seatmap container.
// Example
seatmap.clearFocus();
.
clearSelection
Remove selection of elements inside the container of the seatmap.
// Example
seatmap.clearSelection();
.
changeSeatDataProp
Changes a property of an element within the seatmap.
// Example
seatmap.changeSeatDataProp(seat, prop, value );
changeSeatStatus
Changes the status of an element within the seatmap.
// Example
seatmap.changeSeatStatus(seat, status);
.
focus
Focuses on the seatmap container and the first available seat within it.
// Example
seatmap.focus();
.
focusElement
Focuses on a specific element within the seatmap.
// Example
seatmap.focusElement(elem);
.
focusOnNextSelected
Focuses on the next selected seat in the seat map. Requires the previous seat selected as a parameter.
// Example
seatmap.focusOnNextSelected(previousSeatSelected);
.
getCapacity
Calculates the current capacity of the seatmap.
// Example
seatmap.getCapacity();
.
onSeatClicked
Activated when a seat on the seat map is clicked.
Checks if the seat has been marked as 'blocked' and sends its data via socket events.
// Example
seatmap.onSeatClicked(elem);
.
selectElement
Selects an element within the seatmap.
// Example
seatmap.selectElement(elem);