| package com.google.silvercomet.client; |
| |
| import com.google.gwt.core.client.EntryPoint; |
| import com.google.gwt.core.client.GWT; |
| import com.google.gwt.resources.client.ClientBundle; |
| import com.google.gwt.resources.client.CssResource; |
| |
| import elemental.client.Browser; |
| import elemental.css.CSSStyleDeclaration; |
| import elemental.css.CSSStyleDeclaration.Display; |
| import elemental.css.CSSStyleDeclaration.Unit; |
| import elemental.css.CSSStyleDeclaration.Visibility; |
| import elemental.events.Event; |
| import elemental.events.EventListener; |
| import elemental.events.KeyboardEvent; |
| import elemental.dom.Document; |
| import elemental.dom.Element; |
| import elemental.html.InputElement; |
| import elemental.html.StyleElement; |
| import elemental.util.ArrayOf; |
| import elemental.util.ArrayOfInt; |
| import elemental.util.Collections; |
| import elemental.dom.*; |
| |
| /** |
| * All the view code that is fit to run. |
| * |
| * @author knorton@google.com (Kelly Norton) |
| */ |
| public class Main implements EntryPoint, Model.Listener, EventListener { |
| |
| /** |
| * Access to all the relevant classnames and constants for the CSS selectors. |
| */ |
| public static interface Css extends CssResource { |
| String bar(); |
| |
| String browserInfo(); |
| |
| String count(); |
| |
| String label(); |
| |
| String marker(); |
| |
| String moreResults(); |
| |
| String result(); |
| |
| String resultLeft(); |
| |
| String resultName(); |
| |
| String resultRight(); |
| |
| String root(); |
| |
| int rootHeight(); |
| |
| int rootWidth(); |
| |
| String tickMajor(); |
| |
| String tickMinor(); |
| } |
| |
| /** |
| * Bundles the CSS resources. |
| */ |
| public interface Resources extends ClientBundle { |
| @Source("silver-comet.gwt.css") |
| Css css(); |
| } |
| |
| /** |
| * A simple view to show a runner's finishing info. |
| */ |
| private static class RunnerView { |
| /** |
| * Returns a string describing a runner's place & finishing time. Example: |
| * 325th (1:49:11) |
| */ |
| private static String infoString(Runner runner) { |
| return placeString(runner.place()) + " (" + secondsToTime(runner.time(), true) + ")"; |
| } |
| |
| /** |
| * Produces a english locale human readable places. Example: 1st, 2nd, 11th, |
| * etc. |
| */ |
| private static String placeString(int place) { |
| final int tens = place / 10; |
| final int ones = place % 10; |
| if (ones == 1 && tens != 1) { |
| return place + "st"; |
| } else if (ones == 2 && tens != 1) { |
| return place + "nd"; |
| } else if (ones == 3 && tens != 1) { |
| return place + "rd"; |
| } else { |
| return place + "th"; |
| } |
| } |
| |
| /** |
| * Sets the current runner being displayed. |
| */ |
| private static native void setRunner(Element element, Runner runner) /*-{ |
| element.currentRunner = runner; |
| }-*/; |
| |
| /** |
| * This is a hack for sure, but I claim it demonstrates your ability to |
| * commit clever atrosities in JavaScript even while working in Java. |
| */ |
| static native Runner getRunnerFromElement(Element element) /*-{ |
| return element.currentRunner || element.parentNode.currentRunner; |
| }-*/; |
| |
| private final Element root; |
| |
| private final Element name; |
| |
| private final Element info; |
| |
| private final double secondsPerPixel; |
| |
| /** |
| * Create a new view with a classname for the root element and a scaling |
| * factor for translating along the timeline. |
| */ |
| RunnerView(String rootClass, double secondsPerPixel) { |
| this(rootClass, null, secondsPerPixel); |
| } |
| |
| /** |
| * Create a new view classnames for the root element and text elements and a |
| * scaling factor for translating along the timeline. |
| */ |
| RunnerView(String rootClass, String textClass, double secondsPerPixel) { |
| root = div(rootClass); |
| name = textClass == null ? div() : div(textClass); |
| info = textClass == null ? div() : div(textClass); |
| |
| root.appendChild(name); |
| root.appendChild(info); |
| |
| this.secondsPerPixel = secondsPerPixel; |
| } |
| |
| /** |
| * Returns the root element for the view. Generally used to add the view to |
| * the DOM. |
| */ |
| Element element() { |
| return root; |
| } |
| |
| /** |
| * Make the view invisible. |
| */ |
| void hide() { |
| root.getStyle().setVisibility(Visibility.HIDDEN); |
| } |
| |
| /** |
| * Make the view visible. |
| */ |
| void show() { |
| root.getStyle().removeProperty("visibility"); |
| } |
| |
| /** |
| * Update the view to show the info of a runner. |
| */ |
| void update(Runner runner) { |
| setRunner(root, runner); |
| name.setTextContent(runner.name()); |
| info.setTextContent(infoString(runner)); |
| final int x = (int) ((double) runner.time() / (double) Model.SECONDS_PER_HISTOGRAM_BUCKET |
| * secondsPerPixel); |
| |
| // TODO(knorton): This is actually wrong because it sets left on all |
| // markers. |
| root.getStyle().setLeft(x, Unit.PX); |
| show(); |
| } |
| } |
| |
| private static final int NUM_SEARCH_RESULTS = 6; |
| |
| /* |
| * Get the correct classname based on the index of the result. This allows for |
| * different classnames on the first and last item. |
| */ |
| private static String cssClassForResultItem(Css css, int index) { |
| if (index == 0) { |
| return css.result() + " " + css.resultLeft(); |
| } |
| |
| if (index == NUM_SEARCH_RESULTS - 1) { |
| return css.result() + " " + css.resultRight(); |
| } |
| |
| return css.result(); |
| } |
| |
| /** |
| * Convenience method for creating a new div. |
| */ |
| private static Element div() { |
| return Browser.getDocument().createElement("div"); |
| } |
| |
| /** |
| * Convenience method for creating a new div with a classname. |
| */ |
| private static Element div(String className) { |
| final Element e = div(); |
| e.setClassName(className); |
| return e; |
| } |
| |
| private static String getBrowserInfoString() { |
| final Browser.Info info = Browser.getInfo(); |
| if (info.isWebKit()) { |
| return "WebKit Browser"; |
| } else if (info.isGecko()) { |
| return "Gecko Browser"; |
| } |
| return "Unsupported Browser"; |
| } |
| |
| private static void injectStyles(Document document, String css) { |
| final StyleElement style = (StyleElement)document.createElement("style"); |
| style.setTextContent(css); |
| document.getHead().appendChild(style); |
| } |
| |
| /** |
| * Converts a integer to a {@link String}, prepending a zero if the string |
| * representation is only 1 character in length. |
| */ |
| private static String pad(int number) { |
| return number > 9 ? "" + number : "0" + number; |
| } |
| |
| /** |
| * Converts the number of seconds to a more human readable finishing time. |
| */ |
| private static String secondsToTime(int seconds, boolean includeSeconds) { |
| final int hrs = seconds / 3600; |
| final int mns = seconds / 60 - hrs * 60; |
| if (includeSeconds) { |
| final int scs = seconds - hrs * 3600 - mns * 60; |
| return hrs + ":" + pad(mns) + ":" + pad(scs); |
| } |
| return hrs + ":" + pad(mns); |
| } |
| |
| private final Css css = GWT.<Resources>create(Resources.class).css(); |
| |
| private Element root; |
| |
| private InputElement search; |
| |
| private RunnerView marker; |
| |
| private Model model; |
| |
| private String lastQuery; |
| |
| private Element results; |
| |
| private ArrayOf<RunnerView> resultItems; |
| |
| private double xAxisScale; |
| |
| private Element moreResults; |
| |
| /** |
| * Handles all DOM events for the app. |
| */ |
| @Override |
| public void handleEvent(Event evt) { |
| final Element target = (Element) evt.getTarget(); |
| |
| // Handle searches. |
| if (target == search) { |
| // if (((KeyboardEvent)evt).getKeyCode() == 42) { |
| // clearSearch(); |
| // } else { |
| final String query = search.getValue(); |
| updateSearch(query == null ? "" : query.trim()); |
| // } |
| return; |
| } |
| |
| // Handle clicks on search results. |
| final Runner runner = RunnerView.getRunnerFromElement(target); |
| if (runner != null) { |
| marker.update(runner); |
| clearSearch(); |
| return; |
| } |
| } |
| |
| /** |
| * Indicates that the data failed to load. |
| */ |
| @Override |
| public void modelDidFailLoading(Model model) { |
| } |
| |
| /** |
| * Indicates that the model finished building the search indexes. |
| */ |
| @Override |
| public void modelDidFinishBuildingIndex(Model model) { |
| // TODO(knorton): File crbug about readOnly not working properly. |
| // search.setReadOnly(false); |
| search.focus(); |
| } |
| |
| /** |
| * Indicates that all data has been loaded into the model. |
| */ |
| @Override |
| public void modelDidFinishLoading(Model model) { |
| final ArrayOfInt histogram = model.histogram(); |
| assert histogram.length() > 0 : "histogram is empty."; |
| |
| xAxisScale = (double) css.rootWidth() / (double) histogram.length(); |
| |
| // Build the histogram graph. |
| render(); |
| |
| // Create the marker. |
| marker = new RunnerView(css.marker(), xAxisScale); |
| marker.hide(); |
| root.appendChild(marker.element()); |
| Browser.getDocument().getBody().getStyle().setOpacity(1.0); |
| } |
| |
| /** |
| * The main entry point for the application. |
| */ |
| public void onModuleLoad() { |
| injectStyles(Browser.getDocument(), css.getText()); |
| |
| // TODO(knorton): Bad Elemental pattern. |
| search = (InputElement) Browser.getDocument().getElementById("search"); |
| // TODO(knorton): File crbug about readOnly not working properly. |
| // search.setReadOnly(true); |
| search.addEventListener("change", this, false); |
| search.addEventListener("keyup", this, false); |
| search.addEventListener("keydown", this, false); |
| |
| root = (Element) Browser.getDocument().getElementById("c"); |
| root.setClassName(css.root()); |
| |
| results = (Element)Browser.getDocument().getElementById("r"); |
| results.getStyle().setVisibility(Visibility.HIDDEN); |
| results.addEventListener("click", this, false); |
| |
| // Browser info indicator. |
| // TODO(knorton): Put this in a debug perm. |
| final Element info = Browser.getDocument().getElementById("f"); |
| info.setClassName(css.browserInfo()); |
| info.setTextContent(getBrowserInfoString()); |
| |
| model = new Model(this); |
| model.load(); |
| } |
| |
| /** |
| * Clear the search box and hide the result item list. |
| */ |
| private void clearSearch() { |
| search.setValue(""); |
| hideSearchResults(); |
| } |
| |
| /** |
| * Ensure that the DOM for result items has been built and appended to the |
| * DOM. |
| */ |
| private void ensureSearchResultItems() { |
| if (resultItems != null) { |
| return; |
| } |
| |
| resultItems = Collections.arrayOf(); |
| for (int i = 0; i < NUM_SEARCH_RESULTS; ++i) { |
| final RunnerView marker = |
| new RunnerView(cssClassForResultItem(css, i), css.resultName(), xAxisScale); |
| results.appendChild(marker.element()); |
| resultItems.push(marker); |
| } |
| |
| moreResults = div(css.moreResults()); |
| results.appendChild(moreResults); |
| moreResults.getStyle().setDisplay(Display.NONE); |
| } |
| |
| /** |
| * Hide the search result items list. |
| */ |
| private void hideSearchResults() { |
| results.getStyle().setVisibility(Visibility.HIDDEN); |
| } |
| |
| /** |
| * Renders the bar graph. |
| */ |
| private void render() { |
| final ArrayOfInt histogram = model.histogram(); |
| |
| final int padding = 2; |
| final int topPadding = 50; |
| |
| final double dx = xAxisScale; |
| final double dy = (double) css.rootHeight() / histogram.get(histogram.length() - 1); |
| final double halfDx = dx / 2.0; |
| |
| // Render all bars. |
| for (int i = 0, n = histogram.length(); i < n; ++i) { |
| final int value = histogram.get(i); |
| if (value == 0) continue; |
| |
| final int x = (int) (dx * i) + padding; |
| final int h = (int) (dy * value) - topPadding; |
| final int w = (int) dx - padding * 2; |
| |
| // Create the vertical bar. |
| final Element bar = div(css.bar()); |
| final CSSStyleDeclaration barStyle = bar.getStyle(); |
| barStyle.setLeft(x, Unit.PX); |
| barStyle.setBottom("0"); |
| barStyle.setHeight(h, Unit.PX); |
| barStyle.setWidth(w, Unit.PX); |
| |
| // Add a count at the top. |
| final Element count = div(css.count()); |
| count.setTextContent("" + value); |
| |
| bar.appendChild(count); |
| root.appendChild(bar); |
| } |
| |
| // Render labels |
| for (int i = 0, n = histogram.length(); i <= n; ++i) { |
| final int x = (int) (dx * i); |
| final int w = (int) dx; |
| |
| final String time = secondsToTime(i * Model.SECONDS_PER_HISTOGRAM_BUCKET, false); |
| |
| final Element label = div(css.label()); |
| label.setTextContent(time); |
| final CSSStyleDeclaration labelStyle = label.getStyle(); |
| labelStyle.setLeft(x - dx, Unit.PX); |
| labelStyle.setWidth(w, Unit.PX); |
| root.appendChild(label); |
| |
| // TODO(knorton): Heh, that's pretty trashy. I should fix that. :-) |
| final Element tick = |
| div(time.charAt(time.length() - 1) == '0' && time.charAt(time.length() - 2) == '0' |
| ? css.tickMajor() : css.tickMinor()); |
| tick.getStyle().setLeft(x, Unit.PX); |
| root.appendChild(tick); |
| } |
| } |
| |
| /** |
| * Show the search result items list and update it with the specified list of |
| * results. |
| */ |
| private void showSearchResults(ArrayOf<Runner> runners) { |
| ensureSearchResultItems(); |
| results.getStyle().removeProperty("visibility"); |
| for (int i = 0; i < NUM_SEARCH_RESULTS; ++i) { |
| final RunnerView item = resultItems.get(i); |
| if (i < runners.length()) { |
| item.show(); |
| item.update(runners.get(i)); |
| } else { |
| item.hide(); |
| } |
| } |
| |
| if (runners.length() > NUM_SEARCH_RESULTS) { |
| moreResults.setTextContent("+" + (runners.length() - NUM_SEARCH_RESULTS) + " more"); |
| moreResults.getStyle().removeProperty("display"); |
| } else { |
| moreResults.getStyle().setDisplay(Display.NONE); |
| } |
| } |
| |
| /** |
| * Update the search results visually for the specified query. |
| */ |
| private void updateSearch(String query) { |
| if (query.equals(lastQuery)) { |
| return; |
| } |
| |
| lastQuery = query; |
| if (query.length() == 0) { |
| hideSearchResults(); |
| } |
| |
| final ArrayOf<Runner> runners = model.search(query); |
| if (runners == null) { |
| hideSearchResults(); |
| } else if (runners.length() == 1) { |
| hideSearchResults(); |
| marker.update(runners.get(0)); |
| } else { |
| showSearchResults(runners); |
| } |
| } |
| } |