/** * * --Copyright notice-- * * Copyright (c) Andy Evans. * http://www.geog.leeds.ac.uk/people/a.evans/ * This software is licensed under 'The Artistic License 2.0' which can be found at * the Open Source Initiative website at... * http://opensource.org/licenses/artistic-license-2.0 * Please see disclaimers therein. * * The Standard Version source code, and associated documentation can be found at... * Evans, A.J. (2007) 'Simple SIR model with Random Walk' MASS Group, University of Leeds, Britain * [online] http://www.geog.leeds.ac.uk/courses/other/disease-modelling/ * * --End of Copyright notice-- * **/ /** * Basic SIR disease model with random walk agents. * Runs as application or applet.
* Applet params as follows:
* "backgroundColor" example value: "#FFFFFF" - Background hex colour.
* "textColor" example value: "#000000" - Foreground colour for text and box.
* "initialSliderValues" example value: "100,100,0" - Sets the susceptibilitySlider, infectedTimeSlider, recoveryChanceSlider.
* "lock" example value: "false" - whether to lock the values on the scrollbars.
* @author Andy Evans: http://www.geog.leeds.ac.uk/people/a.evans/
* @version 1.0
**/
import controlP5.*;
import java.awt.Toolkit;
import java.awt.datatransfer.*;
import javax.swing.*;
import java.util.*;
import java.awt.Color;
import java.util.StringTokenizer;
// For sliders.
ControlP5 controlP5 = null;
String lock = "false";
// Fonts and GUI.
PFont titleFont;
PFont txtFont;
PFont boldFont;
int sliderSpacing = 90;
int txtColor = #C8DEF5;
int bgColor = #000000;
int sliderBackground = #003652;
int sliderForeground = #00698c;
String[] controlNames = {"susceptibilitySlider", "infectedTimeSlider", "recoveryChanceSlider"}; // These just used for slider names.
String[] controlLabels = {"Suscept\n-ibility(%)", "Infectious\nTime(s)", "Recovery\nChance(%)"};
int[] sliderColors = {#005a00, #640000, #1b1f9f, #333333}; // Note these are also the agent colours, matching the use of the scrollbars. Same order as agent types (below) so we can use types as index for colours.
int[] sliderDefaults = {50,50,50}; // Default starting values.
int[] colorBlindSliderColors = {#666666, #000000, #bbbbbb, #dddddd};
int colorBlindBgColor = #ffffff;
int colorBlindTxtColor = #000000;
// Agent array. Created with first agent and nulled when Q pressed.
// agents[0][] = x coordinate.
// agents[1][] = y coordinate.
// agents[2][] = type (below).
// agents[3][] = time until end of infection.
// agents[4][] = type at end of infection (INSUSCEPTIBLE or DEAD).
int agents[][] = null;
// Agent types.
final int SUSCEPTIBLE = 0;
final int INFECTED = 1;
final int INSUSCEPTIBLE = 2;
final int DEAD = 3;
// Model running (i.e. Random walk motion).
boolean running = false;
// Dimensions of box for agents and agent size.
int stageLeft = 0;
int stageTop = 0;
int stageWidth = 0;
int stageHeight = 0;
int circleSize = 10;
int lineStart = 33;
// For copy and paste.
Clipboard clipboard = null;
boolean[] keys = new boolean[526];
boolean copying = false;
// For recording data for copy and paste.
double timer = 0.0;
double oldTimer = 0.0;
String headerString = "Time,Total,Susceptible,Infected,Insusceptible,Dead\n";
String recordedData = headerString;
/**
* Draws the basic GUI and sets up clipboard if appropriate.
**/
void setup() {
size(800, 400);
// Initiate clipboard - fails for applets.
try {
clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
}
catch (Exception e) {
clipboard = null;
}
// Set up as applet or application
if (clipboard != null) {
frame.setTitle("Disease modelling");
} else {
txtColor = getColor("textColor");
bgColor = getColor("backgroundColor");
sliderDefaults = getSliderDefaults("initialSliderValues");
lock = getParameter("lock");
if (lock == null) lock = "false";
if (lock.equals("true")) {
lineStart = 40;
}
}
background(bgColor);
// GUI
smooth();
txtFont = loadFont("MiriamFixed-12.vlw");
boldFont = loadFont("CourierNewPS-BoldMT-12.vlw");
// GUI elements
controlP5 = new ControlP5(this);
int xPosition = 0;
// Sliders.
for (int count = 0; count < controlNames.length; count++) {
// Sliders
controlP5.addSlider(controlNames[count], 0, 100, sliderColors[count], xPosition + 40, 50-6, 14, height-120);
Slider slider = (Slider)controlP5.controller(controlNames[count]);
slider.setValue(sliderDefaults[count]);
slider.setLabelVisible(false);
if (lock.equals("true")) {
slider.lock();
}
slider.setColorActive(sliderForeground);
slider.setColorBackground(sliderBackground);
xPosition = xPosition + sliderSpacing;
}
// Now we know the size of the app, make the stage sizes.
stageLeft = 40+90+90 + 100;
stageTop = 50 - 6;
stageWidth = (width - 20) - stageLeft;
stageHeight = height - 120;
}
/**
* This method returns a compressed colour int from a PARAM.
* The PARAM value should be a hash symbol and then a * standard web hex colour. If the PARAM is missing * the original instance variable defaults are returned. **/ private int getColor(String param) { String colorString = getParameter(param); if (colorString == null) { if (param.equals("textColor")) return txtColor; else return bgColor; } else { try { int red = (Integer.decode("0x" + colorString.substring(1,3))).intValue(); int green = (Integer.decode("0x" + colorString.substring(3,5))).intValue(); int blue = (Integer.decode("0x" + colorString.substring(5))).intValue(); return (new Color(red, green, blue)).getRGB(); } catch (NumberFormatException nfe) { if (param.equals("textColor")) return txtColor; else return bgColor; } } } /** * Returns an array of ints, comma separated in a param tag. **/ private int[] getSliderDefaults(String param) { String defaultsString = getParameter(param); if (defaultsString == null) { return sliderDefaults; } else { try { StringTokenizer st = new StringTokenizer(defaultsString,","); int [] results = new int[st.countTokens()]; for (int i = 0; i < results.length; i++) { results[i] = Integer.parseInt(st.nextToken()); if (results[i] > 100) results[i] = 100; if (results[i] < 0) results[i] = 0; } return results; } catch (NumberFormatException nfe) { return sliderDefaults; } } } /** * Just used to reset slider colors to normal. * These are set abnormally when altered using their own methods, injected below. **/ public void mouseReleased() { for (int i = 0; i < controlNames.length; i++) { Slider s = (Slider)controlP5.controller(controlNames[i]); s.setColorBackground(sliderBackground); s.setColorActive(sliderForeground); } } /** * Changes color as slider moved. **/ public void susceptibilitySlider(int theValue) { Slider s = (Slider)controlP5.controller("susceptibilitySlider"); s.setColorActive(sliderColors[SUSCEPTIBLE]); s.setColorBackground(sliderColors[INSUSCEPTIBLE]); } /** * Changes color as slider moved. **/ public void infectedTimeSlider(int theValue) { Slider s = (Slider)controlP5.controller("infectedTimeSlider"); s.setColorActive(sliderColors[INFECTED]); s.setColorBackground(sliderBackground); } /** * Changes color as slider moved. **/ public void recoveryChanceSlider(int theValue) { Slider s = (Slider)controlP5.controller("recoveryChanceSlider"); s.setColorActive(sliderColors[INSUSCEPTIBLE]); s.setColorBackground(sliderColors[DEAD]); } /** * Adds agents, with types depending on order and scrollbars. * Also notes any changes and calls routine to update potentially copied data if * there have beeen any. **/ void mousePressed() { if(((mouseX < stageLeft) || (mouseX > stageLeft + stageWidth)) || ((mouseY < stageTop) || (mouseY > stageTop + stageHeight))) return; if ((agents == null) || (mouseButton == RIGHT)) { addAgent(mouseX, mouseY, INFECTED); } else { Slider slider = (Slider)controlP5.controller("susceptibilitySlider"); int value = int(slider.value()); if (Math.random()*100 < value) { addAgent(mouseX, mouseY, SUSCEPTIBLE); } else { addAgent(mouseX, mouseY, INSUSCEPTIBLE); } } recordChanges(); } /** * Adds agents, with types depending on order and scrollbars. **/ private void addAgent(int x, int y, int type) { // If first agent, make them infected. Mainly for those without a right mouse button. if (agents == null) { agents = new int[5][1]; agents[0][0] = x; agents[1][0] = y; agents[2][0] = type; // Set potential infected time. Slider slider = (Slider)controlP5.controller("infectedTimeSlider"); int value = int(slider.value()); agents[3][0] = value * (int)frameRate; // Set potential result of infection. // Note we sort this out now so agent keeps probability at time of formation. Slider slider2 = (Slider)controlP5.controller("recoveryChanceSlider"); int value2 = int(slider2.value()); int rand = (int)(Math.random()*100); agents[4][0] = (rand > value2)? DEAD:INSUSCEPTIBLE; timer = 0.0; // Zero time incase they've let the model run with no agents since start / Q pushed. return; } // Otherwise increase the size of the agents array and add another. int[][] temparray = new int [5][agents[0].length]; for (int i = 0; i < agents[0].length; i++) { temparray[0][i] = agents[0][i]; temparray[1][i] = agents[1][i]; temparray[2][i] = agents[2][i]; temparray[3][i] = agents[3][i]; temparray[4][i] = agents[4][i]; } agents = new int[5][agents[0].length + 1]; for (int i = 0; i < temparray[0].length; i++) { agents[0][i] = temparray[0][i]; agents[1][i] = temparray[1][i]; agents[2][i] = temparray[2][i]; agents[3][i] = temparray[3][i]; agents[4][i] = temparray[4][i]; } // New agent. agents[0][agents[0].length - 1] = x; agents[1][agents[1].length - 1] = y; agents[2][agents[2].length - 1] = type; // Set potential infected time. Slider slider = (Slider)controlP5.controller("infectedTimeSlider"); int value = int(slider.value()); agents[3][agents[2].length - 1] = value * (int)frameRate; // Set potential result of infection. // Note we sort this out now so agent keeps probability at time of formation. Slider slider2 = (Slider)controlP5.controller("recoveryChanceSlider"); int value2 = int(slider2.value()); int rand = (int)(Math.random()*100); agents[4][agents[2].length - 1] = (rand > value2)? DEAD:INSUSCEPTIBLE; return; } /** * Random walk motion. Very basic. **/ private void moveRandomly() { double xRand = 0; double yRand = 0; for (int i = 0; i < agents[0].length; i++) { if (agents[2][i] == DEAD) continue; // Skip the dead. xRand = Math.random(); yRand = Math.random(); if (xRand > 0.5) agents[0][i]++; else agents[0][i]--; if (yRand > 0.5) agents[1][i]++; else agents[1][i]--; // Make sure they don't wander off stage. if (agents[0][i] < stageLeft) agents[0][i]++; if (agents[0][i] > stageLeft + stageWidth) agents[0][i]--; if (agents[1][i] < stageTop) agents[1][i]++; if (agents[1][i] > stageTop + stageHeight) agents[1][i]--; } } /** * Check for collisions. Again, very basic version. **/ private void checkCollisions() { int xDiff = 0; int yDiff = 0; boolean statesChanged = false; for (int i = 0; i < agents[0].length; i++) { if (agents[2][i] == DEAD) continue; // Skip the dead for (int j = i; j < agents[0].length; j++) { // NB Only test against those we haven't yet tested as agent[i] xDiff = Math.abs(agents[0][i] - agents[0][j]); yDiff = Math.abs(agents[1][i] - agents[1][j]); if ((xDiff < circleSize) && (yDiff < circleSize)) { if ((agents[2][i] == INFECTED) && (agents[2][j] == SUSCEPTIBLE)) { agents[2][j] = INFECTED; statesChanged = true; } if ((agents[2][j] == INFECTED)&& (agents[2][i] == SUSCEPTIBLE)) { agents[2][i] = INFECTED; statesChanged = true; } } } // Reduce time recovered (agents[3]) and kill or save as stored in agents[4]. if (agents[2][i] == INFECTED) { agents[3][i]--; if (agents[3][i] < 1) { agents[2][i] = agents[4][i]; statesChanged = true; } } } // If any agents have changed, add new summary of data to data to be copied and pasted. if (statesChanged) { recordChanges(); statesChanged = false; } } /** * Set us in copy mode and copies to clipboard if CTRL-C pressed. **/ void keyPressed() { keys[keyCode] = true; if (checkKey(CONTROL) && checkKey(KeyEvent.VK_C)) { // Enbolden the copy help message (see draw()). copying = true; // Copy the recordedData. StringSelection s = new StringSelection(recordedData); if (clipboard != null) clipboard.setContents(s, s); return; } // Run or stop if (checkKey(KeyEvent.VK_SPACE)) { running = !running; return; } // Clear everything if (checkKey(KeyEvent.VK_Q)) { agents = null; timer = 0.0; recordedData = headerString; // First agent added in mousePressed. return; } // Colorblind option if (checkKey(KeyEvent.VK_G)) { int tempBgColor = bgColor; bgColor = colorBlindBgColor; colorBlindBgColor = tempBgColor; int tempTxtColor = txtColor; txtColor = colorBlindTxtColor; colorBlindTxtColor = tempTxtColor; int[] tempStoredColors = new int[sliderColors.length]; System.arraycopy(sliderColors,0,tempStoredColors,0,sliderColors.length); System.arraycopy(colorBlindSliderColors,0,sliderColors,0,sliderColors.length); System.arraycopy(tempStoredColors,0,colorBlindSliderColors,0,sliderColors.length); Slider s = (Slider)controlP5.controller("susceptibilitySlider"); s.setColorActive(sliderForeground); s.setColorBackground(sliderBackground); s.updateEvents(); s = (Slider)controlP5.controller("infectedTimeSlider"); s.setColorActive(sliderForeground); s.setColorBackground(sliderBackground); s = (Slider)controlP5.controller("recoveryChanceSlider"); s.setColorActive(sliderForeground); s.setColorBackground(sliderBackground); background(bgColor); return; } } /** * Set us out of copy mode. **/ void keyReleased() { keys[keyCode] = false; // Unenbolden the copy help message (see draw()). copying = false; } /** * CTRL checking code from http://wiki.processing.org/w/Multiple_key_presses **/ boolean checkKey(int k) { if (keys.length >= k) { return keys[k]; } return false; } /** * Adds to store of data ready for copying and pasting. **/ private void recordChanges() { // recordedData reinitialised whenever cleared. // Timer reinitialised in same place. // recordChanges called when agents added or collide, or recover/die. int [] typeCounts = new int[4]; // Count agents of different types. for (int i = 0; i < agents[0].length; i++) { typeCounts[agents[2][i]]++; } // Construct other variables. double time = (timer/frameRate); int total = typeCounts[SUSCEPTIBLE] + typeCounts[INFECTED] + typeCounts[INSUSCEPTIBLE] + typeCounts[DEAD]; // If the user hasn't run the model between changes, usually because they are adding lots of agents, and there is data. // Remove the last entry so they don't get a record for each agent added (NB they do get this if the model is running, // but then they're adding them over time, here they're not, the time would be the same for each agent). if ((timer == oldTimer) && (recordedData.length() > headerString.length())) { recordedData = recordedData.substring(0,recordedData.lastIndexOf("\n", recordedData.length() - 2) + 1); } // Add to the stored data string. recordedData = recordedData + time + "," + total + "," + typeCounts[SUSCEPTIBLE] + "," + typeCounts[INFECTED] + "," + typeCounts[INSUSCEPTIBLE] + "," + typeCounts[DEAD] + "\n"; // Reset the timer to check whether they've run the model. oldTimer = timer; } /** * Draws the GUI with new color. **/ void draw() { // Increment time running since first run started / clear key pushed. if (running) timer++; // GUI textFont(txtFont); textSize(12); smooth(); // GUI elements int xPosition = 0; // Sliders' text down side. textAlign(RIGHT); for (int count = 0; count < controlNames.length; count++) { fill(bgColor); noStroke(); rect(xPosition + 15, 42, 24, height-110); fill(txtColor); // Decimal numbers for (int i = 100; i >= 0; i -= 10) { text(i, xPosition + 39, 50 + (100-i)*(((height-120.0)/100.0))); } xPosition = xPosition + sliderSpacing; } // Little flashes to show column colours. noStroke(); fill(sliderColors[0]); rect(40, (50-6)+(height-120)+5, 14, 1); fill(sliderColors[1]); rect(40+90, (50-6)+(height-120)+5, 14, 1); fill(sliderColors[2]); rect(40+90+90, (50-6)+(height-120)+5, 14, 1); // Paint over the old values from the sliders. fill(bgColor); noStroke(); rect(0, height-70, width, 60); // Paint the new values from the sliders. fill(txtColor); xPosition = 0; Slider slider = null; int value = 0; // Loop through sliders. for (int i = 0; i < controlNames.length; i++) { // Get slider value. slider = (Slider)controlP5.controller(controlNames[i]); value = int(slider.value()); // Print value on GUI. textAlign(LEFT); text(value, xPosition + 40, 50 + (height-100.0)); text(controlLabels[i], xPosition + 40, 50 + (height-80.0)); xPosition = xPosition + 90; } // Paint the old stage over. fill(bgColor); rect(stageLeft + 1, stageTop + 1, stageWidth - 1, stageHeight - 1); // Paint the agents. if (agents != null) { for (int i = 0; i < agents[0].length; i++) { fill(sliderColors[agents[2][i]]); ellipse(agents[0][i],agents[1][i],circleSize,circleSize); } } // Paint the stage box, and some black boxes around it to stop the agents appear // half outside the boundary when their centres are up against it. stroke(txtColor); line(stageLeft, stageTop, stageLeft, stageTop + stageHeight); line(stageLeft, stageTop, stageLeft + stageWidth, stageTop); line(stageLeft + stageWidth, stageTop, stageLeft + stageWidth, stageTop + stageHeight); line(stageLeft, stageTop + stageHeight, stageLeft + stageWidth, stageTop + stageHeight); fill(bgColor); stroke(bgColor); rect(stageLeft - circleSize, stageTop - circleSize, circleSize - 1, stageHeight + circleSize + circleSize); // left rect(stageLeft - circleSize, stageTop - circleSize, stageWidth + circleSize + circleSize, circleSize - 1); // top rect(stageLeft + stageWidth + 1, stageTop - circleSize, circleSize, stageHeight + circleSize + circleSize); // right rect(stageLeft - circleSize, stageTop + stageHeight + 1, stageWidth + circleSize + circleSize, circleSize); // bottom fill(txtColor); // Move the agents read for next step. if ((agents != null)&&(running)) { moveRandomly(); checkCollisions(); } // Add instructions. text("Click to add people; 1st person infected, also right mouse button.", stageLeft, lineStart + int((height-90.0))); if (lock.equals("true")) { text("Spacebar runs/stops. Q clears. G for greyscale.", stageLeft, lineStart + 15 + int((height-90.0))); } else { text("Scrollbars change new people only. Spacebar runs/stops. Q clears. ", stageLeft, lineStart + 15 + int((height-90.0))); if (clipboard != null) { text("G for greyscale.", stageLeft, lineStart + 45 + int((height-90.0))); } else { text("G for greyscale.", stageLeft, lineStart + 30 + int((height-90.0))); } } // If we're in copy mode, bold up the next text to give visual clue. if (clipboard != null) { if (copying) { textFont(boldFont); } else { textFont(txtFont); } text("Ctrl+C copies infection data. Use Ctrl+V in other apps to paste.", stageLeft, lineStart + 30 + int((height-90.0))); textFont(txtFont); } }