This commit is contained in:
2025-01-14 14:45:15 +08:00
commit e772701266
486 changed files with 69377 additions and 0 deletions

302
public/html/app.js Normal file
View File

@@ -0,0 +1,302 @@
var autoRefreshIntervalId = null;
let loadedSchedule = null;
const dateTimeFormat = JSJoda.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
const byLinePanel = document.getElementById("byLinePanel");
const byLineTimelineOptions = {
timeAxis: {scale: "hour"},
orientation: {axis: "top"},
stack: false,
xss: {disabled: true}, // Items are XSS safe through JQuery
zoomMin: 1000 * 60 * 60 * 12 // Half day in milliseconds
};
var byLineGroupDataSet = new vis.DataSet();
var byLineItemDataSet = new vis.DataSet();
var byLineTimeline = new vis.Timeline(byLinePanel, byLineItemDataSet, byLineGroupDataSet, byLineTimelineOptions);
const byJobPanel = document.getElementById("byJobPanel");
const byJobTimelineOptions = {
timeAxis: {scale: "hour"},
orientation: {axis: "top"},
stack: false,
xss: {disabled: true}, // Items are XSS safe through JQuery
zoomMin: 1000 * 60 * 60 * 12 // Half day in milliseconds
};
var byJobGroupDataSet = new vis.DataSet();
var byJobItemDataSet = new vis.DataSet();
var byJobTimeline = new vis.Timeline(byJobPanel, byJobItemDataSet, byJobGroupDataSet, byJobTimelineOptions);
$(document).ready(function () {
replaceTimefoldAutoHeaderFooter();
$("#refreshButton").click(function () {
refreshSchedule();
});
$("#solveButton").click(function () {
solve();
});
$("#stopSolvingButton").click(function () {
stopSolving();
});
$("#analyzeButton").click(function () {
analyze();
});
// HACK to allow vis-timeline to work within Bootstrap tabs
$("#byLineTab").on('shown.bs.tab', function (event) {
byLineTimeline.redraw();
})
$("#byJobTab").on('shown.bs.tab', function (event) {
byJobTimeline.redraw();
})
setupAjax();
refreshSchedule();
});
function setupAjax() {
$.ajaxSetup({
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json,text/plain', // plain text is required by solve() returning UUID of the solver job
}
});
// 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
});
};
});
}
function refreshSchedule() {
$.getJSON("/schedule", function (schedule) {
refreshSolvingButtons(schedule.solverStatus != null && schedule.solverStatus !== "NOT_SOLVING");
$("#score").text("Score: " + (schedule.score == null ? "?" : schedule.score));
loadedSchedule = schedule;
const unassignedJobs = $("#unassignedJobs");
unassignedJobs.children().remove();
var unassignedJobsCount = 0;
byLineGroupDataSet.clear();
byJobGroupDataSet.clear();
byLineItemDataSet.clear();
byJobItemDataSet.clear();
$.each(schedule.lines, (index, line) => {
const lineGroupElement = $(`<div/>`)
.append($(`<h5 class="card-title mb-1"/>`).text(line.name))
.append($(`<p class="card-text ms-2 mb-0"/>`).text(line.operator));
byLineGroupDataSet.add({id : line.id, content: lineGroupElement.html()});
});
$.each(schedule.jobs, (index, job) => {
byJobGroupDataSet.add({id : job.id, content: job.name});
byJobItemDataSet.add({
id: job.id + "_readyToIdealEnd", group: job.id,
start: job.readyDateTime,
end: job.idealEndDateTime,
type: "background",
style: "background-color: #8AE23433"
});
byJobItemDataSet.add({
id: job.id + "_idealEndToDue", group: job.id,
start: job.idealEndDateTime,
end: job.dueDateTime,
type: "background",
style: "background-color: #FCAF3E33"
});
if (job.line == null || job.startCleaningDateTime == null || job.startProductionDateTime == null || job.endDateTime == null) {
unassignedJobsCount++;
const durationMinutes = JSJoda.Duration.ofSeconds(job.duration).toMinutes();
const unassignedJobElement = $(`<div class="card-body p-2"/>`)
.append($(`<h5 class="card-title mb-1"/>`).text(job.name))
.append($(`<p class="card-text ms-2 mb-0"/>`).text(`${Math.floor(durationMinutes / 60)} hours ${durationMinutes % 60} mins`))
.append($(`<p class="card-text ms-2 mb-0"/>`).text(`Ready: ${JSJoda.LocalDateTime.parse(job.readyDateTime).format(dateTimeFormat)}`))
.append($(`<p class="card-text ms-2 mb-0"/>`).text(`Ideal end: ${JSJoda.LocalDateTime.parse(job.idealEndDateTime).format(dateTimeFormat)}`))
.append($(`<p class="card-text ms-2 mb-0"/>`).text(`Due: ${JSJoda.LocalDateTime.parse(job.dueDateTime).format(dateTimeFormat)}`));
const byJobJobElement = $(`<div/>`)
.append($(`<h5 class="card-title mb-1"/>`).text(`Unassigned`));
unassignedJobs.append($(`<div class="col"/>`).append($(`<div class="card"/>`).append(unassignedJobElement)));
byJobItemDataSet.add({
id : job.id, group: job.id,
content: byJobJobElement.html(),
start: job.readyDateTime, end: JSJoda.LocalDateTime.parse(job.readyDateTime).plus(JSJoda.Duration.ofSeconds(job.duration)).toString(),
style: "background-color: #EF292999"
});
} else {
const beforeReady = JSJoda.LocalDateTime.parse(job.startProductionDateTime).isBefore(JSJoda.LocalDateTime.parse(job.readyDateTime));
const afterDue = JSJoda.LocalDateTime.parse(job.endDateTime).isAfter(JSJoda.LocalDateTime.parse(job.dueDateTime));
const byLineJobElement = $(`<div/>`)
.append($(`<p class="card-text"/>`).text(job.name));
const byJobJobElement = $(`<div/>`)
.append($(`<p class="card-text"/>`).text(job.line.name));
if (beforeReady) {
byLineJobElement.append($(`<p class="badge badge-danger mb-0"/>`).text(`Before ready (too early)`));
byJobJobElement.append($(`<p class="badge badge-danger mb-0"/>`).text(`Before ready (too early)`));
}
if (afterDue) {
byLineJobElement.append($(`<p class="badge badge-danger mb-0"/>`).text(`After due (too late)`));
byJobJobElement.append($(`<p class="badge badge-danger mb-0"/>`).text(`After due (too late)`));
}
byLineItemDataSet.add({
id : job.id + "_cleaning", group: job.line.id,
content: "Cleaning",
start: job.startCleaningDateTime, end: job.startProductionDateTime,
style: "background-color: #FCAF3E99"
});
byLineItemDataSet.add({
id : job.id, group: job.line.id,
content: byLineJobElement.html(),
start: job.startProductionDateTime, end: job.endDateTime
});
byJobItemDataSet.add({
id : job.id + "_cleaning", group: job.id,
content: "Cleaning",
start: job.startCleaningDateTime, end: job.startProductionDateTime,
style: "background-color: #FCAF3E99"
});
byJobItemDataSet.add({
id : job.id, group: job.id,
content: byJobJobElement.html(),
start: job.startProductionDateTime, end: job.endDateTime
});
}
});
if (unassignedJobsCount === 0) {
unassignedJobs.append($(`<p/>`).text(`There are no unassigned jobs.`));
}
const nextDate = JSJoda.LocalDate.parse(schedule.workCalendar.fromDate).plusDays(1);
byLineTimeline.setWindow(schedule.workCalendar.fromDate, nextDate.toString());
byJobTimeline.setWindow(schedule.workCalendar.fromDate, nextDate.toString());
});
}
function solve() {
$.post("/schedule/solve", function () {
refreshSolvingButtons(true);
}).fail(function (xhr, ajaxOptions, thrownError) {
showError("Start solving failed.", xhr);
});
}
function analyze() {
new bootstrap.Modal("#scoreAnalysisModal").show()
const scoreAnalysisModalContent = $("#scoreAnalysisModalContent");
scoreAnalysisModalContent.children().remove();
if (loadedSchedule.score == null || loadedSchedule.score.indexOf('init') != -1) {
scoreAnalysisModalContent.text("No score to analyze yet, please first press the 'solve' button.");
} else {
$('#scoreAnalysisScoreLabel').text(`(${loadedSchedule.score})`);
$.put("/schedule/analyze", function (scoreAnalysis) {
let constraints = scoreAnalysis.constraints;
constraints.sort((a, b) => {
let aComponents = getScoreComponents(a.score), bComponents = getScoreComponents(b.score);
if (aComponents.hard < 0 && bComponents.hard > 0) return -1;
if (aComponents.hard > 0 && bComponents.soft < 0) return 1;
if (Math.abs(aComponents.hard) > Math.abs(bComponents.hard)) {
return -1;
} else {
if (aComponents.medium < 0 && bComponents.medium > 0) return -1;
if (aComponents.medium > 0 && bComponents.medium < 0) return 1;
if (Math.abs(aComponents.medium) > Math.abs(bComponents.medium)) {
return -1;
} else {
if (aComponents.soft < 0 && bComponents.soft > 0) return -1;
if (aComponents.soft > 0 && bComponents.soft < 0) return 1;
return Math.abs(bComponents.soft) - Math.abs(aComponents.soft);
}
}
});
constraints.map((e) => {
let components = getScoreComponents(e.weight);
e.type = components.hard != 0 ? 'hard' : (components.medium != 0 ? 'medium' : 'soft');
e.weight = components[e.type];
let scores = getScoreComponents(e.score);
e.implicitScore = scores.hard != 0 ? scores.hard : (scores.medium != 0 ? scores.medium : scores.soft);
});
scoreAnalysis.constraints = constraints;
scoreAnalysisModalContent.children().remove();
scoreAnalysisModalContent.text("");
const analysisTable = $(`<table class="table"/>`).css({textAlign: 'center'});
const analysisTHead = $(`<thead/>`).append($(`<tr/>`)
.append($(`<th></th>`))
.append($(`<th>Constraint</th>`).css({textAlign: 'left'}))
.append($(`<th>Type</th>`))
.append($(`<th># Matches</th>`))
.append($(`<th>Weight</th>`))
.append($(`<th>Score</th>`))
.append($(`<th></th>`)));
analysisTable.append(analysisTHead);
const analysisTBody = $(`<tbody/>`)
$.each(scoreAnalysis.constraints, (index, constraintAnalysis) => {
let icon = constraintAnalysis.type == "hard" && constraintAnalysis.implicitScore < 0 ? '<span class="fas fa-exclamation-triangle" style="color: red"></span>' : '';
if (!icon) icon = constraintAnalysis.matches.length == 0 ? '<span class="fas fa-check-circle" style="color: green"></span>' : '';
let row = $(`<tr/>`);
row.append($(`<td/>`).html(icon))
.append($(`<td/>`).text(constraintAnalysis.name).css({textAlign: 'left'}))
.append($(`<td/>`).text(constraintAnalysis.type))
.append($(`<td/>`).html(`<b>${constraintAnalysis.matches.length}</b>`))
.append($(`<td/>`).text(constraintAnalysis.weight))
.append($(`<td/>`).text(constraintAnalysis.implicitScore));
analysisTBody.append(row);
row.append($(`<td/>`));
});
analysisTable.append(analysisTBody);
scoreAnalysisModalContent.append(analysisTable);
}).fail(function (xhr, ajaxOptions, thrownError) {
showError("Analyze failed.", xhr);
}, "text");
}
}
function getScoreComponents(score) {
let components = {hard: 0, medium: 0, soft: 0};
$.each([...score.matchAll(/(-?[0-9]+)(hard|medium|soft)/g)], (i, parts) => {
components[parts[2]] = parseInt(parts[1], 10);
});
return components;
}
function refreshSolvingButtons(solving) {
if (solving) {
$("#solveButton").hide();
$("#stopSolvingButton").show();
if (autoRefreshIntervalId == null) {
autoRefreshIntervalId = setInterval(refreshSchedule, 2000);
}
} else {
$("#solveButton").show();
$("#stopSolvingButton").hide();
if (autoRefreshIntervalId != null) {
clearInterval(autoRefreshIntervalId);
autoRefreshIntervalId = null;
}
}
}
function stopSolving() {
$.post("/schedule/stopSolving", function () {
refreshSolvingButtons(false);
refreshSchedule();
}).fail(function (xhr, ajaxOptions, thrownError) {
showError("Stop solving failed.", xhr);
});
}

45
public/html/ie.html Normal file

File diff suppressed because one or more lines are too long

98
public/html/index.html Normal file
View File

@@ -0,0 +1,98 @@
<html lang="en">
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type">
<meta content="width=device-width, initial-scale=1" name="viewport">
<title>玻璃产线排产示例</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/vis-timeline@7.7.2/styles/vis-timeline-graph2d.min.css"
integrity="sha256-svzNasPg1yR5gvEaRei2jg+n4Pc3sVyMUWeS6xRAh6U=" crossorigin="anonymous">
<link rel="stylesheet" href="/webjars/bootstrap/css/bootstrap.min.css"/>
<link rel="stylesheet" href="/webjars/font-awesome/css/all.css"/>
<link rel="stylesheet" href="/webjars/timefold/css/timefold-webui.css" />
<style>
.vis-time-axis .vis-grid.vis-saturday,
.vis-time-axis .vis-grid.vis-sunday {
background: #D3D7CFFF;
}
</style>
<link rel="icon" href="/webjars/timefold/img/timefold-favicon.svg" type="image/svg+xml">
</head>
<body>
<header id="timefold-auto-header"></header>
<div class="container-fluid">
<div class="sticky-top d-flex justify-content-center align-items-center" aria-live="polite" aria-atomic="true">
<div id="notificationPanel" style="position: absolute; top: .5rem;"></div>
</div>
<h1>玻璃产线排产</h1>
<p>为您的玻璃生产线制定最佳计划。</p>
<div class="mb-2">
<button id="refreshButton" type="button" class="btn btn-secondary">
<span class="fas fa-refresh"></span> Refresh
</button>
<button id="solveButton" type="button" class="btn btn-success">
<span class="fas fa-play"></span> Solve
</button>
<button id="stopSolvingButton" type="button" class="btn btn-danger">
<span class="fas fa-stop"></span> Stop solving
</button>
<span id="score" class="score ms-2 align-middle fw-bold">Score: ?</span>
<button id="analyzeButton" type="button" class="ms-2 btn btn-secondary">
<span class="fas fa-question"></span>
</button>
<div class="float-end">
<ul class="nav nav-pills" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="byLineTab" data-bs-toggle="tab" data-bs-target="#byLinePanel" type="button" role="tab" aria-controls="byLinePanel" aria-selected="true">By line</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="byJobTab" data-bs-toggle="tab" data-bs-target="#byJobPanel" type="button" role="tab" aria-controls="byJobPanel" aria-selected="false">By job</button>
</li>
</ul>
</div>
</div>
<div class="mb-4 tab-content">
<div class="tab-pane fade show active" id="byLinePanel" role="tabpanel" aria-labelledby="byLineTab">
<div id="lineVisualization"></div>
</div>
<div class="tab-pane fade" id="byJobPanel" role="tabpanel" aria-labelledby="byJobTab">
<div id="jobVisualization"></div>
</div>
</div>
<h2>Unassigned jobs</h2>
<div id="unassignedJobs" class="row row-cols-3 g-3 mb-4"></div>
</div>
<footer id="timefold-auto-footer"></footer>
<div class="modal fadebd-example-modal-lg" id="scoreAnalysisModal" tabindex="-1"
aria-labelledby="scoreAnalysisModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="scoreAnalysisModalLabel">Score analysis <span
id="scoreAnalysisScoreLabel"></span></h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="scoreAnalysisModalContent">
<!-- Filled in by app.js -->
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<script src="/webjars/jquery/jquery.min.js"></script>
<script src="/webjars/bootstrap/js/bootstrap.min.js"></script>
<script src="/webjars/js-joda/dist/js-joda.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vis-timeline@7.7.2/standalone/umd/vis-timeline-graph2d.min.js"
integrity="sha256-Jy2+UO7rZ2Dgik50z3XrrNpnc5+2PAx9MhL2CicodME=" crossorigin="anonymous"></script>
<script src="/webjars/timefold/js/timefold-webui.js"></script>
<script src="/app.js"></script>
</body>
</html>