/* * Steven Herbst * AP Physics 2004 - 2005 * Thomas Jefferson HS for Science and Technology * Instructor: Dr. John Dell * * Software licensed under GNU General Public License (GPL) * See http://www.gnu.org/copyleft/gpl.html for details */ import java.util.*; import java.text.DecimalFormat; // Grid Constants int TICK_SCALE = 10; // Relative spacing of tick marks int TICK_LENGTH = 2; int gridX = 75; int gridY = 75; int translateX, translateY; // Point Constants int DOT_RADIUS = 1; float FLOAT_ERROR = 1e-10; DecimalFormat displayFormat; // Line Constants int START_LINE = 1; // Text Constants int TEXT_HEIGHT = 8; int TEXT_WIDTH = 8; int TEXT_DISPLACEMENT = 40; int TRANSFORM_DISPLACEMENT = 60; // Menu Constants int minimumTime = 500; // Menu selection considered invalid if menu is open for less than minimumTime // Hover Constants float LINE_DIST_MARGIN = 4.0; float POINT_DIST_MARGIN = 5.0; // Global variables // Relativity float beta; float gamma; float[][] transformMatrix, inverseMatrix; // Display float zoom = 1; Point points, transform, lineStart, transformLineStart; // Frames State states, sentinel; // Mouse boolean dragged; // Used to determine context of mouse release // Menu Menu menu; int menuTime; // Time when menu was opened boolean pointAdded; // Used to determine if menu should open // I/O boolean saveFrame; //GUI LinkedList items; // Labels Label info; Label graphLabel, transformLabel; Label lineLabel, transformLineLabel; // Buttons Button layerButtons[]; // Contains the 4 buttons that allow user to navigate frames // Sliders Slider pslider, sslider, xslider, yslider, zslider; /* Class GUI_Item * Subclasses: * Label * Menu * MenuItem * Button * Slider */ abstract class GUI_Item{ abstract void display(); abstract void update(); } class Label extends GUI_Item{ int x, y; String text; Label(String text, int x, int y){ this.x = x; this.y = y; this.text = text; items.add(this); } void display(){ display(0, 0, 0); } void display(int r, int g, int b){ fill(r, g, b); text(text, x, y - TEXT_HEIGHT / 2); } void update(){ } void setText(String text){ this.text = text; } String getText(){ return text; } } class MenuItem extends GUI_Item{ String text; int x, y; int width, height; static final int errorX = 5; static final int bufferX = 5; static final int bufferY = 10; boolean selected = false; Label myLabel; MenuItem(String text, int x, int y){ this.text = text; this.x = x; this.y = y; myLabel = new Label(text, x + bufferX, y + bufferY + TEXT_HEIGHT); width = 2 * bufferX + text.length() * TEXT_WIDTH; height = 2 * bufferY + TEXT_HEIGHT; } boolean over(){ return x - errorX <= mouseX && mouseX <= x + width + errorX && y <= mouseY && mouseY <= y + height; } void update(){ if (over()){ selected = true; } else{ selected = false; } } void display(){ if (selected){ fill(200, 200, 200); } else{ fill(225, 225, 225); } stroke(0, 0, 0); rect(x, y, width, height); myLabel.display(); } } class Menu extends GUI_Item{ LinkedList menuItems; MenuItem selected; int urx, ury, maxWidth; Menu(int urx, int ury){ this.urx = urx; this.ury = ury; menuItems = new LinkedList(); } void addMenuItem(String text){ MenuItem newItem = new MenuItem(text, urx, ury + menuItems.size() * (2 * MenuItem.bufferY + TEXT_HEIGHT)); if (maxWidth < newItem.width){ maxWidth = newItem.width; } menuItems.add(newItem); } void update(){ selected = null; Iterator it = menuItems.iterator(); MenuItem tmp; while(it.hasNext()){ tmp = (MenuItem)it.next(); tmp.width = maxWidth; tmp.update(); if (tmp.selected){ selected = tmp; } } } void display(){ Iterator it = menuItems.iterator(); while(it.hasNext()){ ((GUI_Item)it.next()).display(); } } void clear(){ Iterator it = menuItems.iterator(); MenuItem tmp; while(it.hasNext()){ tmp = (MenuItem)it.next(); items.remove(tmp.myLabel); it.remove(); } } String getSelected(){ if (selected != null){ return selected.text; } else{ return null; } } } class Button extends GUI_Item{ int x, y, width, height; Label myLabel; boolean pressed, enabled = true; static final int errorMarginX = 0; static final int errorMarginY = 5; static final int depth = 1; static final int padding = 3; Button(String text, int x, int y){ this.x = x; this.y = y; this.width = text.length() * TEXT_WIDTH; this.height = TEXT_HEIGHT + 2 * padding; myLabel = new Label(text, x - width / 2, y + padding); items.add(this); } Button(String text, int x, int y, int width, int height){ this.x = x; this.y = y; this.width = width; this.height = height; myLabel = new Label(text, x - width / 2, y); items.add(this); } boolean over(){ return (x - width / 2 - errorMarginX) <= mouseX && mouseX <= (x + width / 2 + errorMarginX) && (y - height / 2 - errorMarginY) <= mouseY && mouseY <= (y + height / 2 + errorMarginY) ; } void update(){ if (mousePressed && over()){ if (enabled){ pressed = true; } pointAdded = true; } } void display(){ if (enabled){ fill(225, 225, 225); } else{ fill(200, 200, 200); } if (pressed && mousePressed && over()){ rect(x - width / 2 + depth, y - width / 2 - depth, width, height); myLabel.x = x - width / 2 + depth; myLabel.y = y - depth + TEXT_HEIGHT + padding; myLabel.display(); } else{ rect(x - width / 2, y - width / 2, width, height); myLabel.x = x - width / 2; myLabel.y = y + TEXT_HEIGHT + padding; myLabel.display(); } } void reset(){ pressed = false; } void setEnabled(boolean choice){ if (choice){ enabled = true; } else{ enabled = false; } } } class Slider extends GUI_Item{ int x, y, width, height, pos, center, displacement; Label myLabel; static final int errorMarginX = 5; static final int errorMarginY = 15; Slider(int x, int y, int width, int height, int displacement){ this.x = x; this.center = x; this.y = y; this.width = width; this.height = height; this.displacement = displacement; pos = 0; items.add(this); } Slider(int x, int y, int width, int height, int displacement, String text){ this.x = x; this.center = x; this.y = y; this.width = width; this.height = height; this.displacement = displacement; pos = 0; myLabel = new Label(text, x - displacement, y - height); items.add(this); } boolean over(){ return (center - displacement - errorMarginX) <= mouseX && mouseX <= (center + displacement + errorMarginX) && (y - height / 2 - errorMarginY) <= mouseY && mouseY <= (y + height / 2 + errorMarginY) ; } void update(){ if (mousePressed && over()){ pointAdded = true; if(abs(mouseX - center) < displacement){ x = mouseX; } else if (mouseX < center){ x = center - displacement; } else{ x = center + displacement; } } if (abs(x - center) < 5){ x = center; } } void display(){ stroke(0, 0, 0); fill(0, 0, 0); line(center - displacement, y, center + displacement, y); line(center - displacement, y - 5, center - displacement, y + 5); line(center, y - 5, center, y + 5); line(center + displacement, y - 5, center + displacement, y + 5); rect(x - width / 2, y - height / 2, width, height); if (myLabel != null){ myLabel.display(); } } float getStatus(){ return 1.0 * (x - center) / displacement; } void setStatus(float s){ if (abs(s) <= displacement){ x = (int)(displacement * s + center); } } } class Point{ float x, y, z; float t; Point next; int condition; Point(){ } Point(float x, float t){ this.x = x; this.t = t; } Point(float x, float y, float t){ this.x = x; this.y = y; this.t = t; } Point(Point p){ this.x = p.x; this.y = p.y; this.t = p.t; this.next = p.next; this.condition = p.condition; } String toString(){ return "(" + displayFormat.format(x) + ", " + displayFormat.format(t) + ")"; } } class State{ State prev, next; float beta, zoom, thetaX, thetaY, thetaZ; Point points; State(Point points){ this.points = points; } State(Point points, float beta, float zoom){ this.points = points; this.beta = beta; this.zoom = zoom; } void append(State s){ State tmpNode = next; tmpNode.prev = s; s.next = tmpNode; s.prev = this; this.next = s; } } void setup(){ size(600, 600, P3D); textFont(loadFont("CourierNew36.vlw"), 12); dragged = false; pointAdded = false; saveFrame = false; translateX = 0; translateY = 0; points = null; transform = null; lineStart = null; transformLineStart = null; items = new LinkedList(); layerButtons = new Button[4]; sentinel = new State(null); pslider = new Slider(3 * width / 4, height / 8, 10, 10, 50, "Beta:"); sslider = new Slider(3 * width / 4, height / 4, 10, 10, 50, "Scale:"); layerButtons[0] = new Button("<", width / 4 - gridX, height / 4 + gridY + 25); layerButtons[1] = new Button(">", width / 4 - gridX + 10, height / 4 + gridY + 25); layerButtons[2] = new Button("<", 3 * width / 4 - gridX, 3 * height / 4 + gridY + 25); layerButtons[3] = new Button(">", 3 * width / 4 - gridX + 10, 3 * height / 4 + gridY + 25); initMatrices(); initStates(); graphLabel = new Label("",3 * width / 16, height / 4 + gridY + 2 * TEXT_HEIGHT); transformLabel = new Label("", 11 * width / 16, 3 * height / 4 + gridY + 2 * TEXT_HEIGHT); lineLabel = new Label("", 3 * width / 16, height / 4 + gridY + 5 * TEXT_HEIGHT); transformLineLabel = new Label("", 11 * width / 16, 3 * height / 4 + gridY + 5 * TEXT_HEIGHT); info = new Label("", 8 * TEXT_WIDTH, height / 2); displayFormat = new DecimalFormat("#.##"); framerate(30); } void draw(){ pointAdded = false; translateX = 0; translateY = 0; if (menu != null){ menu.update(); menu.display(); } else{ background(255, 255, 255); displayAll(); beta = pslider.getStatus(); zoom = pow(10, sslider.getStatus()); setTranslate(width / 4, height / 4); drawGrid(); plotPoints(points); boolean pointSelected = false; Point p = closestPoint(points, getMouseCoords()); if (p != null){ graphLabel.setText("" + p); transformLabel.setText("" + doTransform(p)); pointSelected = true; } setTranslate(3 * width / 4, 3 * height / 4); drawGrid(); updateTransform(); transformPoints(); plotPoints(transform); p = closestPoint(transform, getMouseCoords()); if (p != null){ graphLabel.setText("" + reverseTransform(p)); transformLabel.setText("" + p); pointSelected = true; } if (!pointSelected){ graphLabel.setText(""); transformLabel.setText(""); } if (layerButtons[0].pressed || layerButtons[2].pressed){ prevState(); layerButtons[0].reset(); layerButtons[2].reset(); } else if(layerButtons[1].pressed || layerButtons[3].pressed){ nextState(); layerButtons[1].reset(); layerButtons[3].reset(); } if (saveFrame){ saveFrame = false; saveFrame(); } } } /* Display methods: * displayLineInfo() * setTranslate() * drawGrid() * plot() * plotPoints() * connect() * addPoint() */ void displayLineInfo(float slope, float interval){ if (1 < abs(slope)){ info.setText("\n\nTime-like\n\nInterval: " + interval); } else if (abs(slope) < 1){ info.setText("\n\nSpace-like\n\nInterval: " + interval); } else{ info.setText("\n\nLight-like\n\nInterval: " + interval); } } void setTranslate(int x, int y){ translate(x - translateX, y - translateY); translateX = x; translateY = y; } void drawGrid(){ stroke(0, 0, 255); line(-gridX, 0, 0, gridX, 0, 0); line(0, -gridY, 0, 0, gridY, 0); stroke(0, 255, 0); line(-gridX, gridY, 0, gridX, -gridY, 0); line(-gridX, -gridY, 0, gridX, gridY, 0); stroke(0, 0, 0); for(int i = 0; i <= gridX; i += zoom * TICK_SCALE) /* Ticks for x-axis */ { line(i, -TICK_LENGTH, 0, i, TICK_LENGTH, 0); line(-i, -TICK_LENGTH, 0, -i, TICK_LENGTH, 0); } for(int j = 0; j <= gridY; j += zoom * TICK_SCALE) /* Ticks for y-axis */ { line(-TICK_LENGTH, j, 0, TICK_LENGTH, j, 0); line(-TICK_LENGTH, -j, 0, TICK_LENGTH, -j, 0); } } void plot(Point p){ plot(p, 255, 0, 0); } void plot(Point p, int r, int g, int b){ stroke(r, g, b); fill(r, g, b); int cx = (int)(p.x); int cy = (int)(p.y); int ct = (int)(-1.0 * p.t); point(cx - 1, ct - 1, cy - 1); point(cx - 1, ct - 1, cy); point(cx - 1, ct, cy); point(cx - 1, ct, cy - 1); point(cx, ct - 1, cy); point(cx, ct - 1, cy - 1); point(cx, ct, cy); point(cx, ct, cy - 1); } void connect(Point p){ connect(p, 255, 0, 0); } void connect(Point p, int r, int g, int b){ if (p.next != null){ stroke(r, g, b); line(p.x, -p.t, p.y, p.next.x, -p.next.t, p.next.y); } } void plotPoints(Point head){ plotPoints(head, false); } void plotPoints(Point head, boolean connect){ plotPoints(head, null, connect); } void plotPoints(Point head, Point tail, boolean connect){ plotPoints(head, tail, connect, 255, 0, 0); } void plotPoints(Point head, Point tail, boolean connect, int r, int g, int b){ Point curPoint = head; Point tmpPoint; boolean lineDisplayed = false; while(curPoint != null && !eq(curPoint, tail)){ tmpPoint = scale(curPoint); if(inGraph(tmpPoint)){ tmpPoint.next = scale(curPoint.next); if (connect || curPoint.condition == START_LINE){ float slope = computeM(curPoint, curPoint.next); float y0 = computeB(curPoint, slope); float d1 = computeD(scale(getMouseCoords(width / 4, height / 4), 1/zoom), slope, y0); float d2 = computeD(scale(getMouseCoords(3 * width / 4, 3 * height / 4), 1/zoom), slope, y0); if (d1 < LINE_DIST_MARGIN){ float interval = interval(curPoint, curPoint.next); lineLabel.setText("Start: " + curPoint + "\nEnd: " + curPoint.next); transformLineLabel.setText("Start: " + doTransform(curPoint) + "\nEnd: " + doTransform(curPoint.next)); lineDisplayed = true; displayLineInfo(slope, interval); } if (d2 < LINE_DIST_MARGIN){ float interval = interval(curPoint, curPoint.next); transformLineLabel.setText("Start: " + curPoint + "\nEnd: " + curPoint.next); lineLabel.setText("Start: " + reverseTransform(curPoint) + "\nEnd: " + reverseTransform(curPoint.next)); lineDisplayed = true; displayLineInfo(slope, interval); } connect(tmpPoint, r, g, b); } plot(tmpPoint, r, g, b); } curPoint = curPoint.next; } if (!lineDisplayed){ lineLabel.setText(""); transformLineLabel.setText(""); } } void addPoint(Point p){ if (points == null){ points = p; } else{ p.next = points; points = p; } } /* GUI Methods * displayAll() */ void displayAll(){ Iterator it = items.iterator(); GUI_Item tmp; while(it.hasNext()){ tmp = (GUI_Item)it.next(); tmp.display(); } } /* Point methods * closestPoint() * scale() * eq() * dist() */ Point closestPoint(Point head, Point p){ if (head == null){ return null; } Point closest = head; float closestDist = dist(head, p); float curDist; head = head.next; while(head != null){ curDist = dist(head, p); if(curDist < closestDist){ closest = head; closestDist = curDist; } head = head.next; } if (closestDist < POINT_DIST_MARGIN){ return closest; } else{ return null; } } Point scale(Point p){ return scale(p, zoom); } Point scale(Point p, float zoom){ if (p != null){ Point tmp = new Point(p.x * zoom, p.y * zoom, p.t * zoom); tmp.next = p.next; tmp.condition = p.condition; return tmp; } else{ return null; } } boolean eq(Point p1, Point p2){ return p1 != null && p2 != null && abs(p1.x - p2.x) <= FLOAT_ERROR && abs(p1.y - p2.y) <= FLOAT_ERROR && abs(p1.t - p2.t) <= FLOAT_ERROR; } float dist(Point p1, Point p2){ return dist(p1.x, p1.t, p2.x, p2.t); } float dist(Point p1){ return dist(p1, new Point(0, 0, 0)); } /* Relativity methods * updateGamma() * initMatrices() * updateTransform() * reverseTransform() * doTransform() * transformPoints() * interval() * mult() */ void updateGamma(){ gamma = 1.0 / sqrt(1.0 - beta*beta); } void initMatrices(){ transformMatrix = new float[3][3]; inverseMatrix = new float[3][3]; transformMatrix[2][2] = 1; inverseMatrix[2][2] = 1; } void updateTransform(){ updateGamma(); transformMatrix[0][0] = gamma; transformMatrix[0][1] = -gamma * beta; transformMatrix[1][0] = -gamma * beta; transformMatrix[1][1] = gamma; float commonFactor = 1 / (gamma * (1 - beta * beta)); inverseMatrix[0][0] = commonFactor; inverseMatrix[0][1] = beta * commonFactor; inverseMatrix[1][0] = beta * commonFactor; inverseMatrix[1][1] = commonFactor; } Point reverseTransform(Point p){ return doTransform(p, inverseMatrix); } Point doTransform(Point p, float[][] m){ float[][] coords = {{p.t}, {p.x}, {p.y}}; float[][] resultMatrix = mult(m, coords); Point transformedPoint = new Point(resultMatrix[1][0], resultMatrix[2][0], resultMatrix[0][0]); transformedPoint.next = p.next; transformedPoint.condition = p.condition; return transformedPoint; } Point doTransform(Point p){ return doTransform(p, transformMatrix); } void transformPoints(){ if (points == null){ transform = null; return; } transform = new Point(); Point lastPoint = new Point(); Point curPoint = points; Point newPoint = transform; Point tmp; while(curPoint != null){ newPoint.next = new Point(); tmp = doTransform(curPoint); newPoint.x = tmp.x; newPoint.y = tmp.y; newPoint.t = tmp.t; newPoint.condition = curPoint.condition; lastPoint = newPoint; newPoint = newPoint.next; curPoint = curPoint.next; } lastPoint.next = null; } float interval(float x1, float t1, float x2, float t2){ return (x2 - x1)*(x2 - x1) - (t2 - t1) * (t2 - t1); } float interval(Point p){ return interval(p, p.next); } float interval(Point p1, Point p2){ return interval(p1.x, p1.t, p2.x, p2.t); } float[][] mult(float[][] m1, float[][]m2){ float result[][] = new float[3][1]; result[0][0] = m1[0][0] * m2[0][0] + m1[0][1] * m2[1][0] + m1[0][2] * m2[2][0]; result[1][0] = m1[1][0] * m2[0][0] + m1[1][1] * m2[1][0] + m1[1][2] * m2[2][0]; result[2][0] = m1[2][0] * m2[0][0] + m1[2][1] * m2[2][0] + m1[2][2] * m2[2][0]; return result; } /* GUI methods * getMouseCoords() * mouseDragged() * mouseReleased() * mousePressed() * updateAll() */ Point getMouseCoords(){ return getMouseCoords(translateX, translateY); } Point getMouseCoords(int tx, int tt){ return getMouseCoords(new Point(mouseX, mouseY), tx, tt); } Point getMouseCoords(Point m, int tx, int tt){ float initX = m.x - tx; float initT = -(m.t - tt); return new Point(initX, initT); } void mouseDragged(){ updateAll(); dragged = true; } void mouseReleased(){ if (menu == null){ Point m = getMouseCoords(width / 4, height / 4); if (dragged && inGraph(m.x, m.t) && lineStart != null){ Point lineEnd = scale(m, 1/zoom); addPoint(lineEnd); addPoint(lineStart); } else{ m = getMouseCoords(3 * width / 4, 3 * height / 4); if (dragged && inGraph(m.x, m.t) && transformLineStart != null){ Point transformLineEnd = scale(m, 1/zoom); Point lineStart = reverseTransform(transformLineStart); Point lineEnd = reverseTransform(transformLineEnd); addPoint(lineEnd); addPoint(lineStart); } } } else{ if (minimumTime <= millis() - menuTime){ String choice = menu.getSelected(); if (choice != null){ if (choice.equals("Save screen image")){ saveFrame = true; } if (choice.equals("Help")){ info.setText( "Click once to add a point; drag \n" + "and release to draw a line.\n" + "Position the mouse over a point\n" + "or line to view information about it\n" + "You can adjust the relative\n" + "velocity between the two\n" + "inertial frames by dragging the\n" + "\"Beta\" slider. The scale\n" + "of the spacetime diagrams can be\n" + "controlled with the \"Scale\"\n" + "slider. To save your work in one\n" + "set of frames and move on to\n" + "a new set, just click one of the\n" + "\">\" buttons. At any point, \n" + "you can return by clicking one of\n" + "the \"<\" buttons. If you simply\n" + "wish to clear the current point set,\n" + "click \"Clear\" in the menu\n" ); } if (choice.equals("About")){ info.setText("Steven Herbst\n" + "AP Physics, 2004 - 2005\n" + "Thomas Jefferson HS for Science and Tech\n" + "Instructor: Dr. John Dell\n" ); } if (choice.equals("Clear")){ points = null; pslider.setStatus(0.0); sslider.setStatus(0.0); lineLabel.setText(""); transformLineLabel.setText(""); graphLabel.setText(""); transformLabel.setText(""); info.setText(""); } if (choice.equals("Restart")){ setup(); } } } menu.clear(); menu = null; } } void mousePressed(){ updateAll(); Point m = getMouseCoords(width / 4, height / 4); dragged = false; Point tmp; if (inGraph(m.x, m.t)){ lineStart = scale(m, 1/zoom); lineStart.condition = START_LINE; pointAdded = true; addPoint(scale(m, 1/zoom)); } else{ m = getMouseCoords(3 * width / 4, 3 * height / 4); if (inGraph(m.x, m.t)){ transformLineStart = scale(m, 1/zoom); transformLineStart.condition = START_LINE; pointAdded = true; addPoint(reverseTransform(scale(m, 1/zoom))); } } if (pointAdded == false){ menu = new Menu(mouseX, mouseY); if (!online){ menu.addMenuItem("Save screen image"); } menu.addMenuItem("Help"); menu.addMenuItem("About"); menu.addMenuItem("Clear"); menu.addMenuItem("Restart"); menuTime = millis(); } } void updateAll(){ Iterator it = items.iterator(); GUI_Item tmp; while(it.hasNext()){ tmp = (GUI_Item)it.next(); tmp.update(); } } /* Frame methods: * initStates() * saveState() * nextState() * newState() * prevState() */ void initStates(){ layerButtons[0].setEnabled(false); layerButtons[2].setEnabled(false); states = new State(null); states.prev = sentinel; states.next = sentinel; } void saveState(){ states.points = points; states.beta = pslider.getStatus(); states.zoom = sslider.getStatus(); } void nextState(){ saveState(); layerButtons[0].setEnabled(true); layerButtons[2].setEnabled(true); info.setText(""); if (states.next != sentinel){ states = states.next; points = states.points; pslider.setStatus(states.beta); sslider.setStatus(states.zoom); } else{ newState(); } } void newState(){ states.append(new State(null)); states = states.next; points = null; pslider.setStatus(0.0); sslider.setStatus(0.0); } void prevState(){ saveState(); if (states.prev != sentinel){ states = states.prev; points = states.points; pslider.setStatus(states.beta); sslider.setStatus(states.zoom); } if (states.prev == sentinel){ layerButtons[0].setEnabled(false); layerButtons[2].setEnabled(false); } } /* Graph methods * computeM() * computeB() * computeD() * inGraph() */ float computeM(Point p1, Point p2){ return (p2.t - p1.t) / (p2.x - p1.x); } float computeB(Point p, float m){ return p.t - m * p.x; } float computeD(Point p, float m, float b){ return abs((p.t - m * p.x - b) / sqrt(m * m + 1)); } boolean inGraph(Point p){ return inGraph(p.x, p.t); } boolean inGraph(float x, float y){ return inGraph(x, y, 0, 0); } boolean inGraph(float x, float y, int cx, int cy){ return cx - gridX <= x && x <= cx + gridX && cy - gridY <= y && y <= cx + gridY; }