From 1fdc4bb27fdea607fae6bc74208eceb1fc9fcbc2 Mon Sep 17 00:00:00 2001
From: jslightham <31053827+jslightham@users.noreply.github.com>
Date: Mon, 25 May 2020 12:43:28 -0400
Subject: [PATCH] mostly finished
---
index.html | 169 ++++++++++++++
main.js | 625 ++++++++++++++++++++++++++++++++++++++++++++++++++++
shot.mp3 | Bin 0 -> 22040 bytes
wallhit.mp3 | Bin 0 -> 1896 bytes
4 files changed, 794 insertions(+)
create mode 100644 index.html
create mode 100644 main.js
create mode 100644 shot.mp3
create mode 100644 wallhit.mp3
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..2715fa5
--- /dev/null
+++ b/index.html
@@ -0,0 +1,169 @@
+
+
+
+
+
+
+
+ Collision Simulation
+
+
+
+
+
+ Collision Simulator
+ Project by Johnathon Slightham
+
+
+
+
+
+
+
+
+
Modifiers
+ Coefficient of Kinetic Friction:
+
+ Collision Effectiveness (100% - No energy lost):
%
+ Draw Velocity Vector:
+
Placed Particles
+
+
+ | ID # |
+ Mass |
+ X Velocity |
+ Y Velocity |
+ X Acceleration |
+ Y Acceleration |
+
+
+ Total Kinetic Energy:
J
+
Add Particles
+
+
Modify Graph
+ Graph X Axis:
+
+
+ Graph Y Axis:
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/main.js b/main.js
new file mode 100644
index 0000000..0115d93
--- /dev/null
+++ b/main.js
@@ -0,0 +1,625 @@
+class Particle{
+ constructor(id, x, y, vx, vy, m){
+ this.id = id;
+ this.x = x;
+ this.y = y;
+ this.vx = vx;
+ this.vy = vy;
+ this.m = m;
+ this.r = getRadius(m);
+ this.v = Math.sqrt(Math.pow(this.vx, 2) + Math.pow(this.vy, 2));
+ this.ek = [];
+ this.ek.push([0, 0]);
+ this.ax = 0
+ this.ay = 0;
+ }
+}
+
+// Functions to display or hide the welcome screen
+function on() {
+ document.getElementById("playButton").style.display = "block";
+}
+
+function off() {
+ document.getElementById("playButton").style.display = "none";
+}
+
+var particles = [];
+var ctx;
+var canvas;
+var table;
+var a = 0;
+var sa;
+var ek = [];
+var eff = 0.85;
+var shot = new Audio("shot.mp3");
+var wallhit = new Audio("wallhit.mp3");
+var selector = document.getElementById('graph_type');
+var sfr = 0;
+var fr = 0;
+var drawVelocityVector = false;
+var measurementData = [];
+var chartX = "measurement";
+var chartY = "ekt"
+measurementData.push('Measurement Number');
+measurementData.push(0);
+
+ek.push('Kientic Energy (J)');
+ek.push(0);
+
+var measurement = 0;
+
+// Load the google charts API
+google.charts.load('current', {'packages':['corechart']});
+
+function main(elem){
+ off();
+ canvas = document.getElementById(elem);
+ ctx = canvas.getContext("2d");
+ table = document.getElementById("particles");
+
+ fr = document.getElementById("fr").value;
+ //sfr = document.getElementById("sfr").value;
+
+ drawVelocityVector = document.getElementById("drawvelocity").checked;
+
+ reloadDropdowns();
+
+ // Set a callback to run when the charts API is loaded
+ google.charts.setOnLoadCallback(drawKEChart);
+
+ //canvas.addEventListener("mousedown", doMouseDown, false);
+ //canvas.addEventListener("onmouseup", doMouseUp, false);
+
+ // Loop through each particle every 10 milliseconds, create measurements every 1 second
+ setInterval(loop, 10);
+ setInterval(measureData, 1000);
+}
+
+// The main loop function that handles each particle
+function loop(){
+
+ // Set volumes of sounds according to % energy transferred
+ eff = document.getElementById("effe").value/100;
+ shot.volume = 1 - eff;
+ wallhit.volume = 1 - eff;
+
+ // Acceleration = 9.8*uk (derived from F=ma, -uk*g*m=m*a)
+ a = -(9.8)*fr;
+ sa = -(9.8)*sfr;
+
+ chartX = document.getElementById("graphx").value;
+ chartY = document.getElementById("graphy").value;
+ drawVelocityVector = document.getElementById("drawvelocity").checked;
+
+ // Write the current kinetic energy, then reset to 0 for recalculation
+ document.getElementById("ek").innerHTML = ek[ek.length-1];
+
+ // Draw board, then process collisions.
+ draw();
+ separate();
+ detectCollisions();
+
+ // Iterate through each particle, to change it's velocity from acceleration, and position from velocity
+ particles.forEach((elem, i) => {
+
+ // Determine velocity vector and store for later use
+ elem.v = Math.sqrt(Math.pow(elem.vx, 2) + Math.pow(elem.vy, 2));
+
+ // Check for collisions with wall, and on collision set to - velocity
+ if(elem.x + elem.vx > canvas.width - elem.r || elem.x + elem.vx < elem.r){
+ elem.vx = -eff*elem.vx;
+ let wallHitAudio = wallhit.cloneNode();
+ wallHitAudio.volume = 1 - eff;
+ wallHitAudio.play()
+
+ // Fix bug of balls getting stuck in walls, if a ball is in a wall move it out
+ if(elem.x + elem.vx > canvas.width - elem.r){
+ elem.x = canvas.width - elem.r;
+ }
+
+ if(elem.x + elem.vx < elem.r){
+ elem.x = elem.r;
+ }
+
+ }
+ if(elem.y + elem.vy > canvas.height - elem.r || elem.y + elem.vy < elem.r){
+ elem.vy = -eff*elem.vy;
+ let wallHitAudio = wallhit.cloneNode();
+ wallHitAudio.volume = 1 - eff;
+ wallHitAudio.play()
+
+ // Fix bug of balls getting stuck in walls, if a ball is in a wall move it out
+ if(elem.y + elem.vy < elem.r){
+ elem.y = elem.r;
+ }
+ if(elem.y + elem.vy > canvas.width - elem.r){
+ elem.y = canvas.height - elem.r;
+ }
+ }
+
+ // Calculate Theta, and acceleration values based on the direction of the velocity vector
+ elem.theta = Math.atan2(elem.vx,elem.vy);
+ elem.sax = sa*Math.abs(Math.sin(elem.theta));
+ elem.say = sa*Math.abs(Math.cos(elem.theta));
+
+ elem.ax = a*Math.abs(Math.sin(elem.theta));
+ elem.ay = a*Math.abs(Math.cos(elem.theta));
+
+ // If the particle has enough velocity to be decelerated, decelerate it
+ if(!Math.abs(elem.vx) < Math.abs(elem.ax) || !Math.abs(elem.vy) < Math.abs(elem.ay)){
+ // Add acceleration to velocity each iteration
+ if(elem.vx > 0){
+ elem.vx += elem.ax;
+ }
+ else{
+ elem.vx -= elem.ax;
+ }
+
+ // Add acceleration to velocity each iteration
+ if(elem.vy > 0){
+ elem.vy += elem.ay;
+ }
+ else{
+ elem.vy -= elem.ay;
+ }
+
+ }else{
+ elem.ax = 0;
+ elem.ay = 0;
+
+ }
+ // Set velocity to zero if the acceleration is larger than it
+ if(Math.abs(elem.ay) > Math.abs(elem.vy) || Math.abs(elem.ax) > Math.abs(elem.vx)){
+ elem.ax = 0;
+ elem.ay = 0;
+ elem.vx = 0;
+ elem.vy = 0;
+ }else{
+ if((elem.vx == 0 || elem.vy == 0) && (Math.abs(elem.say) > Math.abs(elem.vy) || Math.abs(elem.sax) > Math.abs(elem.vx))){
+ elem.ax = 0;
+ elem.ay = 0;
+ elem.vx = 0;
+ elem.vy = 0;
+ }else{
+ // Each iteration add the velocity to the position
+ elem.x += elem.vx;
+ elem.y += elem.vy;
+ }
+
+ }
+
+ // Display data in the table
+ table.rows[i+1].cells[0].innerHTML = elem.id;
+ table.rows[i+1].cells[1].innerHTML = elem.m + " kg";
+ table.rows[i+1].cells[2].innerHTML = Math.abs(elem.vx.toPrecision(2));
+ table.rows[i+1].cells[3].innerHTML = Math.abs(elem.vy.toPrecision(2));
+ table.rows[i+1].cells[4].innerHTML = elem.ax.toPrecision(2);
+ table.rows[i+1].cells[5].innerHTML = elem.ay.toPrecision(2);
+ })
+}
+
+// Function to draw all particles on the canvas, and draw velocity vectors
+function draw(){
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ particles.forEach((elem, i) =>
+ {
+ // Draw actual particle
+ ctx.beginPath();
+ ctx.fillStyle = "black";
+ ctx.strokeStyle = "black";
+ ctx.arc(elem.x, elem.y, elem.r, 0, 2*Math.PI);
+ ctx.fill();
+ ctx.stroke();
+
+ // Center mass in particle
+ ctx.font = elem.m*0.55 + 'px serif';
+ ctx.fillStyle = "white";
+ ctx.textAlign = "center";
+ ctx.fillText(elem.m+"kg", elem.x, elem.y + elem.r/4);
+
+ // Draw Velocity Vector, if this option is selected
+ if(drawVelocityVector){
+ ctx.beginPath();
+ ctx.strokeStyle = "red";
+ ctx.moveTo(elem.x, elem.y);
+ ctx.lineTo(elem.x + elem.v*Math.sin(elem.theta)* 10, elem.y + elem.v*Math.cos(elem.theta) * 10);
+ ctx.stroke();
+ }
+
+ });
+
+}
+
+// Function that returns the distance between the two particles given
+function getDistance(p1, p2){
+ let xDist = Math.abs(p1.x - p2.x);
+ let yDist = Math.abs(p1.y - p2.y);
+
+ return Math.sqrt(Math.pow(xDist, 2) + Math.pow(yDist, 2));
+}
+
+var collisions = [];
+
+// Function that will separate all particles on the canvas
+function separate(){
+ collisions = [];
+ let noOverlap = false;
+ while(!noOverlap){
+ noOverlap = true;
+ particles.forEach((elem1, i) => {
+ for(var j = i+1; j < particles.length; j++){
+ elem2 = particles[j];
+ // Make sure not comparing the same element
+ if(elem1 != elem2){
+ // If the particles are overlapping
+ if(getDistance(elem1, elem2) < elem1.r + elem2.r){
+ collisions.push({element1: elem1, element2: elem2});
+ console.log("overlap");
+
+ // Move the particle back to its previous position, only if doing so will not put it inside of a wall
+ if(!elem1.x - elem1.vx > canvas.width - elem1.r || !elem1.x - elem1.vx < elem1.r)
+ elem1.x -= elem1.vx;
+
+ if(!elem2.x - elem2.vx > canvas.width - elem2.r || !elem2.x - elem2.vx < elem2.r)
+ elem2.x -= elem2.vx;
+
+ if(!elem1.y - elem1.vy > canvas.height - elem1.r || !elem1.y - elem1.vy < elem1.r)
+ elem1.y -= elem1.vy;
+
+ if(!elem2.y - elem2.vy > canvas.height - elem2.r || !elem2.y - elem2.vy < elem2.r)
+ elem2.y -= elem2.vy;
+
+ noOverlap = false;
+
+ draw();
+ }
+ }
+ }
+ })
+ }
+}
+
+// Function that detects, and handles all collisions.
+function detectCollisions(){
+
+ collisions.forEach((collision) => {
+ let shotAudio = shot.cloneNode()
+ shotAudio.volume = 1 - eff;
+ shotAudio.play()
+ elem1 = collision.element1;
+ elem2 = collision.element2;
+
+ // Check if the particles being compared are the same, if they are don't run collision detection or particles will collide with themselves.
+ if(elem1 != elem2){
+ elem1.px = elem1.x;
+ elem1.py = elem1.y;
+ elem2.px = elem2.x;
+ elem2.py = elem2.y;
+
+ // Store previous velocities, this will be used later when detecting static friction
+ elem1.pvx = elem1.vx;
+ elem1.pvy = elem1.vy;
+ elem2.pvx = elem2.vx;
+ elem2.pvy = elem2.vy;
+
+ // If the particles are still overlapping, separate them again
+ if(getDistance(elem1, elem2) < elem1.r + elem2.r){
+ console.log("overlap");
+ elem1.x -= elem1.vx;
+ elem1.y -= elem1.vy;
+ elem2.x -= elem2.vx;
+ elem2.y -= elem2.vy;
+ }
+
+ // Theta = angle of velocity
+ let theta1 = Math.atan2(elem1.vx,elem1.vy);
+ let theta2 = Math.atan2(elem2.vx,elem2.vy);
+
+ // Phi = angle of collision
+ let phi = Math.atan2((elem2.px - elem1.px),(elem2.py - elem1.py));
+
+ // Find rotated x & y components, with x axis parallel to the contact normal vector
+ let v1xr = elem1.v*Math.cos(theta1 - phi);
+ let v1yr = elem1.v*Math.sin(theta1 - phi);
+ let v2xr = elem2.v*Math.cos(theta2 - phi);
+ let v2yr = elem2.v*Math.sin(theta2 - phi);
+
+ // Use the rotated components to solve a 1 dimensional collision.
+ let v1fxr = eff*(v1xr*(elem1.m - elem2.m) + 2*elem2.m*v2xr)/(elem1.m + elem2.m);
+ let v2fxr = eff*(v2xr*(elem2.m - elem1.m) + 2*elem1.m*v1xr)/(elem2.m + elem1.m);
+
+ elem1.vy = v1fxr*Math.cos(phi) + v1yr*Math.cos(phi + Math.PI/2);
+ elem1.vx = v1fxr*Math.sin(phi) + v1yr*Math.sin(phi + Math.PI/2);
+ elem2.vy = v2fxr*Math.cos(phi) + v2yr*Math.cos(phi + Math.PI/2);
+ elem2.vx = v2fxr*Math.sin(phi) + v2yr*Math.sin(phi + Math.PI/2);
+
+ // Calculate new theta, and static acceleration for static friction checking
+ elem1.theta = Math.atan2(elem1.vx,elem1.vy);
+ elem1.sax = sa*Math.abs(Math.sin(elem1.theta));
+ elem1.say = sa*Math.abs(Math.cos(elem1.theta));
+
+ elem2.theta = Math.atan2(elem2.vx,elem2.vy);
+ elem2.sax = sa*Math.abs(Math.sin(elem2.theta));
+ elem2.say = sa*Math.abs(Math.cos(elem2.theta));
+
+ if((elem2.sax > elem2.vx || elem2.say > elem2.vy) && (elem2.pvx == 0 || elem2.pvy == 0)){
+ //elem2.vx = 0;
+ //elem2.vy = 0;
+ }
+
+ if(elem1.sax > elem1.vx || elem1.say > elem1.vy && (elem1.pvx == 0 || elem1.pvy == 0)){
+ //elem1.vx = 0;
+ //elem1.vy = 0;
+ }
+
+ }
+ });
+}
+
+// Function called when the add particle button is submitted
+function addParticle(){
+ var form = document.getElementById("add");
+ var errMsg = "Errors: "; // String to store all errors with entered data
+ // Mass cannot be less than 1 error
+ if(form["mass"].value < 1){
+ errMsg += "Cannot Have Negative, or Zero Mass! "
+ }
+ // Mass is too large for the canvas size error
+ else if(getRadius(form["mass"].value)*2 + 10 > canvas.width || getRadius(form["mass"].value)*2 + 10 > canvas.height){
+ errMsg += "That mass is too large for this canvas! "
+ }else if(!form["xvel"].value || !form["yvel"].value || !form["mass"].value){
+ errMsg += "One or more values is not defined! "
+ }
+ // If no errors, add rows to the table and insert the particle into the array
+ else{
+
+
+ var xv = parseInt(form["xvel"].value)
+ var yv = parseInt(form["yvel"].value)
+
+ // Check if the acceleration is larger than the velocity, if so set velocity to 0 and not negative.
+
+ let n = 1;
+ if(particles.length > 0){
+ n = particles[particles.length-1].id +1;
+ }
+
+ // Find a position that is not ontop of another ball, or inside of a wall. This will randomize the position 9 times or until a successful location is discovered.
+ let validPos = false;
+ let numtries = 0;
+ let tx = 0; let ty = 0;
+ while(!validPos && numtries < 9){
+ numtries ++;
+ validPos = true;
+ tx = Math.round(Math.random()*((canvas.width - getRadius(parseInt(form["mass"].value))) - getRadius(parseInt(form["mass"].value)) - 5) + getRadius(parseInt(form["mass"].value)) + 5);
+ ty = Math.round(Math.random()*((canvas.height - getRadius(parseInt(form["mass"].value))) - getRadius(parseInt(form["mass"].value)) - 5) + getRadius(parseInt(form["mass"].value)) + 5);
+ let temp = new Particle(n, tx, ty, xv, yv, parseInt(form["mass"].value)+5);
+ particles.forEach(particle =>{
+ if(getDistance(particle, temp) < particle.r + temp.r){
+ validPos = false;
+ }
+ })
+ }
+ // if a valid position was found create it
+ if(validPos){
+ table.insertRow();
+ table.rows[table.rows.length -1].insertCell();
+ table.rows[table.rows.length -1].insertCell();
+ table.rows[table.rows.length -1].insertCell();
+ table.rows[table.rows.length -1].insertCell();
+ table.rows[table.rows.length -1].insertCell();
+ table.rows[table.rows.length -1].insertCell();
+ particles.push(new Particle(n, tx, ty, xv, yv, parseInt(form["mass"].value)));
+ }else{
+ errMsg += "Not enough room for that particle on the canvas! "
+ }
+
+ reloadDropdowns();
+ }
+ if(errMsg != "Errors: "){
+ alert(errMsg);
+ }
+}
+
+// Function that returns a radius, by multiplying the given mass by a constant
+function getRadius(mass){
+ return mass*0.8;
+}
+
+// Function to remove a particle from the array by id
+function remParticle(id){
+ alert("remove");
+ id = parseInt(id, 10);
+
+ particles = particles.filter(function(element, i){
+ eid = element.id;
+
+ if(id == eid){
+ table.deleteRow(i+1);
+ }
+ return id != eid;
+ });
+ console.log(particles);
+}
+
+// Function that adds the data at the current time to their respective array
+function measureData(){
+ measurement++;
+ let ekt = 0;
+ particles.forEach((elem, i) => {
+ ekt += 0.5*Math.pow(elem.v, 2)*elem.m;
+ // Ek=0.5mv^2, add to total
+ elem.ek.push(0.5*Math.pow(elem.v, 2)*elem.m);
+ measurementData.push(measurement);
+
+ })
+ ek.push(ekt.toPrecision(6));
+ if(measurement > 100){
+ measurement = 0;
+ measurementData = [];
+ measurementData.push('Time (s)');
+ measurementData.push(0);
+ ek = [];
+ ek.push('Kientic Energy (J)');
+ ek.push(0);
+
+ particles.forEach((elem, i) => {
+ elem.ek = [];
+ elem.ek.push('Kientic Energy (J)');
+ })
+ }
+ drawKEChart();
+}
+
+// Function to draw the graph based on the selected values
+function drawKEChart() {
+ //console.log(chartX);
+ let x = [];
+ if(chartX == "measurement"){
+ x = [...measurementData];
+ }else if(chartX == "ekt"){
+ x = [...ek];
+ }else{
+ x = [...particles[parseInt(chartX, 10)-1].ek];
+ }
+
+ let y = [];
+ if(chartY == "measurement"){
+ y = [...measurementData];
+ }else if(chartY == "ekt"){
+ y = [...ek];
+ }else{
+ y = [...particles[parseInt(chartY, 10)-1].ek];
+ }
+
+ var arr = [];
+ x.forEach((n, i) =>{
+ arr.push([n, y[i]]);
+ })
+
+ var data = google.visualization.arrayToDataTable(arr);
+
+ var options = {
+ title: 'Graph (Change X and Y Axis in the above form):',
+ legend: { position: 'bottom' }
+ };
+
+ var chart = new google.visualization.LineChart(document.getElementById('curve_chart'));
+
+ chart.draw(data, options);
+
+}
+
+// Function called when static friction is changed, to make sure a valid number is selected
+function sfrChange(){
+ let element = document.getElementById("sfr");
+ if(element.value >= 1){
+ alert("The coefficient of static friction cannot be more than 1 for a billiard ball!");
+ element.value = sfr;
+ }else if(element.value <= fr){
+ alert("The coefficient of kinetic friction cannot be more than the coefficient of static friction!");
+ element.value = sfr;
+ }
+ else{
+ sfr = document.getElementById("sfr").value;
+ }
+}
+
+// Function called when kinetic friction is changed, to make sure a valid number is selected
+function frChange(){
+ let element = document.getElementById("fr");
+ if(element.value >= 1){
+ alert("The coefficient of kinetic friction cannot be more than 1 for a billiard ball!");
+ element.value = fr;
+ }
+ //else if(element.value >= sfr){
+ // alert("The coefficient of kinetic friction cannot be more than the coefficient of static friction!");
+ // element.value = fr;
+ //}
+ else{
+ fr = element.value;
+ }
+}
+
+// Function called when % effectiveness is changed, to make sure a valid number is selected
+function effeChange(){
+ let element = document.getElementById("effe");
+ if(element.value > 100){
+ alert("There cannot be more than 100% efficient collisions! ");
+ element.value = eff*100;
+ }
+ else if(element.value < 0){
+ alert("There cannot be less than 0% effecient collisions! ");
+ element.value = eff*100;
+ }else{
+ eff = element.value/100;
+ }
+}
+
+// Function called to reset the dropdown menus when particles are added
+function reloadDropdowns(){
+
+ let xSelect = document.getElementById("graphx");
+ for(let i = 0; i < xSelect.options.length; i++){
+ xSelect.options.remove(i);
+ i--;
+ }
+
+ let ySelect = document.getElementById("graphy");
+ for(let j = 0; j < ySelect.options.length; j++){
+ ySelect.options.remove(j);
+ j--;
+ }
+
+ let xtOption = document.createElement("option");
+ xtOption.text = "Time"
+ xtOption.value = "measurement"
+
+ let xeOption = document.createElement("option");
+ xeOption.text = "Total Kinetic Energy"
+ xeOption.value = "ekt"
+
+ let ytOption = document.createElement("option");
+ ytOption.text = "Time"
+ ytOption.value = "measurement"
+
+ let yeOption = document.createElement("option");
+ yeOption.text = "Total Kinetic Energy"
+ yeOption.value = "ekt"
+
+ xSelect.options.add(xtOption, 0);
+ xSelect.options.add(xeOption, 1);
+
+ ySelect.options.add(ytOption, 1);
+ ySelect.options.add(yeOption, 0);
+
+ particles.forEach((elem, i) =>{
+ let temp = document.createElement("option");
+ let temp2 = document.createElement("option");
+ temp.text = "Particle " + (i+1).toString() + " Kinetic Energy"
+ temp.value = i+1;
+ temp2.text = "Particle " + (i+1).toString() + " Kinetic Energy"
+ temp2.value = i+1;
+ xSelect.options.add(temp, xSelect.options.length-1);
+ ySelect.options.add(temp2, ySelect.options.length-1);
+ })
+}
+
+function setVelocity(event){
+ let x = event.clientX-25;
+ let y = event.clientX-25;
+ while(mouseIsDown){
+ console.log(x);
+ }
+}
+
+function doMouseDown(event){
+ mouseIsDown = true;
+ setVelocity(event);
+}
+
+function doMouseUp(event){
+ alert();
+ mouseIsDown = false;
+}
\ No newline at end of file
diff --git a/shot.mp3 b/shot.mp3
new file mode 100644
index 0000000000000000000000000000000000000000..58ab220ac4aa9885420637b81daeef68311d3df2
GIT binary patch
literal 22040
zcmdqJWpLeGuq}AZ%#1NJGcz+YGc(7IF=jgEV~ClVV`gSd%rP@V%xChwJNNhdJ2hP;
zsY+W_YH2U6?q0h~;J0Ku!2iYc>|JdCU4r&+;{X73@Bxs>$oTl=s^a>swoY{_O7`
z9i5(@pI=^HU*Fu^-#?&B?;g=K4eC^q0m2|0+u4G)v$Ue8U(mRB;j`T}cdS6<10&}8T_c`uhNm>f1}
z$%Pj7r=XO6xyfWmI36(zj^VBt<5%hRlinq*)sE@v9vCGg=6@Y2@SXG`SA(sj`syxuatFm~AsIeR+W
zO8#0~`P(8pjb?b5l-tJFXE|Eln?;XEl|B~AXg5rjA$rDm^DS?KrgnU&>;S4
zK6VU7-fYsLKNJxdJBEnIEUI?l=s?Rx=&R7yElOTu;QQDBfDo!eF=AXQYJY*O!GK46
zshBQ^04L{r_U$CBeD2bljeiNVOk<@KD4M;~F8vte8A$CXu~Alb^n%k!9NQm{dF{G1
zS*!7D*%xt@bLJ{z@eH=4^m5W+O>d&U*?%Gs%)0&Bc01>Dxoa?!?bo-He<%{HYM~kc
zp1j-En_dN_Fa2(h2wQIQd`x`YTCe~17-v|U!F{fBmVcjr(>O@d>v{+ZCe|e;T@9S(^7rPfF~U>pm4>KD
ziUc)=u+PD_wzo$apAQCq!bcNNodLu+NRp!VZ%;1V`0-)oI(FKa4mCk+w&H+z$k(hO
zWs%hN&6ZK*bf{NPNEz_kA3NL-xGP;97e_O|>*U$Yv=e}gJC8>=4}j#>d@KtJ0f-2h
zB%8g-N6wm$o6!9!MDAgm8JwnwwQz~|Pg_Y#i1C{bPY^~QUr>tA9gWx-Cm#q-n2{P#
zL_v4cQ^JO|8iSRfoT^%v!XI9L2F;zxAx$Z
zVRDxUahZ#UQ`zWX0$?(vP3^~9tDJrBz`
z)>J)gEaq)WtD4L*{bQkIR%4FT;%qIZ-#Ce;`LVFnB2hN?mT&L3!wjx
zj_AL4WEmRWZ^l-?{nw)`J+~TpC^jno)YqWjj%i^0Nbw#!G67p@N_84XPupl*9vN8y
zE+anWzBjk82)<+eY6f7WS<9g63Y*62aR*l?f;(b^I~N8i4O+?8BPvn$G{jL
z{9Ya8)c0@TpWPaxE2`Q9VVFt|oJw(ANgDW