Public
Edited
Feb 13
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
inputs = {
const offset = new Date().getTimezoneOffset();
let newBase = new Date("2024-08-15T08:00:00-04:00");

let tmrwStart = new Date(
`${newBase.toLocaleDateString("en-US")} 08:00:00 EDT`
);

let tmrwEnd = new Date(`${newBase.toLocaleDateString("en-US")} 10:00:00 GMT`);
console.log(tmrwEnd);
const inputs = {
shiftStart: Inputs.date({
label: "Schedule Date",
value: tmrwStart.toISOString()
}),
maxPerSlot: Inputs.text({
readonly: false,
label: "Max Appts Per Slot",
value: "4",
width: 80
}),
startLocation: Inputs.text({
readonly: false,
label: "Technician Start/End Location",
value: "36.14863,-86.7795"
}),
serviceDuration: Inputs.text({
readonly: false,
label: "Service Duration(min)",
value: "60",
width: 80
}),
serviceWindowDivisions: Inputs.select(
[
{ name: "6", value: 6 },
{ name: "4", value: 4 },
{ name: "3", value: 3 },
{ name: "2", value: 2 }
],
{
label: "Number of Time Divisions in a Day",
width: 80,
sort: "descending",
format: (x) => x.name
}
),
roundTrip: Inputs.toggle({ label: "Roundtrip", value: true })
};
return inputs;
}
Insert cell
mutable theSlotsTable = []
Insert cell
function findParentByPath(baseSchedule, path) {
const pathParts = path.split(" > ");
const srchArray = [pathParts[1], pathParts[2]];
// Start traversing from the root
let currentNode = baseSchedule;
console.log(currentNode);
let parentNode = null;
console.log(srchArray);
// Traverse the path, skipping the last part which is the target node itself
for (let i = 0; i < srchArray.length - 1; i++) {
console.log(currentNode.children);
if (currentNode.children) {
parentNode = currentNode;
currentNode = currentNode.children.find(
(child) => child.name === srchArray[i]
);
} else {
// If we reach a node that doesn't have the expected children, the path is invalid
return null;
}
}

return parentNode;
}
Insert cell
mutable originSearchResult = FileAttachment("untitled.json").json()
Insert cell
function insertChildAtPath(baseSchedule, path, newChildData) {
// Split the path into an array of node names
const pathArray = path.split(" > ");
const srchArray = [pathArray[1], pathArray[2]];
// Initialize the current node as the root of the hierarchy
let currentNode = baseSchedule;

// Traverse the hierarchy according to the path
for (const nodeName of srchArray) {
// Find the child node with the matching name
const childNode = currentNode.children?.find(
(child) => child.name === nodeName
);

if (!childNode) {
console.error(`Node with name '${nodeName}' not found in path`);
return false; // Path does not exist
}

// Move to the child node
currentNode = childNode;
}

// Now `currentNode` is the parent where we want to add the new child
if (!currentNode.children) {
currentNode.children = []; // Initialize children array if it doesn't exist
}

// Insert the new child node into the parent's children array
currentNode.children.push(newChildData);
return true; // Indicate success
}
Insert cell
function findParentNode(root, targetNode) {
let parentNode = null;

// Traverse the hierarchy tree
root.each((node) => {
if (node.children) {
// Check if any of the node's children match the target node
node.children.forEach((child) => {
if (child === targetNode) {
parentNode = node;
}
});
}
});

return parentNode;
}
Insert cell
function findNodeByPath(root, path) {
const pathArray = path.split(" > ");
let currentNode = root;
const srchArray = [pathArray[1], pathArray[2]];
for (const name of srchArray) {
const foundChild = currentNode.children.find(
(child) => child.data.name === name
);
if (!foundChild) {
console.error(`Node with name '${name}' not found in path`);
return null;
}
currentNode = foundChild;
}

return currentNode.children || [];
}
Insert cell
function findNodeByName(root, nodeName) {
// Check if the current node is the one we're looking for
if (root.name === nodeName) {
return root;
}

// If the current node has children, search recursively
if (root.children) {
for (const child of root.children) {
const result = findNodeByName(child, nodeName);
if (result) {
return result; // Return the found node
}
}
}

// Return null if the node wasn't found
return null;
}
Insert cell
function saveHierarchyToBaseSchedule(hierarchy) {
console.log(hierarchy);
function traverse(node) {
// Copy the data of the current node
const nodeData = { ...node.data };

// If the node has children, recursively traverse them
if (node.children) {
nodeData.children = node.children.map(traverse);
}

return nodeData;
}

// Start traversal from the root of the hierarchy
const updatedBaseSchedule = traverse(hierarchy);

// Save the reconstructed tree back to mutable baseSchedule
console.log(updatedBaseSchedule);
mutable baseSchedule = updatedBaseSchedule;
}
Insert cell
function moveNode(sourceNode, targetNode) {
// Remove sourceNode from its current parent
const parentIndex = sourceNode.parent.children.indexOf(sourceNode);
sourceNode.parent.children.splice(parentIndex, 1);

// If the sourceNode's parent has no more children, set its children to null
if (sourceNode.parent.children.length === 0) {
sourceNode.parent.children = null;
}

// Add sourceNode to the targetNode's children
if (!targetNode.children) {
targetNode.children = [];
}
targetNode.children.push(sourceNode);

// Update the sourceNode's parent to be the targetNode
sourceNode.parent = targetNode;
}
Insert cell
geofenceData = {
let more = true;
let geofenceCollection = {
status: "",
data: {
list: []
}
};
let pn = 1;
while (more) {
let retval = null;
retval = await fetch(
`https://${serviceHost}/geofence/list?key=${apiKey}&ps=100&pn=${pn}`,
{
headers: {
"content-type": "application/json"
}
}
).then((response) => response.json());
more = retval.data.page.hasmore;
pn++;
retval.data.list.forEach((evt) => {
//if (evt.geojson) {
// evt.geojson.properties = {
// name: evt.name
// };
//}

if (!evt.meta_data.test) {
console.log(evt);
geofenceCollection.data.list.push(evt);
}
});
}
geofenceCollection.data.list = geofenceCollection.data.list.filter(
(item) => item.event_type != "idle"
);
return geofenceCollection;
}

Insert cell
baseRoutes = FileAttachment("gc_test-6@4.json").json()
Insert cell
function isFullNode(d) {
// Filter out children whose name contains 'start' or 'end'
const validChildren = d.children
? d.children.filter(
(child) =>
!child.data.name.toLowerCase().includes("start") &&
!child.data.name.toLowerCase().includes("end")
)
: [];

// Return true if the node has exactly 4 valid children
console.log(validChildren.length);
return validChildren.length >= parseInt(panel1.maxPerSlot);
}
Insert cell
function collapseNodesByCriteria(node, criteria) {
if (criteria(node)) {
// Collapse the node by setting its children to null
node.children = null;
} else if (node.children) {
// Recursively check and collapse children
node.children.forEach((child) => collapseNodesByCriteria(child, criteria));
}
}
Insert cell
// Traverse the hierarchy using a breadth-first approach
function findOpenSlots(node, maxPerSlot) {
let openSlots = [];

node.each((d) => {
if (d.depth === 2 && d.children) {
const validChildren = d.children.filter(
(child) =>
!child.data.name.toLowerCase().includes("start") &&
!child.data.name.toLowerCase().includes("end")
);

// Check if this node has fewer than 4 children
if (validChildren.length < parseInt(panel1.maxPerSlot)) {
// Now check whether it is feasible to insert the job based
// on location of existing jobs, travel time, service time and the time slot start and end
openSlots.push({
slot: d.data.name,
openSpots: maxPerSlot - validChildren.length,
path: d
.ancestors()
.map((ancestor) => ancestor.data.name)
.reverse()
.join(" > ")
});
}
} else if (d.depth === 2 && !d.children) {
// If the slot has no children, it has 'maxPerSlot' open spots and
// we'll assume that the slot is feasible for adding one job
openSlots.push({
slot: d.data.name,
openSpots: parseInt(panel1.maxPerSlot),
path: d
.ancestors()
.map((ancestor) => ancestor.data.name)
.reverse()
.join(" > ")
});
}
});

return openSlots;
}
Insert cell
function generateTimeWindows(numSlots, planningDay) {
const options = { month: "long", day: "numeric" };
const startHour = 8; // Workday starts at 08:00 EDT
const endHour = 20; // Workday ends at 18:00 EDT
const slotDuration = ((endHour - startHour) * 3600) / numSlots; // Duration of each slot in seconds

const timeWindows = [];

for (let i = 0; i < numSlots; i++) {
const startTime =
new Date(
`${planningDay}T${String(
startHour + (i * (endHour - startHour)) / numSlots
).padStart(2, "0")}:00:00-04:00`
).getTime() / 1000;
const endTime = startTime + slotDuration - 1;

const slotName = `${String(
startHour + (i * (endHour - startHour)) / numSlots
).padStart(2, "0")}-${String(
startHour + ((i + 1) * (endHour - startHour)) / numSlots
).padStart(2, "0")}`;

timeWindows.push({
name: slotName,
start: startTime,
end: endTime,
max: 6,
children: []
});
}

return timeWindows;
}
Insert cell
function recalculateSchedule() {
const options = { month: "long", day: "numeric" };
const timeWindows = generateTimeWindows(
panel1.serviceWindowDivisions.value,
panel1.shiftStart.toISOString().split("T")[0]
);

mutable baseSchedule = {
name: panel1.shiftStart.toISOString().split("T")[0],
children: []
};
baseRoutes.result.routes.forEach((r) => {
let v = {
name: `${r.vehicle}`,
children: JSON.parse(JSON.stringify(timeWindows))
};
r.steps.forEach((s) => {
let j = {
name: s.metadata
? `${formatTimestampToHHMM(s.arrival)} ${
s.metadata.name.split(" ")[1]
} ${s.metadata.address.split(",")[0]}`
: `${formatTimestampToHHMM(s.arrival)} ${s.type}`,
location: `${s.location[0]},${s.location[1]}`,
arrival: s.arrival,
new: false
};
let slot = v.children.find(
(timeSlot) => s.arrival >= timeSlot.start && s.arrival <= timeSlot.end
);
if (slot) {
slot.children.push(j);
}
});

mutable baseSchedule.children.push(v);
});
return d3.hierarchy(mutable baseSchedule);
}
Insert cell
mutable baseSchedule = null
Insert cell
import { nextbillion, secret2 } from "@nbai/nextbillion-ai-dependencies"
Insert cell
showGeofences = function (nbmap) {
let firstSymbolId;
let geofencesjson = {
type: "FeatureCollection",
features: []
};
const layers = nbmap.map.getStyle().layers;
for (const layer of layers) {
firstSymbolId = layer.id;
}

geofenceData.data.list.forEach((fence) => {
let feature = {
type: "Feature",
properties: { name: fence.name.toUpperCase() },
geometry: fence.geojson
};
geofencesjson.features.push(feature);
});
if (!nbmap.map.getSource("geofences")) {
nbmap.map.addSource("geofences", {
type: "geojson",
data: null
});
}
console.log(geofencesjson);
nbmap.map.getSource("geofences").setData(geofencesjson);
if (nbmap.map.getLayer("geofences")) {
nbmap.map.removeLayer("geofences");
}
nbmap.map.addLayer({
id: "geofences",
type: "fill",
source: "geofences",
paint: {
"fill-color": "#CC3366",
"fill-opacity": 0.2
}
});
nbmap.map.addLayer(
{
id: "geofence-label",
type: "symbol", // Use "symbol" for labeling
source: "geofences",
layout: {
"text-field": ["get", "name"],
"text-size": 12,
"text-offset": [0, 1],
"text-anchor": "center"
},
paint: {
"text-halo-width": 5,
"text-color": "#000", // Label text color
"text-halo-color": "#FFF"
}
},
firstSymbolId
);

}
Insert cell
serviceHost = "api.nextbillion.io"
//serviceHost = "stg-gcp-sgp.nextbillion.io"
Insert cell
apiKey = "73d4bee8352b46e483d75fb924889ada"
Insert cell
{
nextbillion.setApiKey(apiKey);
return html`<b>Set API key</b> <br><code> nextbillion.setApiKey(${apiKey.substr(
0,
4
)}....)</code>`;
}
Insert cell
axios = {
let axios = await require("axios");
return axios;
}
Insert cell
turf = {
let turf = await require("https://unpkg.com/@turf/turf@6/turf.min.js");
return turf;
}
Insert cell
async function mvrp(
data,
options = {
objective: {
minimise_num_depots: false,
travel_cost: "distance"
}
}
) {
let url = "";

url = `https://api.nextbillion.io/optimization/v2`;

const optimizeRequest = await axios
.post(`${url}` + `?key=${apiKey}`, {
...data,
options
})
.then((res) => res.data);

const mvrpRequestId = optimizeRequest.id;
let attempts = 0;

while (attempts < 50) {
const result = await axios({
url: `${url}/result?id=${mvrpRequestId}&key=${apiKey}`,
method: "GET",
headers: {}
});

if (
result.data &&
result.data.status === "Ok" &&
result.data.message == ""
) {
result.data.id = mvrpRequestId;

// KPIs
let deliveryCount = 0;
result.data.result.routes.forEach((rte, rteidx) => {
rte.steps.forEach((step, index) => {
if (step.id && step.id != 99) {
++deliveryCount;
}
});
});
result.data.kpis = {
totalDistance: `${(result.data.result.summary.distance / 1000).toFixed(
1
)} km`,
totalTime: `${(result.data.result.summary.duration / 60).toFixed(
1
)} mn`,
nbrRoutes: result.data.result.summary.routes,
avgStops: (deliveryCount / result.data.result.summary.routes).toFixed(
1
),
unassigned: result.data.result.summary.unassigned,
frontSeatWeight: result.data.description.split("|")[1]
};
return result.data;
}
attempts++;

await new Promise((resolve) => setTimeout(resolve, 2 * 1000));
}
}
Insert cell
// Function to determine if a point is inside any geofence
function isPointInAnyGeofence(pointCoordinates, geofenceData) {
const pt = turf.point(pointCoordinates);

for (const geofence of geofenceData.data.list) {
if (geofence.type === "polygon" && geofence.geojson.type === "Polygon") {
const poly = turf.polygon(geofence.geojson.coordinates);

if (turf.booleanPointInPolygon(pt, poly)) {
return geofence;
}
}
}
return false;
}
Insert cell
nbaiGeocodePlugin = require("https://d12qcqjlhp2ahm.cloudfront.net/mbac_test.js")
Insert cell
async function externalGeocoder(input, _features, config) {
const { features } = await nbaiDiscoverGeocode({
query: input,
proximity: config.proximity,
nbaiKey: config.nbaiKey
});
return features;
}
Insert cell
async function nbaiDiscoverGeocode(config) {
let countryList = [
"GBR",
"AND",
"USA",
"DEU",
"CAN",
"MEX",
"IND",
"FRA",
"SGP",
"GBR",
"ZAF",
"SWE",
"FIN",
"NOR",
"DNK",
"ESP",
"POR",
"BEL",
"CHE",
"ARE",
"SAU",
"BRA",
"ARG",
"AUS",
"AUT",
"EST",
"IDN",
"IRL",
"ISR",
"ITA",
"JPN"
];
const response = await fetch(
`https://api.nextbillion.io/h/discover?q=${
config.query
}&limit=5&in=countryCode:${countryList.toString()}&lang=en&at=36.195,-86.8&key=${apiKey}`
);

let data = await response.json();
let collection = {
type: "FeatureCollection",
features: []
};

data.items.forEach((itm) => {
let ft = {
type: "Feature",
geometry: {
type: "Point",
coordinates: [itm.position.lng, itm.position.lat]
},
properties: {
address: itm.address.label,
full_result: itm
},
place_name: itm.address.label,
result_type: itm.resultType,
place_type: itm.categories ? itm.categories[0].name : "N/A",
center: [itm.position.lng, itm.position.lat]
};
collection.features.push(ft);
});

return collection;
}
Insert cell
function formatTimestampToHHMM(timestamp) {
const date = new Date(timestamp * 1000); // Multiply by 1000 to convert to milliseconds

// Format the date to always use Eastern Time (EDT) and extract the hours and minutes
const options = {
timeZone: "America/New_York",
hour: "2-digit",
minute: "2-digit",
hour12: false // Use 24-hour format
};

const formattedTime = date.toLocaleString("en-US", options);
return formattedTime;
}
Insert cell
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css">
Insert cell
<link href="https://maps-gl.nextbillion.io/maps/v2/api/css" rel="stylesheet" />
Insert cell
<link
rel="stylesheet"
href="https://d12qcqjlhp2ahm.cloudfront.net/mbac_test.css"
type="text/css"
/>
Insert cell
<style>
svg {
background-color: #f0f0f0;
}
.nbai-ctrl-geocoder {
position: relative;
display: flex;
align-items: center;
width: 100%; /* Ensure the container takes full width if necessary */
}

.nbai-ctrl-geocoder--input {
flex: 1; /* Allow the input to grow and fill the available space */
width: 100%; /* Ensure input takes the full width of the flex container */
box-sizing: border-box; /* Include padding and border in the element's total width and height */
padding: 6px 45px;
border: 1px solid #ccc; /* Add a border to match your design */
border-radius: 4px; /* Optional: Add rounded corners */
}
</style>
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more