From ff15031113d6326d1ae1dedd9ef0da0c17f0d015 Mon Sep 17 00:00:00 2001 From: Rodrigo Damazio Date: Fri, 6 Jan 2012 03:25:51 -0200 Subject: [PATCH] Breaking down SearchActivity into display and search logic pieces. --- .../android/apps/mytracks/SearchActivity.java | 251 +--------------- .../apps/mytracks/content/SearchEngine.java | 277 ++++++++++++++++++ 2 files changed, 285 insertions(+), 243 deletions(-) create mode 100644 MyTracks/src/com/google/android/apps/mytracks/content/SearchEngine.java diff --git a/MyTracks/src/com/google/android/apps/mytracks/SearchActivity.java b/MyTracks/src/com/google/android/apps/mytracks/SearchActivity.java index 05e37d90d..59df27b4a 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/SearchActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/SearchActivity.java @@ -17,19 +17,19 @@ package com.google.android.apps.mytracks; import static com.google.android.apps.mytracks.Constants.TAG; -import com.google.android.apps.mytracks.content.MyTracksProviderUtils; +import com.google.android.apps.mytracks.content.SearchEngine; import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.TracksColumns; import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.content.WaypointsColumns; -import com.google.android.apps.mytracks.stats.TripStatistics; +import com.google.android.apps.mytracks.content.SearchEngine.ScoredResult; +import com.google.android.apps.mytracks.content.SearchEngine.SearchQuery; import com.google.android.maps.mytracks.R; import android.app.ListActivity; import android.app.SearchManager; import android.content.ContentUris; import android.content.Intent; -import android.database.Cursor; import android.location.Location; import android.location.LocationManager; import android.net.Uri; @@ -41,11 +41,10 @@ import android.widget.SimpleAdapter; import java.util.ArrayList; import java.util.Collection; -import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.TreeSet; +import java.util.SortedSet; /** * Activity to search for tracks or waypoints. @@ -54,82 +53,18 @@ import java.util.TreeSet; */ public class SearchActivity extends ListActivity { - private static final double TRACK_CATEGORY_PROMOTION = 2.0; - - private static final double TRACK_DESCRIPTION_PROMOTION = 8.0; - - private static final double TRACK_NAME_PROMOTION = 16.0; - - private static final double CURRENT_TRACK_WAYPOINT_PROMOTION = 2.0; - - private static final double CURRENT_TRACK_DEMOTION = 0.5; - private static final String EXTRA_CURRENT_TRACK_ID = "trackId"; - /** Maximum number of waypoints which will be retrieved and scored. */ - private static final int MAX_SCORED_WAYPOINTS = 100; - - private static final Comparator SCORED_RESULT_COMPARATOR = - new Comparator() { - @Override - public int compare(ScoredResult r1, ScoredResult r2) { - // Score ordering. - int scoreDiff = (int) (r2.score - r1.score); - if (scoreDiff != 0) { - return scoreDiff; - } - - // Arbitrary ordering, by ID. - long id1 = r1.track != null ? r1.track.getId() : r1.waypoint.getId(); - long id2 = r2.track != null ? r2.track.getId() : r2.waypoint.getId(); - return (int) (id2 - id1); - } - }; - private static final boolean LOG_SCORES = true; - private MyTracksProviderUtils providerUtils; - + private SearchEngine engine; private LocationManager locationManager; - private static class SearchQuery { - public SearchQuery(String textQuery, Location currentLocation, long currentTrackId, - long currentTimestamp) { - this.textQuery = textQuery; - this.currentLocation = currentLocation; - this.currentTrackId = currentTrackId; - this.currentTimestamp = currentTimestamp; - } - - final String textQuery; - final Location currentLocation; - final long currentTrackId; - final long currentTimestamp; - } - - private static class ScoredResult { - ScoredResult(Track track, double score) { - this.track = track; - this.waypoint = null; - this.score = score; - } - - ScoredResult(Waypoint waypoint, double score) { - this.track = null; - this.waypoint = waypoint; - this.score = score; - } - - final Track track; - final Waypoint waypoint; - final double score; - } - @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - providerUtils = MyTracksProviderUtils.Factory.get(this); + engine = new SearchEngine(this); locationManager = (LocationManager) getSystemService(LOCATION_SERVICE); handleIntent(getIntent()); @@ -166,15 +101,7 @@ public class SearchActivity extends ListActivity { } private void doSearch(SearchQuery query) { - ArrayList tracks = new ArrayList(); - ArrayList waypoints = new ArrayList(); - TreeSet scoredResults = new TreeSet(SCORED_RESULT_COMPARATOR); - - retrieveTracks(query, tracks); - retrieveWaypoints(query, waypoints); - - scoreTrackResults(tracks, query, scoredResults); - scoreWaypointResults(waypoints, query, scoredResults); + SortedSet scoredResults = engine.doSearch(query); final List> displayResults = prepareResultsforDisplay(scoredResults); @@ -182,6 +109,7 @@ public class SearchActivity extends ListActivity { Log.i(TAG, "Search scores: " + displayResults); } + // Then go back to the UI thread to display them. runOnUiThread(new Runnable() { @Override public void run() { @@ -190,165 +118,6 @@ public class SearchActivity extends ListActivity { }); } - private void retrieveTracks(SearchQuery query, ArrayList tracks) { - // TODO: Use a different sort order as a ranking function. - String queryLikeSelection = "%" + query.textQuery + "%"; - String trackSelection = - TracksColumns.NAME + " LIKE ? OR " + - TracksColumns.DESCRIPTION + " LIKE ? OR " + - TracksColumns.CATEGORY + " LIKE ?"; - String[] trackSelectionArgs = new String[] { - queryLikeSelection, - queryLikeSelection, - queryLikeSelection }; - - // TODO: Limit number of results to be scored. - Cursor tracksCursor = providerUtils.getTracksCursor(trackSelection, trackSelectionArgs); - if (tracksCursor != null) { - try { - tracks.ensureCapacity(tracksCursor.getCount()); - - while (tracksCursor.moveToNext()) { - tracks.add(providerUtils.createTrack(tracksCursor)); - } - } finally { - tracksCursor.close(); - } - } - } - - private void retrieveWaypoints(SearchQuery query, ArrayList waypoints) { - // TODO: Use a different sort order as a ranking function. - String queryLikeSelection2 = "%" + query.textQuery + "%"; - String waypointSelection = - WaypointsColumns.NAME + " LIKE ? OR " + - WaypointsColumns.DESCRIPTION + " LIKE ? OR " + - WaypointsColumns.CATEGORY + " LIKE ?"; - String[] waypointSelectionArgs = new String[] { - queryLikeSelection2, - queryLikeSelection2, - queryLikeSelection2 }; - Cursor waypointsCursor = providerUtils.getWaypointsCursor(waypointSelection, waypointSelectionArgs, MAX_SCORED_WAYPOINTS); - if (waypointsCursor != null) { - try { - waypoints.ensureCapacity(waypointsCursor.getCount()); - - while (waypointsCursor.moveToNext()) { - waypoints.add(providerUtils.createWaypoint(waypointsCursor)); - } - } finally { - waypointsCursor.close(); - } - } - } - - private void scoreTrackResults(Collection tracks, SearchQuery query, Collection output) { - for (Track track : tracks) { - // Calculate the score. - double score = scoreTrackResult(query, track); - - // Add to the output. - output.add(new ScoredResult(track, score)); - } - } - - private double scoreTrackResult(SearchQuery query, Track track) { - double score = 1.0; - - score *= getTitleBoost(query, track.getName(), track.getDescription(), track.getCategory()); - - TripStatistics statistics = track.getStatistics(); - double meanLatitude = (statistics.getTop() + statistics.getBottom()) / 2000000.0; - double meanLongitude = (statistics.getRight() + statistics.getLeft()) / 2000000.0; - score *= getDistanceBoost(query, meanLatitude, meanLongitude); - - long meanTimestamp = (statistics.getStartTime() + statistics.getStopTime()) / 2L; - score *= getTimeBoost(query, meanTimestamp); - - // Score the currently-selected track lower (user is already there, wouldn't be searching for it). - if (track.getId() == query.currentTrackId) { - score *= CURRENT_TRACK_DEMOTION; - } - - return score; - } - - private void scoreWaypointResults(Collection waypoints, SearchQuery query, Collection output) { - for (Waypoint waypoint : waypoints) { - // Calculate the score. - double score = scoreWaypointResult(query, waypoint); - - // Add to the output. - output.add(new ScoredResult(waypoint, score)); - } - } - - private double scoreWaypointResult(SearchQuery query, Waypoint waypoint) { - double score = 1.0; - - Location location = waypoint.getLocation(); - score *= getTitleBoost(query, waypoint.getName(), waypoint.getDescription(), waypoint.getCategory()); - score *= getDistanceBoost(query, location.getLatitude(), location.getLongitude()); - score *= getTimeBoost(query, location.getTime()); - - // Score waypoints in the currently-selected track higher (searching inside the current track). - if (waypoint.getTrackId() == query.currentTrackId) { - score *= CURRENT_TRACK_WAYPOINT_PROMOTION; - } - - return score; - } - - private double getTitleBoost(SearchQuery query, - String name, String description, String category) { - // Title boost: track name > description > category. - double boost = 1.0; - if (name.toLowerCase().contains(query.textQuery)) { - boost *= TRACK_NAME_PROMOTION; - } - if (description.toLowerCase().contains(query.textQuery)) { - boost *= TRACK_DESCRIPTION_PROMOTION; - } - if (category.toLowerCase().contains(query.textQuery)) { - boost *= TRACK_CATEGORY_PROMOTION; - } - return boost; - } - - private double getTimeBoost(SearchQuery query, long timestamp) { - // Score recent tracks higher. - long timeAgoHours = (query.currentTimestamp - timestamp) / (60L * 60L); - if (timeAgoHours > 0L) { - return squash(timeAgoHours); - } else { - // Should rarely happen (track recorded in the last hour). - return Double.POSITIVE_INFINITY; - } - } - - private double getDistanceBoost(SearchQuery query, double latitude, double longitude) { - if (query.currentLocation == null) { - return 1.0; - } - - float[] distanceResults = new float[1]; - - Location.distanceBetween( - latitude, longitude, - query.currentLocation.getLatitude(), query.currentLocation.getLongitude(), - distanceResults); - - // Score tracks close to the current location higher. - double distanceKm = distanceResults[0] / 1000.0; - if (distanceKm > 0.0) { - // Use the inverse of the amortized distance. - return squash(distanceKm); - } else { - // Should rarely happen (distance is exactly 0). - return Double.POSITIVE_INFINITY; - } - } - private List> prepareResultsforDisplay( Collection scoredResults) { ArrayList> output = new ArrayList>(scoredResults.size()); @@ -434,8 +203,4 @@ public class SearchActivity extends ListActivity { } startActivity(intent); } - - private static double squash(double timeAgoHours) { - return 1.0 / Math.log1p(timeAgoHours); - } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/SearchEngine.java b/MyTracks/src/com/google/android/apps/mytracks/content/SearchEngine.java new file mode 100644 index 000000000..ecd9e9789 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/content/SearchEngine.java @@ -0,0 +1,277 @@ +/* + * Copyright 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.android.apps.mytracks.content; + +import com.google.android.apps.mytracks.stats.TripStatistics; + +import android.content.Context; +import android.database.Cursor; +import android.location.Location; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.SortedSet; +import java.util.TreeSet; + +public class SearchEngine { + + private static final double TRACK_CATEGORY_PROMOTION = 2.0; + + private static final double TRACK_DESCRIPTION_PROMOTION = 8.0; + + private static final double TRACK_NAME_PROMOTION = 16.0; + + private static final double CURRENT_TRACK_WAYPOINT_PROMOTION = 2.0; + + private static final double CURRENT_TRACK_DEMOTION = 0.5; + + /** Maximum number of waypoints which will be retrieved and scored. */ + private static final int MAX_SCORED_WAYPOINTS = 100; + + public static class SearchQuery { + public SearchQuery(String textQuery, Location currentLocation, long currentTrackId, + long currentTimestamp) { + this.textQuery = textQuery; + this.currentLocation = currentLocation; + this.currentTrackId = currentTrackId; + this.currentTimestamp = currentTimestamp; + } + + final String textQuery; + final Location currentLocation; + final long currentTrackId; + final long currentTimestamp; + } + + public static class ScoredResult { + ScoredResult(Track track, double score) { + this.track = track; + this.waypoint = null; + this.score = score; + } + + ScoredResult(Waypoint waypoint, double score) { + this.track = null; + this.waypoint = waypoint; + this.score = score; + } + + final Track track; + final Waypoint waypoint; + final double score; + } + + private static final Comparator SCORED_RESULT_COMPARATOR = + new Comparator() { + @Override + public int compare(ScoredResult r1, ScoredResult r2) { + // Score ordering. + int scoreDiff = (int) (r2.score - r1.score); + if (scoreDiff != 0) { + return scoreDiff; + } + + // Arbitrary ordering, by ID. + long id1 = r1.track != null ? r1.track.getId() : r1.waypoint.getId(); + long id2 = r2.track != null ? r2.track.getId() : r2.waypoint.getId(); + return (int) (id2 - id1); + } + }; + + private final MyTracksProviderUtils providerUtils; + + public SearchEngine(Context ctx) { + providerUtils = MyTracksProviderUtils.Factory.get(ctx); + } + + public SortedSet doSearch(SearchQuery query) { + ArrayList tracks = new ArrayList(); + ArrayList waypoints = new ArrayList(); + TreeSet scoredResults = new TreeSet(SCORED_RESULT_COMPARATOR); + + retrieveTracks(query, tracks); + retrieveWaypoints(query, waypoints); + + scoreTrackResults(tracks, query, scoredResults); + scoreWaypointResults(waypoints, query, scoredResults); + + return scoredResults; + } + + private void retrieveTracks(SearchQuery query, ArrayList tracks) { + // TODO: Use a different sort order as a ranking function. + String queryLikeSelection = "%" + query.textQuery + "%"; + String trackSelection = + TracksColumns.NAME + " LIKE ? OR " + + TracksColumns.DESCRIPTION + " LIKE ? OR " + + TracksColumns.CATEGORY + " LIKE ?"; + String[] trackSelectionArgs = new String[] { + queryLikeSelection, + queryLikeSelection, + queryLikeSelection }; + + // TODO: Limit number of results to be scored. + Cursor tracksCursor = providerUtils.getTracksCursor(trackSelection, trackSelectionArgs); + if (tracksCursor != null) { + try { + tracks.ensureCapacity(tracksCursor.getCount()); + + while (tracksCursor.moveToNext()) { + tracks.add(providerUtils.createTrack(tracksCursor)); + } + } finally { + tracksCursor.close(); + } + } + } + + private void retrieveWaypoints(SearchQuery query, ArrayList waypoints) { + // TODO: Use a different sort order as a ranking function. + String queryLikeSelection2 = "%" + query.textQuery + "%"; + String waypointSelection = + WaypointsColumns.NAME + " LIKE ? OR " + + WaypointsColumns.DESCRIPTION + " LIKE ? OR " + + WaypointsColumns.CATEGORY + " LIKE ?"; + String[] waypointSelectionArgs = new String[] { + queryLikeSelection2, + queryLikeSelection2, + queryLikeSelection2 }; + Cursor waypointsCursor = providerUtils.getWaypointsCursor(waypointSelection, waypointSelectionArgs, MAX_SCORED_WAYPOINTS); + if (waypointsCursor != null) { + try { + waypoints.ensureCapacity(waypointsCursor.getCount()); + + while (waypointsCursor.moveToNext()) { + waypoints.add(providerUtils.createWaypoint(waypointsCursor)); + } + } finally { + waypointsCursor.close(); + } + } + } + + private void scoreTrackResults(Collection tracks, SearchQuery query, Collection output) { + for (Track track : tracks) { + // Calculate the score. + double score = scoreTrackResult(query, track); + + // Add to the output. + output.add(new ScoredResult(track, score)); + } + } + + private double scoreTrackResult(SearchQuery query, Track track) { + double score = 1.0; + + score *= getTitleBoost(query, track.getName(), track.getDescription(), track.getCategory()); + + TripStatistics statistics = track.getStatistics(); + double meanLatitude = (statistics.getTop() + statistics.getBottom()) / 2000000.0; + double meanLongitude = (statistics.getRight() + statistics.getLeft()) / 2000000.0; + score *= getDistanceBoost(query, meanLatitude, meanLongitude); + + long meanTimestamp = (statistics.getStartTime() + statistics.getStopTime()) / 2L; + score *= getTimeBoost(query, meanTimestamp); + + // Score the currently-selected track lower (user is already there, wouldn't be searching for it). + if (track.getId() == query.currentTrackId) { + score *= CURRENT_TRACK_DEMOTION; + } + + return score; + } + + private void scoreWaypointResults(Collection waypoints, SearchQuery query, Collection output) { + for (Waypoint waypoint : waypoints) { + // Calculate the score. + double score = scoreWaypointResult(query, waypoint); + + // Add to the output. + output.add(new ScoredResult(waypoint, score)); + } + } + + private double scoreWaypointResult(SearchQuery query, Waypoint waypoint) { + double score = 1.0; + + Location location = waypoint.getLocation(); + score *= getTitleBoost(query, waypoint.getName(), waypoint.getDescription(), waypoint.getCategory()); + score *= getDistanceBoost(query, location.getLatitude(), location.getLongitude()); + score *= getTimeBoost(query, location.getTime()); + + // Score waypoints in the currently-selected track higher (searching inside the current track). + if (waypoint.getTrackId() == query.currentTrackId) { + score *= CURRENT_TRACK_WAYPOINT_PROMOTION; + } + + return score; + } + + private double getTitleBoost(SearchQuery query, + String name, String description, String category) { + // Title boost: track name > description > category. + double boost = 1.0; + if (name.toLowerCase().contains(query.textQuery)) { + boost *= TRACK_NAME_PROMOTION; + } + if (description.toLowerCase().contains(query.textQuery)) { + boost *= TRACK_DESCRIPTION_PROMOTION; + } + if (category.toLowerCase().contains(query.textQuery)) { + boost *= TRACK_CATEGORY_PROMOTION; + } + return boost; + } + + private double getTimeBoost(SearchQuery query, long timestamp) { + // Score recent tracks higher. + long timeAgoHours = (query.currentTimestamp - timestamp) / (60L * 60L); + if (timeAgoHours > 0L) { + return squash(timeAgoHours); + } else { + // Should rarely happen (track recorded in the last hour). + return Double.POSITIVE_INFINITY; + } + } + + private double getDistanceBoost(SearchQuery query, double latitude, double longitude) { + if (query.currentLocation == null) { + return 1.0; + } + + float[] distanceResults = new float[1]; + + Location.distanceBetween( + latitude, longitude, + query.currentLocation.getLatitude(), query.currentLocation.getLongitude(), + distanceResults); + + // Score tracks close to the current location higher. + double distanceKm = distanceResults[0] / 1000.0; + if (distanceKm > 0.0) { + // Use the inverse of the amortized distance. + return squash(distanceKm); + } else { + // Should rarely happen (distance is exactly 0). + return Double.POSITIVE_INFINITY; + } + } + + private static double squash(double timeAgoHours) { + return 1.0 / Math.log1p(timeAgoHours); + } +}