Implement incremental search

This commit is contained in:
Victor Shcherb 2011-09-16 01:07:10 +02:00
parent 890277dd4b
commit 4d56ca74de
11 changed files with 218 additions and 139 deletions

View file

@ -12,4 +12,9 @@ public interface ResultMatcher<T> {
*/
boolean publish(T object);
/**
* @returns true to stop processing
*/
boolean isCancelled();
}

View file

@ -104,6 +104,9 @@ public class BinaryMapAddressReaderAdapter {
}
}
codedIS.popLimit(oldLimit);
if(resultMatcher != null && resultMatcher.isCancelled()){
codedIS.skipRawBytes(codedIS.getBytesUntilLimit());
}
break;
default:
skipUnknownField(t);
@ -441,7 +444,7 @@ public class BinaryMapAddressReaderAdapter {
}
// do not preload streets in city
protected LatLon findIntersectedStreets(City c, Street s, Street s2, List<Street> streets) throws IOException {
protected LatLon findIntersectedStreets(City c, Street s, Street s2, List<Street> streets) throws IOException {
if(s.getIndexInCity() == -1){
return null;
}
@ -493,6 +496,9 @@ public class BinaryMapAddressReaderAdapter {
}
}
codedIS.popLimit(oldLimit);
if(resultMatcher != null && resultMatcher.isCancelled()){
codedIS.skipRawBytes(codedIS.getBytesUntilLimit());
}
break;
default:
skipUnknownField(t);

View file

@ -1,5 +1,6 @@
package net.osmand.plus;
import java.text.Collator;
import net.osmand.StringMatcher;
@ -10,18 +11,50 @@ import net.osmand.StringMatcher;
*
* @author pavol.zibrita
*/
public abstract class CollatorStringMatcher implements StringMatcher {
public class CollatorStringMatcher implements StringMatcher {
private final Collator collator;
private final StringMatcherMode mode;
private final String part;
public CollatorStringMatcher(Collator collator) {
public enum StringMatcherMode {
CHECK_ONLY_STARTS_WITH,
CHECK_STARTS_FROM_SPACE,
CHECK_STARTS_FROM_SPACE_NOT_BEGINNING,
CHECK_CONTAINS
}
public CollatorStringMatcher(Collator collator, String part, StringMatcherMode mode) {
this.collator = collator;
this.part = part;
this.mode = mode;
}
public Collator getCollator() {
return collator;
}
@Override
public boolean matches(String name) {
return cmatches(collator, name, part, mode);
}
public static boolean cmatches(Collator collator, String base, String part, StringMatcherMode mode){
switch (mode) {
case CHECK_CONTAINS:
return ccontains(collator, base, part);
case CHECK_STARTS_FROM_SPACE:
return cstartsWith(collator, base, part, true, true);
case CHECK_STARTS_FROM_SPACE_NOT_BEGINNING:
return cstartsWith(collator, base, part, false, true);
case CHECK_ONLY_STARTS_WITH:
return cstartsWith(collator, base, part, true, false);
}
return false;
}
/**
* Check if part contains in base
*
@ -30,7 +63,7 @@ public abstract class CollatorStringMatcher implements StringMatcher {
* @param base String where to search
* @return true if part is contained in base
*/
public static boolean ccontains(Collator collator, String part, String base) {
public static boolean ccontains(Collator collator, String base, String part) {
int pos = 0;
if (part.length() > 3) {
// improve searching by searching first 3 characters
@ -56,17 +89,40 @@ public abstract class CollatorStringMatcher implements StringMatcher {
}
/**
* Checks if string starts with another string
* Checks if string starts with another string.
* Special check try to find as well in the middle of name
*
* @param collator
* @param searchIn
* @param theStart
* @return true if searchIn starts with token
*/
public static boolean cstartsWith(Collator collator, String searchIn, String theStart) {
public static boolean cstartsWith(Collator collator, String searchIn, String theStart,
boolean checkBeginning, boolean checkSpaces) {
int startLength = theStart.length();
int searchInLength = searchIn.length();
if (startLength == 0) {
return true;
}
if (startLength > searchInLength) {
return false;
}
// simulate starts with for collator
return collator.equals(
searchIn.substring(0,
Math.min(searchIn.length(), theStart.length())), theStart);
if (checkBeginning) {
boolean starts = collator.equals(searchIn.substring(0, startLength), theStart);
if (starts) {
return true;
}
}
if (checkSpaces) {
for (int i = 1; i <= searchInLength - startLength; i++) {
if (Character.isSpace(searchIn.charAt(i - 1)) && !Character.isSpace(searchIn.charAt(i))) {
if (collator.equals(searchIn.substring(i, i + startLength), theStart)) {
return true;
}
}
}
}
return false;
}
}

View file

@ -1,29 +0,0 @@
package net.osmand.plus;
import java.text.Collator;
/**
* This simple contains string matcher uses collator to check,
* if the part is contained in matched string.
*
* @author pavol.zibrita
*/
public class ContainsStringMatcher extends CollatorStringMatcher {
private final String part;
/**
* @param part Search this string in matched base string, see {@link #matches(String)}
* @param collator Collator to use
*/
public ContainsStringMatcher(String part, Collator collator) {
super(collator);
this.part = part;
}
@Override
public boolean matches(String base) {
return ccontains(getCollator(), part, base);
}
}

View file

@ -1,13 +1,11 @@
package net.osmand.plus;
import static net.osmand.plus.CollatorStringMatcher.ccontains;
import static net.osmand.plus.CollatorStringMatcher.cstartsWith;
import static net.osmand.plus.CollatorStringMatcher.cmatches;
import java.io.IOException;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@ -23,6 +21,7 @@ import net.osmand.data.MapObject;
import net.osmand.data.PostCode;
import net.osmand.data.Street;
import net.osmand.osm.LatLon;
import net.osmand.plus.CollatorStringMatcher.StringMatcherMode;
import org.apache.commons.logging.Log;
@ -61,13 +60,13 @@ public class RegionAddressRepositoryBinary implements RegionAddressRepository {
}
preloadBuildings(street, null);
name = name.toLowerCase();
int ind = 0;
for (Building building : street.getBuildings()) {
if(resultMatcher.isCancelled()){
return buildingsToFill;
}
String bName = useEnglishNames ? building.getEnName() : building.getName(); //lower case not needed, collator ensures that
if (cstartsWith(collator,bName,name)) {
buildingsToFill.add(ind, building);
ind++;
} else if (ccontains(collator,name,bName)) {
if (cmatches(collator, bName, name, StringMatcherMode.CHECK_ONLY_STARTS_WITH)) {
resultMatcher.publish(building);
buildingsToFill.add(building);
}
}
@ -87,6 +86,11 @@ public class RegionAddressRepositoryBinary implements RegionAddressRepository {
}
// not use ccontains It is really slow, takes about 10 times more than other steps
private StringMatcherMode[] streetsCheckMode = new StringMatcherMode[] {StringMatcherMode.CHECK_ONLY_STARTS_WITH,
StringMatcherMode.CHECK_STARTS_FROM_SPACE_NOT_BEGINNING};
@Override
public List<Street> fillWithSuggestedStreets(MapObject o, ResultMatcher<Street> resultMatcher, String... names) {
assert o instanceof PostCode || o instanceof City;
@ -99,18 +103,22 @@ public class RegionAddressRepositoryBinary implements RegionAddressRepository {
return streetsToFill;
}
preloadStreets(o, null);
int ind = 0;
Collection<Street> streets = post == null ? city.getStreets() : post.getStreets() ;
Iterator<Street> iterator = streets.iterator();
while(iterator.hasNext()) {
Street s = iterator.next();
String sName = useEnglishNames ? s.getEnName() : s.getName(); // lower case not needed, collator ensures that
for (String name : names) {
if (cstartsWith(collator, sName, name)) {
streetsToFill.add(ind, s);
ind++;
} else if (ccontains(collator, name, sName) || ccontains(collator, sName, name)) {
streetsToFill.add(s);
Collection<Street> streets = post == null ? city.getStreets() : post.getStreets();
// 1st step loading by starts with
for (StringMatcherMode mode : streetsCheckMode) {
for (Street s : streets) {
if (resultMatcher.isCancelled()) {
return streetsToFill;
}
String sName = s.getName(useEnglishNames); // lower case not needed, collator ensures that
for (String name : names) {
boolean match = CollatorStringMatcher.cmatches(collator, sName, name, mode);
if (match) {
resultMatcher.publish(s);
streetsToFill.add(s);
}
}
}
}
@ -143,72 +151,55 @@ public class RegionAddressRepositoryBinary implements RegionAddressRepository {
preloadCities(resultMatcher);
citiesToFill.addAll(cities.values());
return citiesToFill;
} else {
preloadCities(null);
}
preloadCities(null);
if (name.length() == 0) {
citiesToFill.addAll(cities.values());
return citiesToFill;
}
try {
// essentially index is created that cities towns are first in cities map
int ind = 0;
if (name.length() >= 2 && Algoritms.containsDigit(name)) {
// also try to identify postcodes
String uName = name.toUpperCase();
for (PostCode code : file.getPostcodes(region, resultMatcher, new ContainsStringMatcher(uName, collator))) {
if (cstartsWith(collator, code.getName(), uName)) {
citiesToFill.add(ind++, code);
} else if (ccontains(collator, code.getName(), uName)) {
citiesToFill.add(code);
for (PostCode code : file.getPostcodes(region, resultMatcher, new CollatorStringMatcher(collator, uName,
StringMatcherMode.CHECK_CONTAINS))) {
citiesToFill.add(code);
if (resultMatcher.isCancelled()) {
return citiesToFill;
}
}
}
if (name.length() < 3) {
if (name.length() == 0) {
citiesToFill.addAll(cities.values());
} else {
name = name.toLowerCase();
for (City c : cities.values()) {
String cName = useEnglishNames ? c.getEnName() : c.getName(); //lower case not needed, collator ensures that
if (cstartsWith(collator, cName, name)) {
if (resultMatcher.publish(c)) {
citiesToFill.add(c);
}
}
}
}
} else {
name = name.toLowerCase();
for (City c : cities.values()) {
String cName = useEnglishNames ? c.getEnName() : c.getName(); //lower case not needed, collator ensures that
if (cstartsWith(collator,cName,name)) {
if (resultMatcher.publish(c)) {
citiesToFill.add(ind, c);
ind++;
}
} else if (ccontains(collator,name,cName)) {
if (resultMatcher.publish(c)) {
citiesToFill.add(c);
}
}
}
int initialsize = citiesToFill.size();
for(City c : file.getVillages(region, resultMatcher, new ContainsStringMatcher(name,collator), useEnglishNames )){
String cName = c.getName(useEnglishNames); //lower case not needed, collator ensures that
if (cstartsWith(collator,cName,name)) {
citiesToFill.add(ind, c);
ind++;
} else if (ccontains(collator,name, cName)) {
name = name.toLowerCase();
for (City c : cities.values()) {
String cName = c.getName(useEnglishNames); // lower case not needed, collator ensures that
if (cmatches(collator, cName, name, StringMatcherMode.CHECK_STARTS_FROM_SPACE)) {
if (resultMatcher.publish(c)) {
citiesToFill.add(c);
}
}
log.debug("Loaded citites " + (citiesToFill.size() - initialsize)); //$NON-NLS-1$
if (resultMatcher.isCancelled()) {
return citiesToFill;
}
}
int initialsize = citiesToFill.size();
if (name.length() >= 3) {
for (City c : file.getVillages(region, resultMatcher, new CollatorStringMatcher(collator, name,
StringMatcherMode.CHECK_STARTS_FROM_SPACE), useEnglishNames)) {
citiesToFill.add(c);
if (resultMatcher.isCancelled()) {
return citiesToFill;
}
}
}
log.debug("Loaded citites " + (citiesToFill.size() - initialsize)); //$NON-NLS-1$
} catch (IOException e) {
log.error("Disk operation failed" , e); //$NON-NLS-1$
log.error("Disk operation failed", e); //$NON-NLS-1$
}
return citiesToFill;
}
@Override
@ -255,6 +246,11 @@ public class RegionAddressRepositoryBinary implements RegionAddressRepository {
return region;
}
@Override
public String toString() {
return getName() + " repository";
}
@Override
public boolean useEnglishNames() {
return useEnglishNames;
@ -356,4 +352,6 @@ public class RegionAddressRepositoryBinary implements RegionAddressRepository {
return file.getRegionCenter(region);
}
}

View file

@ -28,7 +28,7 @@ public class SearchBuildingByNameActivity extends SearchByNameAbstractActivity<B
protected void onPostExecute(Void result) {
((TextView)findViewById(R.id.Label)).setText(R.string.incremental_search_building);
progress.setVisibility(View.INVISIBLE);
resetText();
updateSearchText();
}
@Override
@ -38,6 +38,7 @@ public class SearchBuildingByNameActivity extends SearchByNameAbstractActivity<B
}
@Override
protected Void doInBackground(Object... params) {
region = ((OsmandApplication)getApplication()).getResourceManager().getRegionRepository(settings.getLastSearchedRegion());
if(region != null){
postcode = region.getPostcode(settings.getLastSearchedPostcode());
city = region.getCityById(settings.getLastSearchedCity());
@ -47,7 +48,11 @@ public class SearchBuildingByNameActivity extends SearchByNameAbstractActivity<B
street = region.getStreetByName(city, settings.getLastSearchedStreet());
}
}
region = ((OsmandApplication)getApplication()).getResourceManager().getRegionRepository(settings.getLastSearchedRegion());
if(street != null){
// preload here to avoid concurrent modification
region.fillWithSuggestedBuildings(postcode, street, "", null);
}
return null;
}
};
@ -62,6 +67,10 @@ public class SearchBuildingByNameActivity extends SearchByNameAbstractActivity<B
task.progress(object);
return true;
}
@Override
public boolean isCancelled() {
return task.isCancelled();
}
});
}
return new ArrayList<Building>();

View file

@ -41,17 +41,19 @@ public abstract class SearchByNameAbstractActivity<T> extends ListActivity {
super.onCreate(savedInstanceState);
settings = OsmandSettings.getOsmandSettings(this);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.search_by_name);
initializeTask = getInitializeTask();
NamesAdapter namesAdapter = new NamesAdapter(new ArrayList<T>()); //$NON-NLS-1$
setListAdapter(namesAdapter);
progress = (ProgressBar) findViewById(R.id.ProgressBar);
searchText = (EditText) findViewById(R.id.SearchText);
searchText.addTextChangedListener(new TextWatcher(){
@Override
public void afterTextChanged(Editable s) {
if(initializeTask.getStatus() == Status.FINISHED){
if(initializeTask == null || initializeTask.getStatus() == Status.FINISHED){
setText(s.toString());
}
}
@ -61,7 +63,6 @@ public abstract class SearchByNameAbstractActivity<T> extends ListActivity {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
});
searchText.requestFocus();
@ -69,13 +70,11 @@ public abstract class SearchByNameAbstractActivity<T> extends ListActivity {
findViewById(R.id.ResetButton).setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
resetText();
searchText.setText("");
}
});
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
initializeTask = getInitializeTask();
if(initializeTask != null){
initializeTask.execute();
}
@ -86,7 +85,6 @@ public abstract class SearchByNameAbstractActivity<T> extends ListActivity {
return null;
}
public boolean isFilterableByDefault(){
return false;
}
@ -95,29 +93,24 @@ public abstract class SearchByNameAbstractActivity<T> extends ListActivity {
return searchText.getText();
}
public void resetText(){
setText("");
public void updateSearchText(){
setText(searchText.getText().toString());
}
public void setText(final String filter) {
if(isFilterableByDefault()){
if (isFilterableByDefault()) {
((NamesAdapter) getListAdapter()).getFilter().filter(filter);
return;
}
((NamesAdapter) getListAdapter()).clear();
Status status = searchTask.getStatus();
if(status == Status.FINISHED){
searchTask = new SearchByNameTask();
} else if(status == Status.RUNNING){
if (status != Status.FINISHED) {
searchTask.cancel(true);
// TODO improve
searchTask = new SearchByNameTask();
}
searchTask = new SearchByNameTask();
searchTask.execute(filter);
}
public abstract List<T> getObjects(String filter, SearchByNameTask searchTask);
public abstract void updateTextView(T obj, TextView txt);
@ -157,12 +150,16 @@ public abstract class SearchByNameAbstractActivity<T> extends ListActivity {
protected class SearchByNameTask extends AsyncTask<String, T, List<T>> {
private String filter;
private long startTime;
@Override
protected List<T> doInBackground(String... params) {
if(params == null || params.length == 0){
return null;
}
String filter = params[0];
filter = params[0];
startTime = System.currentTimeMillis();
return getObjects(filter, this);
}
@ -184,6 +181,7 @@ public abstract class SearchByNameAbstractActivity<T> extends ListActivity {
@Override
protected void onPostExecute(List<T> result) {
System.out.println("Search " + filter + " finished in " + (System.currentTimeMillis() - startTime));
if (!isCancelled() && result != null) {
((NamesAdapter) getListAdapter()).setNotifyOnChange(false);
((NamesAdapter) getListAdapter()).clear();

View file

@ -10,13 +10,10 @@ import net.osmand.data.MapObject;
import net.osmand.data.PostCode;
import net.osmand.osm.LatLon;
import net.osmand.osm.MapUtils;
import net.osmand.plus.OsmandSettings;
import net.osmand.plus.R;
import net.osmand.plus.RegionAddressRepository;
import net.osmand.plus.activities.OsmandApplication;
import net.osmand.plus.activities.search.SearchByNameAbstractActivity.SearchByNameTask;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
@ -26,12 +23,12 @@ public class SearchCityByNameActivity extends SearchByNameAbstractActivity<MapOb
@Override
public AsyncTask<Object, ?, ?> getInitializeTask() {
return new AsyncTask<Object, Void, Void>(){
return new AsyncTask<Object, MapObject, Void>(){
@Override
protected void onPostExecute(Void result) {
((TextView)findViewById(R.id.Label)).setText(R.string.incremental_search_city);
progress.setVisibility(View.INVISIBLE);
resetText();
updateSearchText();
}
@Override
@ -39,9 +36,35 @@ public class SearchCityByNameActivity extends SearchByNameAbstractActivity<MapOb
((TextView)findViewById(R.id.Label)).setText(R.string.loading_cities);
progress.setVisibility(View.VISIBLE);
}
@Override
protected void onProgressUpdate(MapObject... values) {
if (hasWindowFocus()) {
for (MapObject t : values) {
((NamesAdapter) getListAdapter()).add(t);
}
}
}
@Override
protected Void doInBackground(Object... params) {
region = ((OsmandApplication)getApplication()).getResourceManager().getRegionRepository(settings.getLastSearchedRegion());
if(region != null){
// preload cities
region.fillWithSuggestedCities("", new ResultMatcher<MapObject>() {
@Override
public boolean publish(MapObject object) {
publishProgress(object);
return true;
}
@Override
public boolean isCancelled() {
return false;
}
}, locationToSearch);
}
return null;
}
};
@ -50,13 +73,18 @@ public class SearchCityByNameActivity extends SearchByNameAbstractActivity<MapOb
@Override
public List<MapObject> getObjects(String filter, final SearchByNameTask task) {
if(region != null){
region.fillWithSuggestedCities(filter, new ResultMatcher<MapObject>() {
return region.fillWithSuggestedCities(filter, new ResultMatcher<MapObject>() {
@Override
public boolean publish(MapObject object) {
task.progress(object);
return true;
}
@Override
public boolean isCancelled() {
return task.isCancelled();
}
}, locationToSearch);
}
return new ArrayList<MapObject>();

View file

@ -21,6 +21,8 @@ public class SearchRegionByNameActivity extends SearchByNameAbstractActivity<Reg
if(((OsmandApplication)getApplication()).getResourceManager().getAddressRepositories().isEmpty()){
Toast.makeText(this, R.string.none_region_found, Toast.LENGTH_LONG).show();
}
NamesAdapter namesAdapter = new NamesAdapter(getObjects("", null)); //$NON-NLS-1$
setListAdapter(namesAdapter);
}
@Override

View file

@ -9,7 +9,6 @@ import net.osmand.data.Street;
import net.osmand.plus.R;
import net.osmand.plus.RegionAddressRepository;
import net.osmand.plus.activities.OsmandApplication;
import net.osmand.plus.activities.search.SearchByNameAbstractActivity.SearchByNameTask;
import android.os.AsyncTask;
import android.view.View;
import android.widget.TextView;
@ -29,7 +28,7 @@ public class SearchStreet2ByNameActivity extends SearchByNameAbstractActivity<St
protected void onPostExecute(Void result) {
((TextView)findViewById(R.id.Label)).setText(R.string.incremental_search_street);
progress.setVisibility(View.INVISIBLE);
resetText();
updateSearchText();
}
@Override
@ -51,7 +50,7 @@ public class SearchStreet2ByNameActivity extends SearchByNameAbstractActivity<St
} else if(city != null){
street1 = region.getStreetByName(city, (settings.getLastSearchedStreet()));
}
if(city != null){
if(city != null && street1 != null){
List<Street> t = new ArrayList<Street>();
region.fillWithSuggestedStreetsIntersectStreets(city, street1, t);
initialList = t;

View file

@ -22,12 +22,12 @@ public class SearchStreetByNameActivity extends SearchByNameAbstractActivity<Str
@Override
public AsyncTask<Object, ?, ?> getInitializeTask() {
return new AsyncTask<Object, Void, Void>(){
return new AsyncTask<Object, Street, Void>(){
@Override
protected void onPostExecute(Void result) {
((TextView)findViewById(R.id.Label)).setText(R.string.incremental_search_street);
progress.setVisibility(View.INVISIBLE);
resetText();
updateSearchText();
}
@Override
@ -35,6 +35,7 @@ public class SearchStreetByNameActivity extends SearchByNameAbstractActivity<Str
((TextView)findViewById(R.id.Label)).setText(R.string.loading_streets);
progress.setVisibility(View.VISIBLE);
}
@Override
protected Void doInBackground(Object... params) {
region = ((OsmandApplication)getApplication()).getResourceManager().getRegionRepository(settings.getLastSearchedRegion());
@ -44,6 +45,8 @@ public class SearchStreetByNameActivity extends SearchByNameAbstractActivity<Str
city = region.getCityById(settings.getLastSearchedCity());
}
}
// preload here to avoid concurrent modification
region.fillWithSuggestedStreets(postcode == null ? city : postcode, null);
return null;
}
};
@ -58,6 +61,10 @@ public class SearchStreetByNameActivity extends SearchByNameAbstractActivity<Str
task.progress(object);
return true;
}
@Override
public boolean isCancelled() {
return task.isCancelled();
}
}, filter);
}
return new ArrayList<Street>();