static.app.js Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of process-optaplanner-springboot
Show all versions of process-optaplanner-springboot
Flight assignment process and optimization
/**
* Renders the display of a flight
* @param {object} flight - The flight to display
*
* @returns jQuery
*/
function renderFlight(flight, tasks) {
let header;
const finalizePassengerListTasks = findTasks("finalizePassengerList", tasks);
if (finalizePassengerListTasks.length === 1) {
const generatePassengerElement = (flight.flight.passengerList.length === 0)? [
element("button", { class: "btn btn-secondary", style: "margin: 5px;", onClick: () => generatePassengersForFlight(flight) }, "Generate passenger list")
]: []
header = element("div", {},
element("h5", {}, getFlightName(flight)),
element("button", {
class: "btn btn-secondary",
role: "button",
style: "margin: 5px;",
"data-flight": flight.id,
"data-action": "add",
"data-toggle": "modal",
"data-target": "#add-passenger-modal",
},
"Add passenger"
),
...generatePassengerElement,
element("button", {
class: "btn btn-primary",
role: "button",
style: "margin: 5px;",
onClick: () => {
$.post(`/rest/flights/${flight.id}/finalizePassengerList/${finalizePassengerListTasks[0].id}`, JSON.stringify({}), () => {
const refreshFlight = () => {
$.getJSON(`/rest/flights/${flight.id}`, flight => {
$.getJSON(`/rest/flights/${flight.id}/tasks`, tasks => {
$(`#${flight.id} > *`).replaceWith(renderFlight(flight, tasks));
}).then(() => {
if (flight.isSolving) {
setTimeout(refreshFlight, 500);
}
});
});
};
refreshFlight();
});
}
}, "Finalize passenger list")
);
}
else if (flight.isSolving) {
header = element("div", {},
element("h5", {}, getFlightName(flight)), element("h6", {}, "Solving..."), element("span", {}, `Score: ${getFlightScore(flight.flight)}`));
}
else {
const finalizeSeatAssignmentTasks = findTasks("finalizeSeatAssignment", tasks);
const finalizeSeatAssignmentButton = (finalizeSeatAssignmentTasks.length === 1)? [element("button", {
class: "btn btn-primary",
role: "button",
onClick: () => {
$.post(`/rest/flights/${flight.id}/finalizeSeatAssignment/${finalizeSeatAssignmentTasks[0].id}`, JSON.stringify({}), () => {
refresh();
});
}
}, "Finalize seat assignments")] : [];
header = element("div", {},
element("h5", {}, getFlightName(flight)),
element("div", {}, `Score: ${getFlightScore(flight.flight)}`),
...finalizeSeatAssignmentButton
);
}
const getSeatName = (seat) => {
const seatRegex = /(\d+);(\d+)/;
const matches = seat.match(seatRegex);
const row = parseInt(matches[1]);
const column = parseInt(matches[2]);
return flight.flight.seatList.find(s => s.row === row && s.column === column).name;
};
const passengerRequestString = passenger => `${passenger.name}, Is Paying for Seat? ${passenger.paidForSeat? "Yes, Seat " + getSeatName(passenger.seat) : "No"}`;
return element("div", {},
element("div", {},
header,
element("div", { style: "display: grid; grid-template-columns: 1fr 1fr;"},
element("div", { style: "grid-column: 1;" },
// Passenger Request List
suspense(element("div", {}, "is loading"),
element("div", { class: "list-group" }, ...findTasks("approveDenyPassenger", tasks).map(futureTask => future(futureTask, { isLoading: true },
resolve => $.getJSON(`/rest/flights/${flight.id}/approveDenyPassenger/${futureTask.id}`, resolve),
task => element(
"div", { class: "list-group-item list-group-item-action flex-column align-items-start d-flex w-100 justify-content-between" },
// Note: we take advantage that undefined is falsey here (task doesn't have property "isLoading")
element("span", {},
element("i", { class: "fas fa-user", style: "margin-right: 10px;" }, ""),
element("span", {}, task.isLoading? "" : passengerRequestString(task.passenger)),
),
element("span", {},
element("button", task.isLoading? { class: "btn btn-primary" } : {
class: "btn btn-primary",
onClick: () => {
$.post(`/rest/flights/${flight.id}/approveDenyPassenger/${task.id}`, JSON.stringify({
isPassengerApproved: true
}), () => {
refresh();
});
}
}, "Approve"),
element("button", task.isLoading? { class: "btn btn-danger" } : {
class: "btn btn-danger",
onClick: () => {
$.post(`/rest/flights/${flight.id}/approveDenyPassenger/${task.id}`, JSON.stringify({
isPassengerApproved: false
}), () => {
refresh();
});
}
}, "Deny")
)
)))))),
element("div", {},
flightSeats(flight, ({ passenger }) => (passenger)? element("div", {}, passenger.name) : element("div", {}, "")),
// Approved Passenger List
element("div", { class: "card" },
element("h5", {}, `Passenger list (${flight.flight.passengerList.length} passenger${(flight.flight.passengerList.length == 1)? "" : "s"})`),
element("button", {
class: "collapsed",
"data-toggle": "collapse",
"data-target": `#${flight.id}-passenger-list`,
"aria-expanded": "false",
"aria-controls": `#${flight.id}-passenger-list`,
onClick: () => $(`#${flight.id}-passenger-list`).collapse("toggle")
},
"Show/Hide"
),
element("div", { id: `${flight.id}-passenger-list`, class: "collapse list-group" }, ...flight.flight.passengerList.map(passenger => element(
"div", { class: "list-group-item list-group-item-action flex-column align-items-start d-flex w-100 justify-content-between" },
element("span", {},
element("i", { class: "fas fa-user", style: "margin-right: 10px;" }, ""),
element("span", {}, passenger.name),
),
element("span", {},
`Seat: ${passenger.seat? passenger.seat.name : "Unassigned"}`
)
))
)
)
)
),
));
}
function flightSeats(flight, flightSeatToElementMap) {
const rowToGridRow = row => 2*row + 1;
const columnToGridColumn = col => col + Math.round(col / (flight.flight.seatColumnSize)) + 1;
const sortedSeatList = [...flight.flight.seatList].sort((a,b) => (a.row - b.row === 0)? a.column - b.column : a.row - b.row);
const seatToIndictment = sortedSeatList.map(seat => getSeatIndictment(seat, flight.flight.passengerList));
const seatColor = (index) => (seatToIndictment[index].score.hard < 0)? 'var(--global-color-unavailable)' :
(seatToIndictment[index].score.soft < 0)? 'var(--global-color-undesired)' :
(seatToIndictment[index].matchPreference)? 'var(--global-color-desired)' :
'var(--global-color-normal)';
const seatTooltip = (seat, index) => (seatToIndictment[index].indictments.length === 0)? `The seat ${seat.name} does not violate any constraints.` :
seatToIndictment[index].indictments.map(indictment => `${indictment.name}: ${indictment.description} (${indictment.score.hard} Hard/${indictment.score.soft} Soft)`)
.reduce((prev, curr) => `${prev};\n${curr}`);
const emergencyExitRow = rowToGridRow(sortedSeatList.find(seat => seat.emergencyExitRow).row);
return element("div", { style: `display: grid;
grid-column: 2;
grid-auto-flow: dense;
grid-template-rows: repeat(${2*flight.flight.seatRowSize}, minmax(50px, 1fr));
grid-template-columns: repeat(${flight.flight.seatColumnSize + 1}, minmax(100px, 1fr));
column-gap: 15px;
padding-left: 15px;
padding-right: 15px;
width: min-content;
border: 1px solid;
` }, ...sortedSeatList.map((seat, index) => element(
"span", {
style: `grid-row: ${rowToGridRow(seat.row)};
grid-column: ${columnToGridColumn(seat.column)};
margin-top: 15px;
background-color: ${seatColor(index)};
text-align: center;
`,
title: seatTooltip(seat, index)
},
element("span", {
class: "fas fa-couch",
style: "margin-right: 5px;"
}, ""),
element("span", {}, seat.name)
)),
...sortedSeatList.map((seat, index) => flightSeatToElementMap({flight: flight.flight, seat: seat, passenger: flight.flight.passengerList.find(passenger => passenger.seat !== null &&
passenger.seat.row === seat.row && passenger.seat.column === seat.column) } ).css({
"grid-row": String(rowToGridRow(seat.row) + 1),
"grid-column": String(columnToGridColumn(seat.column)),
"background-color": seatColor(index),
"text-align": "center",
"margin-bottom": "15px"
}).attr("title", seatTooltip(seat, index))),
element("div", {
style: `grid-column: 1 / -1;
grid-row: ${emergencyExitRow} / span 2;
margin-top: 15px;
margin-bottom: 15px;
margin-left: -15px;
margin-right: -15px;
border-top: 2px dashed;
border-bottom: 2px dashed;
pointer-events: none;
`
}, "")
);
}
function getFlightScore(flight) {
return flight.score? `${flight.score.hardScore}hard/${flight.score.softScore}soft` : null;
}
function getSeatIndictment(seat, passengerList) {
const out = {
score: {
hard: 0,
soft: 0
},
indictments: []
};
const passengersInSeat = passengerList.filter(passenger => passenger.seat !== null && passenger.seat.name === seat.name);
if (passengersInSeat.length > 1) {
out.score.hard -= 1;
out.indictments.push({
name: "Seat conflict",
description: `The seat ${seat.name} has multiple passengers: ${passengersInSeat.map(passenger => passenger.name)}.`,
score: { hard: -1, soft: 0 }
});
}
passengersInSeat.forEach(passenger => {
if (seat.emergencyExitRow && !passenger.emergencyExitRowCapable) {
out.score.hard -= 1;
out.indictments.push({
name: "Emergency exit row has incapable passenger",
description: `The seat ${seat.name} is in the emergency exit row but the passenger ${passenger.name} cannot assist in an emergency.`,
score: { hard: -1, soft: 0 }
});
}
if (passenger.seatTypePreference !== null && passenger.seatTypePreference !== seat.seatType) {
out.score.soft -= 1;
out.indictments.push({
name: "Seat type preference",
description: `The passenger ${passenger.name} prefers ${passenger.seatTypePreference} seats but got a ${seat.seatType} seat.`,
score: { hard: 0, soft: -1 }
});
}
else if (passenger.seatTypePreference !== null && passenger.seatTypePreference === seat.seatType) {
out.matchPreference = true;
out.indictments.push({
name: "Seat type preference",
description: `The passenger ${passenger.name} prefers ${passenger.seatTypePreference} seats and got a ${seat.seatType} seat.`,
score: { hard: 0, soft: 0 }
});
}
});
return out;
}
let myFlights = [];
let flightOrder = [];
function refresh() {
$.getJSON("/rest/flights", flights => {
const toRemove = [...flightOrder];
const toAdd = [];
flights.forEach(flight => {
if (!flightOrder.includes(flight.id)) {
flightOrder.push(flight.id);
toAdd.push(flight.id);
}
else {
toRemove.splice(toRemove.indexOf(flight.id), 1);
}
});
toRemove.forEach(flight => {
flightOrder.splice(flightOrder.indexOf(flight), 1);
$(`#${flight}`).remove();
});
toAdd.forEach(flightId => element("div", { id: flightId }, element("span", {})).appendTo("#flights-container"));
myFlights = flights;
flights.forEach(flight => {
$.getJSON(`/rest/flights/${flight.id}/tasks`, tasks => {
$(`#${flight.id} > *`).replaceWith(renderFlight(flight, tasks));
});
});
});
}
function getFlightName(flight) {
return `${flight.flight.origin} -> ${flight.flight.destination}, departing on ${moment(flight.params.departureDateTime).format("LLL")}`;
}
function findTasks(taskName, tasks) {
return tasks.filter(task => task.name === taskName);
}
function getPassengersToApproveDeny(flight, tasks, map) {
findTasks("approveDenyPassenger", tasks).map(task => {
return $.getJSON(`/rest/flights/${flight.id}/approveDenyPassenger/${task.id}`, map);
});
}
function generatePassengersForFlight(flight) {
const SEAT_TYPE_PREFERENCE_CHOICES = ["NONE", "WINDOW", "AISLE"];
const passengersToAddList = [];
for (let i = 0; i < flight.flight.seatList.length * 0.8; i++) {
passengersToAddList.push(JSON.stringify({
name: randomName(),
seatTypePreference: SEAT_TYPE_PREFERENCE_CHOICES[Math.floor(Math.random() * SEAT_TYPE_PREFERENCE_CHOICES.length)],
emergencyExitRowCapable: Math.random() < 0.8,
paidForSeat: false
}));
}
Promise.all(passengersToAddList.map(newPassengerRequest => $.post(`/rest/flights/${flight.id}/newPassengerRequest`, newPassengerRequest, () => {}, "json"))).then(() => {
$.getJSON(`/rest/flights/${flight.id}/tasks`, tasks => {
Promise.all(findTasks("approveDenyPassenger", tasks).map(task => $.post(`/rest/flights/${flight.id}/approveDenyPassenger/${task.id}`, JSON.stringify({
isPassengerApproved: true
}), () => {}, "json"))).then(() => refresh());
});
});
}
function initModal() {
$("#new-flight-modal").on('show.bs.modal', () => {
const newFlightAction = $("#new-flight-action");
newFlightAction.unbind();
const modal = $(this);
$('.modal-title').text("Create New Flight");
$('#origin').val("JFK");
$('#destination').val("SFO");
$("#departureDate").val(moment().add(1, 'day').startOf('day').format("YYYY-MM-DD"));
$("#departureTime").val(moment().add(1, 'day').startOf('minute').format("HH:mm"));
$("#seatRowSize").val(4);
$("#seatColumnSize").val(6);
newFlightAction.off('click').click(() => {
const origin = $('#origin').val();
const destination = $('#destination').val();
const departureDate = $('#departureDate').val();
const departureTime = $('#departureTime').val();
const departureDateTime = departureDate + "T" + departureTime;
const seatRowSize = $('#seatRowSize').val();
const seatColumnSize = $("#seatColumnSize").val();
const newFlightData = JSON.stringify({
params: {
origin,
destination,
departureDateTime,
seatRowSize,
seatColumnSize
}
});
$.post("/rest/flights", newFlightData, () => {
refresh();
}, "json");
$('#new-flight-modal').modal('toggle');
});
});
$("#add-passenger-modal").on('show.bs.modal', event => {
var button = $(event.relatedTarget);
var action = button.data('action');
var flight = button.data('flight');
const flightObject = myFlights.find(f => f.id === flight);
var addPassengerAction = $("#add-passenger-action");
addPassengerAction.unbind();
var modal = $("#add-passenger-modal");
modal.find('.modal-content').css({ width: 'min-content' });
modal.find('.modal-title').text(`Add Passenger to Flight ${getFlightName(flightObject)}`);
modal.find('#name').val(randomName());
modal.find('#seatTypePreference').val("NONE");
modal.find('#emergencyExitRowCapable').prop('checked', false);
modal.find('#paidForSeat').prop('checked', false);
modal.find('#seatPicker').replaceWith(element("div", { id: "seatPicker", class: "seat-picker hide-seat-picker" }, flightSeats(flightObject, ({seat, passenger}) => element("input",
{ type: "radio", class: "form-control", name: "flight-seat", value: seat.row + ";" + seat.column, disabled: passenger !== undefined, hidden: passenger !== undefined }))));
modal.find("#paidForSeat").off('click').click(() => $("#add-passenger-modal").find('#seatPicker').toggleClass("hide-seat-picker"));
addPassengerAction.off('click').click(() => {
const name = $('#name').val();
const seatTypePreference = $('#seatTypePreference').val();
const emergencyExitRowCapable = $('#emergencyExitRowCapable').prop( "checked" );
const paidForSeat = $('#paidForSeat').prop( "checked" );
const seat = (paidForSeat)? $('input[name="flight-seat"]:checked').val() : null;
const newPassengerRequest = JSON.stringify({
name,
seatTypePreference,
emergencyExitRowCapable,
paidForSeat,
seat
});
$.post(`/rest/flights/${flight}/newPassengerRequest`, newPassengerRequest, () => {
refresh();
}, "json");
$('#add-passenger-modal').modal('toggle');
});
});
}
/**
* Returns a new jQuery object with a given element type, attributes, and text/children elements
* @param {string} elementType - The type of the element to create (for instance, div)
* @param {object} props - props to add to the object (class, id, onClick, etc.)
* @param {string[]|jQuery[]} childs - The children elements of the object; either a single string or a list
* of jQuery elements
* @returns jQuery
*/
function element(elementType, props, ...childs) {
const out = $(`<${elementType} />`);
const onRegex = /^on.+/g;
Object.keys(props).forEach(prop => {
if (prop.match(onRegex)) {
if (typeof out[prop.substring(2).toLowerCase()] === "function") {
out[prop.substring(2).toLowerCase()](props[prop]);
}
else {
const msg = `There is no event handler for ${prop} in JQuery.`;
console.error(msg);
throw new Error(msg);
}
}
else {
out.attr(prop, props[prop]);
}
});
if (childs.length === 1 && typeof childs[0] === 'string' || typeof childs[0] === 'number') {
out.text(childs[0]);
}
else {
childs.forEach(child => {
if (child instanceof $) {
out.append(child);
}
else {
const msg = "Only JQuery elements allowed in elements with more than 1 child.";
console.error(msg);
throw new Error(msg);
}
});
}
return out;
}
const memoizeMap = new Map();
/**
* Returns a jQuery object created from consumer with the default value, which will be updated
* when a future promise resolves.
* @param {object} defaultValue - Default value to be used by the consumer
* @param {(function resolve) => void} futureValueSupplier - A promise that returns the value to be used by the consumer
* @param {(object props) => jQuery} consumer - Creates the jquery object to be used
* @returns jQuery
*/
function future(key, defaultValue, futureValueSupplier, consumer) {
if (key !== null && memoizeMap.has(key)) {
return consumer(memoizeMap.get(key));
}
const out = consumer(defaultValue);
out.attr("data-is-loading", true);
futureValueSupplier(futureValue => {
if (key !== null) {
memoizeMap.set(key, futureValue);
}
const oldIsLoading = out.parent().attr("data-is-loading");
if (oldIsLoading !== undefined) {
const newIsLoading = parseInt(oldIsLoading) - 1;
if (newIsLoading !== 0) {
out.parent().attr("data-is-loading", newIsLoading);
}
else {
out.parent().removeAttr("data-is-loading");
}
}
out.replaceWith(consumer(futureValue));
});
return out;
}
function suspense(loadingElement, parentElement) {
const loadingCount = parentElement.children("[data-is-loading]").length;
if (loadingCount > 0) {
parentElement.attr("data-is-loading", loadingCount);
parentElement.addClass("suspended");
}
parentElement.addClass("suspense");
return element("div", {},
parentElement,
loadingElement
);
}
function showError(title, xhr) {
var serverErrorMessage = xhr.responseJSON == null ? "No response from server." : xhr.responseJSON.message;
console.error(title + "\n" + serverErrorMessage);
var notification = $(""
+ ""
+ "Error"
+ ""
+ ""
+ "" + title + "
" + serverErrorMessage + "
"
+ "");
$("#notificationPanel").append(notification);
notification.toast({delay: 30000});
notification.toast('show');
}
$(document).ready( function() {
$.ajaxSetup({
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
}
});
// Extend jQuery to support $.put() and $.delete()
jQuery.each( [ "put", "delete" ], function( i, method ) {
jQuery[method] = function (url, data, callback, type) {
if (jQuery.isFunction(data)) {
type = type || callback;
callback = data;
data = undefined;
}
return jQuery.ajax({
url: url,
type: method,
dataType: type,
data: data,
success: callback
});
};
});
initModal();
refresh();
});
// ****************************************************************************
// Name Generator
// ****************************************************************************
let nameSequence = 0;
function randomName() {
const FIRST_NAMES = ["Amy", "Bill", "Chris", "Dennis", "Hope", "Eve", "Frank", "Ivana", "Julianna", "Manci", "Olivia", "Sarah"];
const MIDDLE_INITALS = [...Array('Z'.charCodeAt(0) - 'A'.charCodeAt(0) + 1).keys()].map(i => String.fromCharCode(i + 'A'.charCodeAt(0)));
const LAST_NAMES = ["Cole", "Zhang", "Smith"];
const out = `${FIRST_NAMES[nameSequence % FIRST_NAMES.length]} ${MIDDLE_INITALS[Math.floor(nameSequence / FIRST_NAMES.length)]} ${LAST_NAMES[Math.floor((nameSequence / FIRST_NAMES.length) / LAST_NAMES.length)]}`
nameSequence = (nameSequence + 1) % (FIRST_NAMES.length * LAST_NAMES.length);
return out;
}
// ****************************************************************************
// TangoColorFactory
// ****************************************************************************
const SEQUENCE_1 = [0x8AE234, 0xFCE94F, 0x729FCF, 0xE9B96E, 0xAD7FA8];
const SEQUENCE_2 = [0x73D216, 0xEDD400, 0x3465A4, 0xC17D11, 0x75507B];
var colorMap = new Map;
var nextColorCount = 0;
function pickColor(object) {
let color = colorMap[object];
if (color !== undefined) {
return color;
}
color = nextColor();
colorMap[object] = color;
return color;
}
function nextColor() {
let color;
let colorIndex = nextColorCount % SEQUENCE_1.length;
let shadeIndex = Math.floor(nextColorCount / SEQUENCE_1.length);
if (shadeIndex === 0) {
color = SEQUENCE_1[colorIndex];
} else if (shadeIndex === 1) {
color = SEQUENCE_2[colorIndex];
} else {
shadeIndex -= 3;
let floorColor = SEQUENCE_2[colorIndex];
let ceilColor = SEQUENCE_1[colorIndex];
let base = Math.floor((shadeIndex / 2) + 1);
let divisor = 2;
while (base >= divisor) {
divisor *= 2;
}
base = (base * 2) - divisor + 1;
let shadePercentage = base / divisor;
color = buildPercentageColor(floorColor, ceilColor, shadePercentage);
}
nextColorCount++;
return "#" + color.toString(16);
}
function buildPercentageColor(floorColor, ceilColor, shadePercentage) {
let red = (floorColor & 0xFF0000) + Math.floor(shadePercentage * ((ceilColor & 0xFF0000) - (floorColor & 0xFF0000))) & 0xFF0000;
let green = (floorColor & 0x00FF00) + Math.floor(shadePercentage * ((ceilColor & 0x00FF00) - (floorColor & 0x00FF00))) & 0x00FF00;
let blue = (floorColor & 0x0000FF) + Math.floor(shadePercentage * ((ceilColor & 0x0000FF) - (floorColor & 0x0000FF))) & 0x0000FF;
return red | green | blue;
}