Prerequisites
- Basic JavaScript knowledge.
- Basic knowledge of JavaScript asynchronous programming using Promises.
- Helpful: Basic knowledge of jQuery. A page like the W3 Schools jQuery tutorial is sufficient.
How to use this tutorial
This tutorial illustrates step by step how to implement a viewer for bin packing with the PTV Developer Loading Space Optimization API with JavaScript.
Getting started
- Request an API key:
- Register and login at myptv.com
- Activate PTV Developer
- Create your API key
- Download an editor such as Visual Studio Code or Atom which work with HTML and JavaScript.
- You can find more details of requests and responses in the reference manual.
- Download the package from GitHub and get the loadViewer.js and convertResponsePackedBins.js files. Copy the LoadViewer folder which contains the implementation of the viewer using the library three.js.
Create an HTML page and prepare the controls
Create a new HTML file called index.html, prepare all the div layers and finally define all needed scripts.
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Loading Space Optimization</title>
<link rel="stylesheet" href="style.css" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
</head>
<body>
<div class="viewport">
<div class="wrapper">
<h1>Loading Space Optimization</h1>
<a href="https://company.ptvgroup.com/">by PTV Group</a>
<div class="entry-settings">
<h4>Bin</h4>
<h6>...</h6>
</div>
<div class="entry-settings">
<h4>Item</h4>
<h6>...</h6>
</div>
<div class="entry-settings">
<h4>Focus</h4>
<select id="focus">
<option value="BUILD_LAYERS" selected>BUILD_LAYERS</option>
<option value="REDUCE_LOADING_METERS">REDUCE_LOADING_METERS</option>
</select>
</div>
<div class="group space-between">
<button id="btn-start-optimization" disabled>Optimize your items</button>
<button id="clear-data">Clear all data</button>
</div>
</div>
<div id="binViewer"></div>
<div id="optimization-results">
<div>
<div id="bin-switcher" class="group">
<button id="previous-bin"><</button>
<div class="bin-info">
<label id="bin-index">-</label>
<label id="bin-id">-</label>
</div>
<button id="next-bin">></button>
</div>
<div class="key-value"><span>Number of items</span><span id="total-items-count">-</span></div>
<div class="key-value"><span>Travel items volume</span><span id="total-items-volume">-</span></div>
<div class="key-value"><span>Travel items weight</span><span id="total-items-weight">-</span></div>
<div class="key-value"><span>Used weight capacity</span><span id="used-weight-capacity">-</span></div>
<div class="key-value"><span>Used volume capacity</span><span id="used-volume-capacity">-</span></div>
<div class="key-value"><span>Loading meters</span><span id="loading-meters">-</span></div>
</div>
<div id="kpis">
<h3>Optimization results</h3>
<h5>Bins</h5>
<div class="key-value"><span>Used</span><span id="used-bins">-</span></div>
<div class="key-value"><span>Unused</span><span id="unused-bins">-</span></div>
<h5>Items</h5>
<div class="key-value"><span>Packed</span><span id="packed-items">-</span></div>
<div class="key-value"><span>Unpacked</span><span id="unpacked-items">-</span></div>
</div>
</div>
</div>
<!-- load viewer functions -->
<script src="LoadViewer/three.min.js" charset="utf-8"></script>
<script src="LoadViewer/threex.domevents.js" charset="utf-8"></script>
<script src="LoadViewer/OrbitControls.js" charset="utf-8"></script>
<script src="LoadViewer/packwidget.js" charset="utf-8"></script>
<script src="LoadViewer/packWidgetUtils.js" charset="utf-8"></script>
<script src="LoadViewer/mapping.js" charset="utf-8"></script>
<script src="./convertResponsePackedBins.js"></script>
<script src="./loadViewer.js"></script>
<script src="general-helper-functions.js"></script>
<script src="loading-space-optimization.js"></script>
</body>
</html>
Please note that div layer with the id="binViewer" is the base layer for the viewer.
Create the JavaScript code
Create a new JavaScript file named loading-space-optimization.js.
Initialization and main functions
The first step is to add the code below and replace the string "YOUR_API_KEY" with your PTV Developer API key if this has not already been done.
const api_key = "YOUR_API_KEY";
const APIEndpoints = {
StartBinPacking: "https://api.myptv.com/binpacking/v1/bins/async",
GetStatus: (binpackingId) => `https://api.myptv.com/binpacking/v1/status/${binpackingId}`,
GetPackedBins: (binpackingId) => `https://api.myptv.com/binpacking/v1/bins/${binpackingId}`
};
// Always add the API key to request headers
const applyAPIKey = (configuration) => ({
...configuration,
headers: {
"apiKey": api_key,
...configuration ? { ...configuration.headers } : {}
}
});
/**
* This object represents the application state
*/
const appState = {
bins: [],
items: [],
optimizedResult: {request: undefined, response: undefined},
selectedBinIndex: 0
};
/**
* Applications entry point, triggered by "window.onload" event
*/
const initializeApplication = () => {
getElement("btn-start-optimization").addEventListener("click", optimize);
getElement("previous-bin").addEventListener("click", () => switchSelectedBin(-1));
getElement("next-bin").addEventListener("click", () => switchSelectedBin(1));
window.addEventListener('beforeunload', (e) => { e.preventDefault(); e.returnValue = ''; });
};
window.onload = initializeApplication;
Implement the optimization process
Main optimize function
In the javascript file, the optimize function is added. It is responsible to the whole optimization process.
const optimize = async () => {
let requestBody = {
items: appState.items,
bins: appState.bins
};
let focusCtrl = getElement("focus");
let focus = focusCtrl.options[focusCtrl.selectedIndex].value;
const startSuccessful = await startOptimization(focus, requestBody);
if (!startSuccessful) return;
const interval = setInterval( async () => {
const progress = await getOptimizationProgress(startSuccessful.id);
if (!progress) return clearInterval(interval);
if (progress.status === "SUCCEEDED" || progress.status === "FAILED") {
clearInterval(interval);
const optimizedResult = await getOptimizedResult(startSuccessful.id);
if (!optimizedResult) return;
appState.optimizedResult.request = requestBody;
appState.optimizedResult.response = optimizedResult;
handleResponse($("#binViewer"), appState.optimizedResult.request, appState.optimizedResult.response);
populateBinDetails();
populateKPIs();
showElement("optimization-results", "flex");
}
}, 500);
};
The first lines of the function create the request body and get the query parameters.
The startOptimization will start the optimization using as an async process. Then, we use setInterval to watch over the optimization process until it finishes using getOptimizationProgress
When the optimization is finished, the function getOptimizedResult returns the result.
Finally the viewer is initialized using the request and the response. Moreover the binDetails and KPIs layers are updated.
Start the optimization process
Implement the startOptimization function. The function returns the id of the optimization process.
const startOptimization = (focus, requestBody) =>
fetch(
APIEndpoints.StartBinPacking + '?focus=' + focus,
applyHeaders({
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(requestBody)
})
).then(response => response.ok ? response.json() : logError(response));
Watch the optimization process
Implement the getOptimizationProcess function. The function returns the status of the optimization process.
const getOptimizationProgress = (id) =>
fetch(
APIEndpoints.GetStatus(id),
applyHeaders()
).then(response => response.ok ? response.json() : logError(response));
Get the result
Implement the getOptimizedResult function.
const getOptimizedResult = (id) =>
fetch(
APIEndpoints.GetPackedBins(id),
applyHeaders()
).then(response => response.ok ? response.json() : logError(response));
Control the viewer
Initialize the viewer
To initialize the viewer, you need to pass the request and the response.
The main entry point is the handleResponse function, which is responsible to decode the response and initialize the viewer.
Then to switch between the result bins, you can call the changeBinView. In our app, the function is implemented by the following function.
const switchSelectedBin = (step) => {
const { selectedBinIndex } = appState;
const usedBins = appState.optimizedResult.response.packedBins.length;
let newIndex = selectedBinIndex + step;
if (newIndex < 0) newIndex = usedBins - 1;
if (newIndex > usedBins - 1) newIndex = 0;
appState.selectedBinIndex = newIndex;
changeBinView(newIndex);
populateBinDetails();
};
Fill the result layers
Show the bin details
const populateBinDetails = () => {
const response = appState.optimizedResult.response;
const selectedBin = response.packedBins[appState.selectedBinIndex];
getElement("bin-index" ).innerText = 'index: ' + appState.selectedBinIndex;
getElement("bin-id" ).innerText = selectedBin ? selectedBin.binId : '-';
getElement("total-items-count" ).innerText = selectedBin ? selectedBin.packedItems.length : '-';
getElement("total-items-volume" ).innerText = selectedBin ? (selectedBin.totalItemsVolume / 100 / 100 / 100).toFixed(3) + ' \u33A5' : '-';
getElement("total-items-weight" ).innerText = selectedBin ? selectedBin.totalItemsWeight/1000 + ' kg' : "-";
getElement("used-weight-capacity").innerText = selectedBin ? selectedBin.usedWeightCapacity.toFixed(2) + ' %' : "-";
getElement("used-volume-capacity").innerText = selectedBin ? selectedBin.usedVolumeCapacity.toFixed(2) + ' %' : "-";
getElement("loading-meters" ).innerText = selectedBin ? selectedBin.loadingMeters.toFixed(2) + ' m' : "-";
};
Show the global KPIs
const populateKPIs = () => {
const { request, response } = appState.optimizedResult;
const totalAvailableBins = request.bins.reduce((sum, bin) => sum + bin.numberOfInstances, 0);
const totalAvailableItems = request.items.reduce((sum, item) => sum + item.numberOfInstances, 0);
const totalUsedBins = response.packedBins.length;
const totalUnpackedItems = response.itemsNotPacked.reduce((sum, item) => sum + item.numberOfInstances, 0);
getElement("used-bins").innerText = totalUsedBins;
getElement("unused-bins").innerText = totalAvailableBins - totalUsedBins;
getElement("packed-items").innerText = totalAvailableItems - totalUnpackedItems;
getElement("unpacked-items").innerText = totalUnpackedItems;
};