// 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; } }