Skip to content

Instantly share code, notes, and snippets.

@kuanb
Last active August 18, 2021 05:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kuanb/c5cfd0000cec44f05a037a5cce8397db to your computer and use it in GitHub Desktop.
Save kuanb/c5cfd0000cec44f05a037a5cce8397db to your computer and use it in GitHub Desktop.
Playing w/ traffic simulation
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Traffic Simulation</title>
</head>
<body>
<h3>Controls</h3>
<p>
<p>Vehicle Generation State</p>
<input type="radio" id="removeCars" name="carAddRemoveRadio" value="-1">
<label for="removeCars">Remove cars</label><br>
<input type="radio" id="doNothing" name="carAddRemoveRadio" value="0" checked>
<label for="doNothing">Do nothing</label><br>
<input type="radio" id="addCars" name="carAddRemoveRadio" value="1">
<label for="addCars">Add cars</label>
<p>
</p>
Speed Multiplier
<input type="range" min="1" max="10" value="2" class="slider" id="vehicleSpeedsMultiplier">
</p>
</p>
<input type="checkbox" id="loopOnOff" name="loopOnOff" onclick="toggleLooping(this)" checked>
<label for="loopOnOff"> Vehicles Loop Route</label>
</p>
</p>
<button id="triggerBreakDown" onclick="triggerBreakDown()">Trigger Break Down</button>
<button id="unblockBreakDowns" onclick="unblockBreakDowns()">Unblock Break Downs</button>
<button id="stopRunCycle" onclick="clearInterval(runCycle)">Stop Run Cycle</button>
</p>
<h3>Stats</h3>
<p>Total Vehicle Count: <span id="totalVehicleCount"></span></p>
<p>Avg Speed: <span id="avgSpeedStatRendered"></span></p>
</body>
<script src="https://d3js.org/d3.v6.min.js"></script>
<script type="text/javascript">
// reference sentinel values
const speedRange = 1.5;
const speedLimit = 4;
const laneCount = 5;
let VEH_ID_INCREMENTER = 0;
const TRIGGER_BREAKDOWN_FLAG = {on: false, row: null};
const IS_LOOPING_FLAG = {on: true}
const TOOBIG = 9999;
const DomWidth = window.innerWidth * 0.9;
const DomHeight = 100;
const DomSVG = d3.select("body").append("svg").attr("width", DomWidth).attr("height", DomHeight).attr("style", "border:1px solid black");
function getUserSpeedMultiplier () {
return Number(document.getElementById("vehicleSpeedsMultiplier").value);
}
function triggerBreakDown () {
TRIGGER_BREAKDOWN_FLAG.on = true;
TRIGGER_BREAKDOWN_FLAG.row = Math.floor(Math.random() * laneCount);
}
function resetBreakDownFlag () {
TRIGGER_BREAKDOWN_FLAG.on = false;
TRIGGER_BREAKDOWN_FLAG.row = null;
TRIGGER_BREAKDOWN_FLAG.inResetState = false;
}
function unblockBreakDowns () {
for (let acali = 0; acali < laneCount; acali++) {
allCarsAllLanes[acali].forEach(ea => {
ea.isBrokenDown = false;
})
}
}
function toggleLooping (toggleCheckbox) {
IS_LOOPING_FLAG.on = toggleCheckbox.checked;
}
function getAddOrRemoveLikelihood () {
let returnVal = 0;
document.getElementsByName("carAddRemoveRadio").forEach(function (ea) {
if (ea.checked) {
returnVal = Number(ea.value);
}
})
return returnVal;
}
class Vehicle {
constructor(laneNumber, vehId) {
let requiredSpacingComfortFactor = Math.random();
// create a unique id for tracking and increment global counter
this.id = vehId;
this.lane = laneNumber;
this.distance = 0;
this.currentSpeed = 0;
this.acceleration = 0.2 + Math.random();
this.braking = (1 + Math.random()) * -1;
this.hardBraking = 2 * this.braking;
this.vehicleLength = 20 + (Math.random() * 10);
this.vehicleWidth = 10;
this.requiredFrontSpacing = 5 + (requiredSpacingComfortFactor * 55);
this.color = d3.interpolateCubehelixDefault(requiredSpacingComfortFactor);
this.isPolite = requiredSpacingComfortFactor > 0.5;
this.isNervous = this.isPolite && (Math.random() < 0.95);
this.isAggressive = !this.isPolite && (Math.random() < 0.95);
this.willPaceCarInFront = this.isPolite && !this.isNervous && (Math.random() < 0.95);
this.isBrokenDown = false;
this.isMerging = {state: false, phase: 0, destLane: null, mergeBehindCarId: null};
// speed based on personality of driver, in part
if (this.isNervous) {
this.topSpeed = speedLimit + ((Math.random() * 0.25) * speedRange) - speedRange/2;
} else if (this.isAggressive) {
this.topSpeed = speedLimit + ((Math.random() * 0.5 + 0.5) * speedRange) - speedRange/2;
} else {
this.topSpeed = speedLimit + ((Math.random() * 0.5 + 0.25) * speedRange) - speedRange/2;
}
}
minOffsetFromDistance () {
return this.vehicleLength + this.requiredFrontSpacing;
}
maximumComfortableDistanceInFront () {
return this.distance + this.currentSpeed + this.minOffsetFromDistance();
}
speedIfAccelerateFully () {
return Math.min((this.currentSpeed + this.acceleration), this.topSpeed);
}
speedIfDecelerating () {
return Math.max((this.currentSpeed + this.braking), 0);
}
speedIfHardBraking () {
return Math.max((this.currentSpeed + this.hardBraking), 0);
}
updateDistanceAfterCurrentSpeedUpdated () {
if (!this.isBrokenDown) {
this.distance = this.distance + (this.currentSpeed * getUserSpeedMultiplier());
}
}
accelerateForwardMax () {
this.currentSpeed = this.speedIfAccelerateFully();
}
decelerateForwards () {
this.currentSpeed = this.speedIfDecelerating();
}
hardBrakingForwards () {
this.currentSpeed = this.speedIfHardBraking();
}
setCustomSpeed (customSpeed) {
this.currentSpeed = customSpeed;
}
fullStop () {
this.currentSpeed = 0;
}
breakDown () {
this.isBrokenDown = true;
this.currentSpeed = 0;
}
resetMergingState () {
this.lane = this.isMerging.destLane;
this.isMerging = {state: false, phase: 0, destLane: null, mergeBehindCarId: null};
this.domElement.style("fill", this.color).style("opacity", 0.5);
}
estimatePotentialOffsetDistance (customSpeedInput) {
return this.distance + this.minOffsetFromDistance() + (customSpeedInput * getUserSpeedMultiplier());
}
getMergeSpaceNeeded () {
// determine how much of a squeeze is ok for merges
let howMuchSpaceNeededForMerge = {front: 0.5, back: 1.5};
if (this.isAggressive) {
howMuchSpaceNeededForMerge = {front: 0.25, back: 0.75};
}
howMuchSpaceNeededForMerge.distanceBack = this.distance - (howMuchSpaceNeededForMerge.back * this.minOffsetFromDistance());
howMuchSpaceNeededForMerge.distanceFront = this.distance + (howMuchSpaceNeededForMerge.front * this.minOffsetFromDistance());
return howMuchSpaceNeededForMerge;
}
considerMerging(carInFront, leftLaneNumber, leftLaneCars, rightLaneNumber, rightLaneCars) {
let oddsOfMerging = Math.random();
if (this.isAggressive) {
oddsOfMerging += 0.2;
} else if (this.isNervous) {
oddsOfMerging -= 0.5;
} else if (this.isPolite){
oddsOfMerging -= 0.1;
}
let shouldMerge = false;
if (!carInFront) {
// no need to merge if already in front
shouldMerge = false;
} else if (carInFront.distance < this.estimatePotentialOffsetDistance(this.speedIfAccelerateFully()) * 2) {
if (carInFront.currentSpeed < (this.currentSpeed * 0.9)) {
shouldMerge = true;
}
} else if (carInFront.distance < this.estimatePotentialOffsetDistance(this.speedIfAccelerateFully()) * 5) {
if (carInFront.currentSpeed < (this.currentSpeed * 0.8)) {
shouldMerge = true;
}
}
// do not merge off screen
if (this.distance < (DomWidth * 0.15)) {
shouldMerge = false;
} else if (this.distance < (DomWidth > 0.85)) {
shouldMerge = false;
}
// even if you can maybe you won't
shouldMerge = (shouldMerge && oddsOfMerging > 0.975);
const howMuchSpaceNeededForMerge = this.getMergeSpaceNeeded();
// if you can need to pick which lane
let mergeToLane = null;
let nextCarInMergeLane = null;
if (shouldMerge && leftLaneCars) {
let leftSub = leftLaneCars
.filter(ea => ea.distance > howMuchSpaceNeededForMerge.distanceBack)
.filter(ea => ea.distance < howMuchSpaceNeededForMerge.distanceFront);
if (leftSub.length) {
mergeToLane = leftLaneNumber;
let upcomingLeftCars = leftLaneCars.filter(ea => ea.distance >= howMuchSpaceNeededForMerge.distanceFront);
if (upcomingLeftCars.length) {
nextCarInMergeLane = upcomingLeftCars[0].id
}
}
}
if (shouldMerge && rightLaneCars) {
let rightSub = rightLaneCars
.filter(ea => ea.distance > howMuchSpaceNeededForMerge.distanceBack)
.filter(ea => ea.distance < howMuchSpaceNeededForMerge.distanceFront);
if (rightSub.length) {
let didUseRight = false;
if ((mergeToLane != null) && (Math.random() > 0.7)) {
mergeToLane = rightLaneNumber;
didUseRight = true;
} else {
mergeToLane = rightLaneNumber;
didUseRight = true;
}
if (didUseRight) {
let upcomingRightCars = rightLaneCars.filter(ea => ea.distance >= howMuchSpaceNeededForMerge.distanceFront);
if (upcomingRightCars.length) {
nextCarInMergeLane = upcomingRightCars[0].id
}
}
}
}
if ((this.isMerging || shouldMerge) && (mergeToLane != null)) {
if (!this.isMerging.state) {
this.isMerging.state = true;
this.isMerging.mergeBehindCarId = nextCarInMergeLane;
this.isMerging.destLane = mergeToLane;
this.isMerging.phase = 1;
this.domElement.style("fill", "red").style("opacity", 0.85);
}
}
return this.isMerging.state;
}
updateVehiclePosition (carInFront, carInFrontInMergeLane) {
if (!carInFront) {
this.accelerateForwardMax();
} else if (this.isBrokenDown) {
// pass no action if broken down
} else {
// handle when there are cars that have looped
let adjustedDistanceAhead = carInFront.distance;
if (carInFrontInMergeLane && carInFrontInMergeLane.distance < adjustedDistanceAhead) {
adjustedDistanceAhead = carInFrontInMergeLane.distance;
}
if (IS_LOOPING_FLAG.on && (adjustedDistanceAhead <= this.distance)) {
adjustedDistanceAhead += DomWidth;
}
if (this.estimatePotentialOffsetDistance(this.speedIfAccelerateFully()) < adjustedDistanceAhead) {
this.accelerateForwardMax();
} else if (this.estimatePotentialOffsetDistance(this.currentSpeed) < adjustedDistanceAhead) {
if (!this.isPolite) {
this.accelerateForwardMax();
} else if (this.isNervous) {
this.decelerateForwards();
} else if (this.willPaceCarInFront) {
if (carInFrontInMergeLane && carInFrontInMergeLane.currentSpeed < carInFront.currentSpeed) {
this.setCustomSpeed(carInFrontInMergeLane.currentSpeed);
} else {
this.setCustomSpeed(carInFront.currentSpeed);
}
}
// do nothing since no need to update speed if being polite
} else if (this.estimatePotentialOffsetDistance(this.speedIfDecelerating()) < adjustedDistanceAhead) {
if (!this.isPolite) {
this.decelerateForwards();
} else if (this.isNervous) {
this.hardBrakingForwards();
} else {
// try and move forward at current speed
}
} else if (this.estimatePotentialOffsetDistance(this.speedIfHardBraking()) < adjustedDistanceAhead) {
this.hardBrakingForwards();
} else {
this.fullStop();
}
}
this.updateDistanceAfterCurrentSpeedUpdated();
}
redrawCarLocation () {
this.domElement.attr("x", this.distance);
// only keep merging if moving forward (no sideways sliding)
if (this.isMerging.state && this.currentSpeed > 0) {
const shiftAmount = 15 * ((this.isMerging.destLane - this.lane) / 10) * this.isMerging.phase;
const adjY = shiftAmount + 15 + 15 * this.lane;
this.domElement.attr("y", adjY);
}
}
addCarToDom () {
this.domElement = DomSVG
.append("rect")
.style("fill", this.color)
.attr("x", this.distance)
.attr("y", (15 + 15 * this.lane))
.attr("opacity", 0.5)
.attr("height", this.vehicleWidth)
.attr("width", this.vehicleLength);
}
deleteFromDom () {
this.domElement.remove();
}
}
function getCarById (targetId) {
for (let acali = 0; acali < laneCount; acali++) {
let allCars = allCarsAllLanes[acali];
allCars.forEach(currCar => {
if (currCar.id == targetId) {
return currCar;
}
});
}
}
let allCarsAllLanes = [];
for (let i = 0; i < laneCount; i++) {
allCarsAllLanes.push([])
}
const runCycle = setInterval(function() {
const allAverageSpeeds = [];
// merge phase - make merge changes
for (let acali = 0; acali < laneCount; acali++) {
let allCars = allCarsAllLanes[acali];
// first thing is to prune out old merge operations
allCarsAllLanes[acali] = allCarsAllLanes[acali].filter(currCar => {
// handle orphaned already merged cars
if ((currCar.lane != acali) && !currCar.isMerging.state) {
return false;
}
return true;
}).map(currCar => {
// also increment phase of active mergers
if (currCar.isMerging.state && currCar.isMerging.destLane == acali) {
currCar.isMerging.phase = currCar.isMerging.phase + 1;
}
if (currCar.isMerging.state && currCar.isMerging.phase > 10) {
// reset merging state if destination lane has been reached
currCar.resetMergingState();
}
return currCar;
});
for (var i = allCars.length - 1; i >= 0; i--) {
let currCar = allCars[i];
let nextCar = null;
if (i == allCars.length - 1) {
// this is the rightmost car
nextCar = null;
} else {
nextCar = allCars[i+1];
}
let leftLaneNumber = null;
let leftLane = null;
let rightLaneNumber = null;
let rightLane = null;
if (acali > 0) {
leftLaneNumber = acali - 1;
leftLane = allCarsAllLanes[leftLaneNumber];
}
if (acali < allCarsAllLanes.length - 1) {
rightLaneNumber = acali + 1;
rightLane = allCarsAllLanes[rightLaneNumber];
}
// consider merging given current conditions for each car
if (!currCar.isMerging.state && currCar.considerMerging(nextCar, leftLaneNumber, leftLane, rightLaneNumber, rightLane)) {
let a = allCarsAllLanes[currCar.isMerging.destLane].filter(ea => ea.distance < currCar.distance);
let b = allCarsAllLanes[currCar.isMerging.destLane].filter(ea => ea.distance > currCar.distance);
allCarsAllLanes[currCar.isMerging.destLane] = a.concat([currCar]).concat(b);
}
}
}
for (let acali = 0; acali < laneCount; acali++) {
let allCars = allCarsAllLanes[acali];
// there must always be at leat 1 car on the road
if (allCars.length == 0) {
const newVehicleToAdd = new Vehicle(acali, VEH_ID_INCREMENTER);
newVehicleToAdd.addCarToDom();
VEH_ID_INCREMENTER += 1;
allCars = [newVehicleToAdd];
}
let newCarsList = allCars.filter(ea => ea.distance < DomWidth);
let carsThatWentOffScreen = allCars.filter(ea => ea.distance >= DomWidth)
if (IS_LOOPING_FLAG.on) {
carsThatWentOffScreen = carsThatWentOffScreen.map(ea => {
ea.distance = Math.min(0, allCars[0].distance - (ea.minOffsetFromDistance()));
return ea;
});
if (carsThatWentOffScreen.length) {
newCarsList = carsThatWentOffScreen.concat(newCarsList);
}
} else {
carsThatWentOffScreen.forEach(ea => {
ea.deleteFromDom();
});
}
for (var i = newCarsList.length - 1; i >= 0; i--) {
let currCar = newCarsList[i];
let nextCarInMergeLane = null;
if (currCar.isMerging.state) {
nextCarInMergeLane = getCarById(currCar.isMerging.mergeBehindCarId);
}
// avoid drawing when we are a merge element and not already in lane
if (currCar.lane == acali) {
if (TRIGGER_BREAKDOWN_FLAG.on && TRIGGER_BREAKDOWN_FLAG.row == acali) {
currCar.breakDown();
resetBreakDownFlag()
}
if (i == newCarsList.length - 1) {
// if the rightmost car, then reference the leftmost (if loop)
if (IS_LOOPING_FLAG.on) {
currCar.updateVehiclePosition(newCarsList[0], nextCarInMergeLane);
} else {
currCar.updateVehiclePosition(null, nextCarInMergeLane);
}
} else if (newCarsList.length <= 1) {
currCar.updateVehiclePosition(null, nextCarInMergeLane);
} else {
const carInFront = newCarsList[i + 1];
currCar.updateVehiclePosition(carInFront, nextCarInMergeLane);
}
// update render location
currCar.redrawCarLocation();
}
}
const newPotentialCar = new Vehicle(acali, VEH_ID_INCREMENTER);
const lastCarInLine = newCarsList[0];
const likelihoodForAddingCarsOrRemoving = getAddOrRemoveLikelihood();
if (likelihoodForAddingCarsOrRemoving > 0 && (Math.random() > 0.5)) {
if (lastCarInLine.distance > newPotentialCar.minOffsetFromDistance()) {
newPotentialCar.addCarToDom();
VEH_ID_INCREMENTER += 1;
newCarsList = [newPotentialCar].concat(newCarsList);
}
} else if (likelihoodForAddingCarsOrRemoving < 0 && (Math.random() > 0.5)) {
const removeTheseCars = newCarsList.splice(-1, 1);
removeTheseCars.forEach(ea => {
ea.deleteFromDom();
})
}
allAverageSpeeds.extend(newCarsList.map(ea => ea.currentSpeed));
allCarsAllLanes[acali] = newCarsList;
}
let averageCurrentSpeed = 0;
if (allAverageSpeeds.length) {
averageCurrentSpeed = allAverageSpeeds.reduce((a,b) => (a + b))/allAverageSpeeds.length;
}
document.getElementById("avgSpeedStatRendered").innerText = averageCurrentSpeed.toFixed(2);
document.getElementById("totalVehicleCount").innerText = allAverageSpeeds.length + " of " + VEH_ID_INCREMENTER + " total created";
}, 50)
// utility function/s
Array.prototype.extend = function (other_array) {
/* You should include a test to check whether other_array really is an array */
other_array.forEach(function(v) {this.push(v)}, this);
}
</script>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment