Java tutorial
package org.github.gitswarm; /** * Copyright 2008 Michael Ogawa * * This file is part of code_swarm. * * code_swarm is free software: you can redistribute it and/or modify it under the terms of the GNU General Public * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later * version. * * code_swarm is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with code_swarm. If not, see * <http://www.gnu.org/licenses/>. */ import org.github.gitswarm.model.Drawable; import org.github.gitswarm.model.Edge; import org.github.gitswarm.model.PersonNode; import org.github.gitswarm.model.FileNode; import java.io.File; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Collection; import java.util.Collections; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutorService; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import org.apache.commons.lang3.tuple.MutablePair; import org.apache.commons.lang3.tuple.Pair; import org.github.gitswarm.avatar.AvatarFetcher; import org.github.gitswarm.avatar.AvatarFetcherChainer; import org.github.gitswarm.avatar.GitHubFetcher; import org.github.gitswarm.avatar.GravatarFetcher; import org.github.gitswarm.gui.ColorUtil; import org.github.gitswarm.model.Commit; import org.github.gitswarm.model.GitHistoryRepository; import org.github.gitswarm.model.HistoryRepository; import org.github.gitswarm.type.DisplayFile; import static org.github.gitswarm.type.DisplayFile.FUZZY; import static org.github.gitswarm.type.DisplayFile.JELLY; import static org.github.gitswarm.type.DisplayFile.SHARP; import processing.core.PApplet; import processing.core.PFont; import processing.core.PImage; public class GitSwarm extends PApplet { /** * @remark needed for any serializable class */ public static final long serialVersionUID = 0; private static Map<String, FileNode> nodes; private static Map<Pair<FileNode, PersonNode>, Edge> edges; private static Map<String, PersonNode> people; // Liveness cache private static List<PersonNode> livingPeople = new ArrayList<>(); private static List<Edge> livingEdges = new ArrayList<>(); private static List<FileNode> livingNodes = new ArrayList<>(); /** * @return list of people whose life is > 0 */ static List<PersonNode> getLivingPeople() { return Collections.unmodifiableList(livingPeople); } /** * @return list of edges whose life is > 0 */ private static List<Edge> getLivingEdges() { return Collections.unmodifiableList(livingEdges); } /** * @return list of file nodes whose life is > 0 */ private static List<FileNode> getLivingNodes() { return Collections.unmodifiableList(livingNodes); } private static <T extends Drawable> List<T> filterLiving(Collection<T> iter) { ArrayList<T> livingThings = new ArrayList<>(iter.size()); iter.stream().filter((thing) -> (thing.isAlive())).forEachOrdered((thing) -> { livingThings.add(thing); }); return livingThings; } static public void boot() { PApplet.main(new String[] { "org.github.gitswarm.GitSwarm" }); } @Override public void exitActual() { // no no... this.surface.setVisible(false); } // User-defined variables long UPDATE_DELTA = -1; int background; int PARTICLE_SIZE = 2; // Data storage HistoryRepository historyRepository; List<Commit> commits = new ArrayList<>(); LinkedList<List<Integer>> history; boolean finishedLoading = false; // Temporary variables FileEvent currentEvent; Date nextDate; Date prevDate; FileNode prevNode; int maxTouches; // Graphics objects PFont font; PFont boldFont; PImage sprite; PImage avatarMask; boolean paused = false; // Graphics state variables boolean showHelp; boolean showDebug; // Color mapper int currentColor; // Physics engine configuration private final PhysicsEngine physicsEngine = new PhysicsEngineOrderly(); // Formats the date string nicely SimpleDateFormat formatter = new SimpleDateFormat("dd-MM-yyyy"); private long lastDrawDuration = 0; private int maxFramesSaved; protected ExecutorService backgroundExecutor; public AvatarFetcher avatarFetcher = new AvatarFetcherChainer(new GitHubFetcher(), new GravatarFetcher()); private int fontColor; @Override public void settings() { this.width = Config.getInstance().getWidth().getValue(); this.height = Config.getInstance().getHeight().getValue(); size(width, height); } /** * Initialization */ @Override public void setup() { int maxBackgroundThreads = 4; backgroundExecutor = new ThreadPoolExecutor(1, maxBackgroundThreads, Long.MAX_VALUE, TimeUnit.NANOSECONDS, new ArrayBlockingQueue<>(4 * maxBackgroundThreads), new ThreadPoolExecutor.CallerRunsPolicy()); showDebug = false; background = ColorUtil.toAwtColor(Config.getInstance().getBackground().getValue()).getRGB(); fontColor = ColorUtil.toAwtColor(Config.getInstance().getFontColor().getValue()).getRGB(); double framesperday = Config.getInstance().getFramesPerDay(); UPDATE_DELTA = (long) (86400000 / framesperday); smooth(); frameRate(24); // init data structures nodes = new HashMap<>(); edges = new HashMap<>(); people = new HashMap<>(); history = new LinkedList<>(); loadRepEvents(); if (commits.isEmpty()) { return; } prevDate = commits.get(0).getDate(); maxFramesSaved = (int) Math.pow(10, Config.getInstance().getScreenshotFileMask().getValue().replaceAll("[^#]", "").length()); // Create fonts String fontName = Config.getInstance().getFont(); String boldFontName = Config.getInstance().getBoldFont(); Integer fontSize = Config.getInstance().getFontSize().getValue(); Integer fontSizeBold = Config.getInstance().getBoldFontSize().getValue(); font = createFont(fontName, fontSize); boldFont = createFont(boldFontName, fontSizeBold); textFont(font); // Create the file particle image sprite = loadImage("src/main/resources/particle.png"); avatarMask = loadImage("src/main/resources/mask.png"); avatarMask.resize(40, 40); // Add translucency (using itself in this case) sprite.mask(sprite); } /** * Main loop */ @Override public void draw() { long start = System.currentTimeMillis(); background(background); // clear screen with background color this.update(); // update state to next frame // Draw edges (for debugging only) if (Config.getInstance().getShowEdges().getValue()) { edges.values().forEach((edge) -> { if (edge.getLife() > 40) { stroke(255, edge.getLife() + 100); strokeWeight(0.35f); line(edge.getNodeFrom().getPosition().x, edge.getNodeFrom().getPosition().y, edge.getNodeTo().getPosition().x, edge.getNodeTo().getPosition().y); } }); } // Surround names with aura // Then blur it if (Config.getInstance().getDrawNamesHalo().getValue()) { drawPeopleNodesBlur(); } // Then draw names again, but sharp if (Config.getInstance().getDrawNamesSharp().getValue()) { drawPeopleNodesSharp(); } // Draw file particles getLivingNodes().forEach((node) -> { drawFileNode(node); }); textFont(font); if (showDebug) { // debug override legend information drawDebugData(); } else if (Config.getInstance().getShowLegend().getValue()) { // legend only if nothing "more important" drawLegend(); } if (Config.getInstance().getShowPopular().getValue()) { drawPopular(); } if (Config.getInstance().getShowHistogram().getValue()) { drawHistory(); } if (Config.getInstance().getShowDate().getValue()) { drawDate(); } if (Config.getInstance().getTakeSnapshots().getValue()) { dumpFrame(); } // Stop animation when we run out of data if (finishedLoading) { // noLoop(); backgroundExecutor.shutdown(); try { backgroundExecutor.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS); } catch (InterruptedException e) { /* Do nothing, just exit */ } exit(); } long end = System.currentTimeMillis(); lastDrawDuration = end - start; } /** * Surround names with aura */ public void drawPeopleNodesBlur() { colorMode(HSB); // First draw the name getLivingPeople().stream().map((p) -> { fill(hue(p.getFlavor()), 64, 255, p.getLife()); return p; }).forEachOrdered((p) -> { drawPersonNode(p); }); // Then blur it filter(BLUR, 3); } private void drawFileNode(FileNode n) { if (n.isAlive()) { DisplayFile displayFile = Config.getInstance().getDisplayFile(); float currentWidth = 0f; if (displayFile.equals(SHARP)) { colorMode(RGB); fill(n.getNodeHue(), n.getLife()); float w = 3 * PARTICLE_SIZE; currentWidth = w; if (n.getLife() >= n.getMinBold()) { stroke(255, 128); w *= 2; } else { noStroke(); } ellipseMode(CENTER); ellipse(n.getPosition().x, n.getPosition().y, w, w); } if (displayFile.equals(FUZZY)) { tint(n.getNodeHue(), n.getLife()); float w = (8 + (sqrt(n.getTouches()) * 4)) * PARTICLE_SIZE; currentWidth = w; // not used float dubw = w * 2; float halfw = w / 2; if (n.getLife() >= n.getMinBold()) { colorMode(HSB); tint(hue(n.getNodeHue()), saturation(n.getNodeHue()) - 192, 255, n.getLife()); // image( sprite, x - w, y - w, dubw, dubw ); } // else image(sprite, n.getPosition().x - halfw, n.getPosition().y - halfw, w, w); } if (displayFile.equals(JELLY)) { noFill(); if (n.getLife() >= n.getMinBold()) { stroke(255); } else { stroke(n.getNodeHue(), n.getLife()); } float w = sqrt(n.getTouches()) * PARTICLE_SIZE; currentWidth = w; ellipseMode(CENTER); ellipse(n.getPosition().x, n.getPosition().y, w, w); } // Draw motion blur // float d = mPosition.distance(mLastPosition); float nx = n.getPosition().x - n.getLastPosition().x; float ny = n.getPosition().y - n.getLastPosition().y; float d = (float) Math.sqrt(nx * nx + ny * ny); stroke(n.getNodeHue(), min(255f * (d / 10f), 255f) / 10f); strokeCap(ROUND); strokeWeight(currentWidth / 4f); // strokeWeight((float)life / 10.0 * (float)PARTICLE_SIZE); line(n.getPosition().x, n.getPosition().y, n.getLastPosition().x, n.getLastPosition().y); } } private void drawPersonNode(PersonNode p) { if (p.isAlive()) { textAlign(CENTER, CENTER); /** * TODO: proportional font size, or light intensity, or some sort of thing to disable the flashing */ if (p.getLife() >= p.getMinBold()) { textFont(boldFont); } else { textFont(font); } fill(fontColor, p.getLife()); if (Config.getInstance().getShowUsername().getValue()) { text(p.getName(), p.getPosition().x, p.getPosition().y + 10); } if (p.getIcon() != null) { colorMode(RGB); tint(255, 255, 255, max(0, p.getLife() - 80)); image(p.getIcon(), p.getPosition().x - (avatarFetcher.getSize() / 2), p.getPosition().y - (avatarFetcher.getSize() - (Config.getInstance().getShowUsername().getValue() ? 5 : 15))); } } } /** * Draw person's name */ public void drawPeopleNodesSharp() { colorMode(RGB); getLivingPeople().stream().map((p) -> { fill(lerpColor(p.getFlavor(), color(255), 0.5f), max(p.getLife() - 50, 0)); return p; }).forEachOrdered((p) -> { drawPersonNode(p); }); } /** * Draw date in lower-right corner */ public void drawDate() { fill(fontColor, 255); String dateText = formatter.format(prevDate); textAlign(RIGHT, BASELINE); textSize(font.getSize()); text(dateText, width - 3, height - (2 + textDescent())); } /** * Draw histogram in lower-left */ public void drawHistory() { int counter = 0; strokeWeight(PARTICLE_SIZE); for (List<Integer> list : history) { if (!list.isEmpty()) { int color = list.get(0); int start = 0; int end = 0; for (int nextColor : list) { if (nextColor == color) { end++; } else { stroke(color, 255); rectMode(CORNERS); rect(counter, height - start - 3, counter, height - end - 3); start = end; color = nextColor; } } } counter += 1; } } /** * Show color codings */ public void drawLegend() { noStroke(); fill(fontColor, 255); textFont(font); textAlign(LEFT, TOP); text("Legend:", 3, 3); for (int i = 0; i < Config.getInstance().getColorAssigner().getTests().size(); i++) { ColorTest t = Config.getInstance().getColorAssigner().getTests().get(i); fill(t.getC1().getRGB(), 200); text(t.getLabel(), font.getSize(), 3 + ((i + 1) * (font.getSize() + 2))); } } /** * Show debug information about all drawable objects */ public void drawDebugData() { noStroke(); textFont(font); textAlign(LEFT, TOP); fill(fontColor, 200); text("Nodes: " + nodes.size(), 0, 0); text("People: " + people.size(), 0, 10); text("Queue: " + commits.size(), 0, 20); text("Last render time: " + lastDrawDuration, 0, 30); } /** * TODO This could be made to look a lot better. */ private void drawPopular() { CopyOnWriteArrayList<FileNode> al = new CopyOnWriteArrayList<>(); noStroke(); textFont(font); textAlign(RIGHT, TOP); fill(fontColor, 200); text("Popular Nodes (touches):", width - 120, 0); for (FileNode fn : nodes.values()) { if (fn.qualifies()) { // Insertion Sort if (al.size() > 0) { int j = 0; for (; j < al.size(); j++) { if (fn.compareTo(al.get(j)) > 0) { break; } } al.add(j, fn); } else { al.add(fn); } } } int i = 1; ListIterator<FileNode> it = al.listIterator(); while (it.hasNext()) { FileNode n = it.next(); // Limit to the top 10. if (i <= 10) { text(n.getName() + " (" + n.getTouches() + ")", width - 100, 10 * i++); } else if (i > 10) { break; } } } /** * Take screenshot */ public void dumpFrame() { if (frameCount < this.maxFramesSaved) { String screenshotFileMask = Config.getInstance().getScreenshotFileMask().getValue(); final File outputFile = new File(insertFrame("data/" + screenshotFileMask)); final PImage image = get(); outputFile.getParentFile().mkdirs(); backgroundExecutor.execute(() -> { image.save(outputFile.getAbsolutePath()); }); } } private int commitIndex = 0; private Commit currentCommit; private int emptyDate = 0; /** * Update the particle positions */ public void update() { // Create a new histogram line List<Integer> colorList = new ArrayList<>(); history.add(colorList); nextDate = new Date(prevDate.getTime() + UPDATE_DELTA); if (commitIndex + 1 >= commits.size()) { exit(); } currentCommit = commits.get(commitIndex); if (currentCommit.getDate().before(nextDate)) { emptyDate = 0; commitIndex = commitIndex + 1; currentCommit.getEvents().stream().map((event) -> { FileNode file = findNode(event.getPath() + event.getFilename()); // add to histogram colorList.add(file.getNodeHue()); PersonNode person = findPerson(event.getAuthor()); colorMode(RGB); person.setFlavor(lerpColor(person.getFlavor(), file.getNodeHue(), 1.0f / person.getColorCount())); person.setColorCount(person.getColorCount() + 1); Edge edge = findEdge(file, person); file.setEditor(person); return file; }).forEachOrdered((file) -> { prevNode = file; }); } else { emptyDate++; if (emptyDate > Config.getInstance().getAllowedEmptyFrames()) { nextDate = currentCommit.getDate(); } } prevDate = nextDate; // sort colorbins Collections.sort(colorList); // restrict history to drawable area while (history.size() > 320) { history.remove(); } // Init frame: physicsEngine.initializeFrame(); livingPeople = filterLiving(people.values()); livingNodes = filterLiving(nodes.values()); livingEdges = filterLiving(edges.values()); // update velocity getLivingEdges().forEach((edge) -> { physicsEngine.onRelax(edge); }); // update velocity getLivingNodes().forEach((node) -> { physicsEngine.onRelax(node); }); // update velocity getLivingPeople().forEach((person) -> { physicsEngine.onRelax(person); }); // update position getLivingEdges().forEach((edge) -> { physicsEngine.onUpdate(edge); }); // update position getLivingNodes().forEach((node) -> { physicsEngine.onUpdate(node); }); // update position getLivingPeople().stream().map((person) -> { physicsEngine.onUpdate(person); return person; }).map((person) -> { person.getPosition().x = max(50, min(width - 50, person.getPosition().x)); return person; }).forEachOrdered((person) -> { person.getPosition().y = max(45, min(height - 15, person.getPosition().y)); }); // Finalize frame: physicsEngine.finalizeFrame(); } /** * Searches for the FileNode with a given name * * @param name * @return FileNode with matching name or null if not found. */ private FileNode findNode(String name) { FileNode file = nodes.get(name); if (file == null) { file = new FileNode(name, maxTouches); physicsEngine.startLocation(file); physicsEngine.startVelocity(file); colorMode(RGB); nodes.put(name, file); } else { file.freshen(); } return file; } /** * Searches for the Edge connecting the given nodes */ private Edge findEdge(FileNode file, PersonNode person) { Edge edge = edges.get(new MutablePair<>(file, person)); if (edge == null) { edge = new Edge(file, person); edges.put(new MutablePair<>(file, person), edge); } else { edge.freshen(); } return edge; } /** * Searches for the PersonNode with a given name. */ private PersonNode findPerson(String name) { PersonNode person = people.get(name); if (person == null) { person = new PersonNode(name); String iconFile = avatarFetcher.fetchUserImage(person.getName()); if (iconFile != null) { PImage icon = loadImage(iconFile, "unknown"); icon.resize(avatarFetcher.getSize(), avatarFetcher.getSize()); icon.mask(avatarMask); person.setIcon(icon); } physicsEngine.startLocation(person); physicsEngine.startVelocity(person); people.put(name, person); } else { person.freshen(); } return person; } /** * Load the standard event-formatted file. */ public void loadRepEvents() { this.commits = new GitHistoryRepository(Config.getInstance().getGitDirectory()).getHistory(-1); } }