| // Copyright 2010 Google Inc. All Rights Reserved. |
| |
| package com.google.silvercomet.client; |
| |
| import com.google.gwt.xhr.client.XMLHttpRequest; |
| |
| import elemental.client.Browser; |
| import elemental.html.Window; |
| import elemental.util.ArrayOf; |
| import elemental.util.ArrayOfInt; |
| import elemental.util.ArrayOfString; |
| import elemental.util.MapFromStringTo; |
| import elemental.json.Json; |
| import elemental.js.util.StringUtil; |
| import elemental.js.util.Xhr; |
| import elemental.util.CanCompare; |
| import elemental.dom.TimeoutHandler; |
| import elemental.util.Collections; |
| |
| /** |
| * A very simple data model for the application. |
| * |
| * @author knorton@google.com (Kelly Norton) |
| */ |
| public class Model implements Xhr.Callback { |
| |
| /** |
| * A listener to receive callbacks on model events. |
| */ |
| public interface Listener { |
| void modelDidFailLoading(Model model); |
| |
| void modelDidFinishBuildingIndex(Model model); |
| |
| void modelDidFinishLoading(Model model); |
| } |
| |
| private static final String DATA_URL = "results.json"; |
| |
| public static final int SECONDS_PER_HISTOGRAM_BUCKET = 600; |
| |
| /** |
| * Compute a histogram with number of finishers per bucket of time where the |
| * size of the bucket is indicated by <code>seconds</code>. |
| */ |
| private static ArrayOfInt computeHistogram(ArrayOf<Runner> runners, int seconds) { |
| final ArrayOfInt hist = Collections.arrayOfInt(); |
| |
| for (int i = 0, n = runners.length(); i < n; ++i) { |
| int index = runners.get(i).time() / seconds; |
| hist.set(index, hist.isSet(index) ? hist.get(index) + 1 : 1); |
| } |
| |
| int sum = 0; |
| for (int i = 0, n = hist.length(); i < n; ++i) { |
| if (hist.isSet(i)) { |
| sum += hist.get(i); |
| } |
| hist.set(i, sum); |
| } |
| |
| return hist; |
| } |
| |
| /** |
| * Sorts the list of runners by {@link Runner#time()} and updates their places |
| * accordingly. |
| */ |
| private static ArrayOf<Runner> normalize(ArrayOf<Runner> runners) { |
| // Sort by time() which is based on bib time. |
| runners.sort(new CanCompare<Runner>() { |
| @Override |
| public int compare(Runner a, Runner b) { |
| return a.time() - b.time(); |
| } |
| }); |
| |
| // Update the runner's new place. |
| for (int i = 0, n = runners.length(); i < n; ++i) { |
| runners.get(i).setPlace(i + 1); |
| } |
| |
| return runners; |
| } |
| |
| /** |
| * Update the model's index with all possible prefixes of the search key. |
| */ |
| private static void updateIndexForAllPrefixes( |
| MapFromStringTo<ArrayOf<Runner>> index, String key, Runner runner) { |
| assert key.length() > 0 : "key.length must be > 0."; |
| |
| for (int i = 1, n = key.length(); i <= n; ++i) { |
| final String prefix = key.substring(0, i); |
| if (!index.hasKey(prefix)) { |
| index.put(prefix, Collections.<Runner>arrayOf()); |
| } |
| |
| final ArrayOf<Runner> values = index.get(prefix); |
| // Do not add the same runner twice. |
| if (values.get(values.length() - 1) != runner) { |
| index.get(prefix).push(runner); |
| } |
| } |
| } |
| |
| private final Listener listener; |
| |
| private ArrayOfInt histogram = null; |
| |
| private MapFromStringTo<ArrayOf<Runner>> index = Collections.mapFromStringTo(); |
| |
| /** |
| * Create a new model. |
| */ |
| public Model(Listener listener) { |
| this.listener = listener; |
| } |
| |
| /** |
| * Get a reference to the models histogram. |
| */ |
| public ArrayOfInt histogram() { |
| return histogram; |
| } |
| |
| /** |
| * Load the remote data into the model. |
| */ |
| public void load() { |
| Xhr.get(DATA_URL, this); |
| } |
| |
| /** |
| * Called if the XHR fails to load data from there server. |
| */ |
| @Override |
| public void onFail(XMLHttpRequest xhr) { |
| listener.modelDidFailLoading(this); |
| } |
| |
| /** |
| * Called when XHR successfully loads data from the server. |
| */ |
| @Override |
| public void onSuccess(XMLHttpRequest xhr) { |
| update((ArrayOf<Runner>)Json.parse(xhr.getResponseText())); |
| listener.modelDidFinishLoading(this); |
| } |
| |
| /** |
| * Performs a search and returns the list of runners that match. |
| */ |
| public ArrayOf<Runner> search(String query) { |
| return index.get(query); |
| } |
| |
| /** |
| * Update the model's internal data from an list of runners coming from the |
| * server. |
| */ |
| private void update(ArrayOf<Runner> data) { |
| // Sort & mutate source data. |
| final ArrayOf<Runner> runners = normalize(data); |
| |
| // Update indexes later. |
| Browser.getWindow().setTimeout(new TimeoutHandler() { |
| @Override |
| public void onTimeoutHandler() { |
| for (int i = 0, n = runners.length(); i < n; ++i) { |
| final Runner runner = runners.get(i); |
| |
| final String name = runner.name().toLowerCase(); |
| updateIndexForAllPrefixes(index, name, runner); |
| final ArrayOfString words = StringUtil.split(name, " "); |
| for (int j = 0, m = words.length(); j < m; ++j) { |
| updateIndexForAllPrefixes(index, words.get(j), runner); |
| } |
| |
| updateIndexForAllPrefixes(index, "" + runner.place(), runner); |
| updateIndexForAllPrefixes(index, "" + runner.bibNumber(), runner); |
| } |
| listener.modelDidFinishBuildingIndex(Model.this); |
| } |
| }, 0); |
| |
| // Compute histogram. |
| final ArrayOfInt histogram = computeHistogram(runners, SECONDS_PER_HISTOGRAM_BUCKET); |
| |
| // Update fields. |
| this.histogram = histogram; |
| } |
| } |