package net.osmand.search.core;
import net.osmand.Collator;
import net.osmand.CollatorStringMatcher;
import net.osmand.CollatorStringMatcher.StringMatcherMode;
import net.osmand.StringMatcher;
import net.osmand.binary.BinaryMapIndexReader;
import net.osmand.binary.CommonWords;
import net.osmand.binary.BinaryMapIndexReader.SearchRequest;
import net.osmand.data.LatLon;
import net.osmand.data.QuadRect;
import net.osmand.util.Algorithms;
import net.osmand.util.MapUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Pattern;
//immutable object
public class SearchPhrase {
private List<SearchWord> words = new ArrayList<>();
private List<String> unknownWords = new ArrayList<>();
private List<NameStringMatcher> unknownWordsMatcher = new ArrayList<>();
private String unknownSearchWordTrim;
private String unknownSearchPhrase = "";
private NameStringMatcher sm;
private SearchSettings settings;
private List<BinaryMapIndexReader> indexes;
private QuadRect cache1kmRect;
private boolean lastUnknownSearchWordComplete;
private static final String DELIMITER = " ";
private static final String ALLDELIMITERS = "\\s|,";
private static final Pattern reg = Pattern.compile(ALLDELIMITERS);
private Collator clt;
private static Set<String> conjunctions = new TreeSet<>();
static {
// the
conjunctions.add("the");
conjunctions.add("der");
conjunctions.add("den");
conjunctions.add("die");
conjunctions.add("das");
conjunctions.add("la");
conjunctions.add("le");
conjunctions.add("el");
conjunctions.add("il");
// and
conjunctions.add("and");
conjunctions.add("und");
conjunctions.add("en");
conjunctions.add("et");
conjunctions.add("y");
conjunctions.add("и");
// short
conjunctions.add("f");
conjunctions.add("u");
conjunctions.add("jl.");
conjunctions.add("j");
conjunctions.add("sk");
conjunctions.add("w");
conjunctions.add("a.");
conjunctions.add("of");
conjunctions.add("k");
conjunctions.add("r");
conjunctions.add("h");
conjunctions.add("mc");
conjunctions.add("sw");
conjunctions.add("g");
conjunctions.add("v");
conjunctions.add("m");
conjunctions.add("c.");
conjunctions.add("r.");
conjunctions.add("ct");
conjunctions.add("e.");
conjunctions.add("dr.");
conjunctions.add("j.");
conjunctions.add("in");
conjunctions.add("al");
conjunctions.add("út");
conjunctions.add("per");
conjunctions.add("ne");
conjunctions.add("p");
conjunctions.add("et");
conjunctions.add("s.");
conjunctions.add("f.");
conjunctions.add("t");
conjunctions.add("fe");
conjunctions.add("à");
conjunctions.add("i");
conjunctions.add("c");
conjunctions.add("le");
conjunctions.add("s");
conjunctions.add("av.");
conjunctions.add("den");
conjunctions.add("dr");
conjunctions.add("y");
}
public enum SearchPhraseDataType {
MAP, ADDRESS, ROUTING, POI
}
public SearchPhrase(SearchSettings settings, Collator clt) {
this.settings = settings;
this.clt = clt;
}
public Collator getCollator() {
return clt;
}
public SearchPhrase generateNewPhrase(String text, SearchSettings settings) {
SearchPhrase sp = new SearchPhrase(settings, this.clt);
String restText = text;
List<SearchWord> leftWords = this.words;
String thisTxt = getText(true);
if (text.startsWith(thisTxt)) {
// string is longer
restText = text.substring(getText(false).length());
sp.words = new ArrayList<>(this.words);
leftWords = leftWords.subList(leftWords.size(), leftWords.size());
}
for(SearchWord w : leftWords) {
if(restText.startsWith(w.getWord() + DELIMITER)) {
sp.words.add(w);
restText = restText.substring(w.getWord().length() + DELIMITER.length()).trim();
} else {
break;
}
}
sp.unknownSearchPhrase = restText;
sp.unknownWords.clear();
sp.unknownWordsMatcher.clear();
if (!reg.matcher(restText).find()) {
sp.unknownSearchWordTrim = sp.unknownSearchPhrase.trim();
} else {
sp.unknownSearchWordTrim = "";
String[] ws = restText.split(ALLDELIMITERS);
boolean first = true;
for (int i = 0; i < ws.length ; i++) {
String wd = ws[i].trim();
if (wd.length() > 0 && !conjunctions.contains(wd.toLowerCase())) {
if (first) {
sp.unknownSearchWordTrim = wd;
first = false;
} else {
sp.unknownWords.add(wd);
}
}
}
}
sp.lastUnknownSearchWordComplete = false;
if (text.length() > 0 ) {
char ch = text.charAt(text.length() - 1);
sp.lastUnknownSearchWordComplete = ch == ' ' || ch == ',' || ch == '\r' || ch == '\n'
|| ch == ';';
}
return sp;
}
public List<SearchWord> getWords() {
return words;
}
public boolean isUnknownSearchWordComplete() {
return lastUnknownSearchWordComplete || unknownWords.size() > 0;
}
public boolean isLastUnknownSearchWordComplete() {
return lastUnknownSearchWordComplete;
}
public List<String> getUnknownSearchWords() {
return unknownWords;
}
public List<String> getUnknownSearchWords(Collection<String> exclude) {
if(exclude == null || unknownWords.size() == 0 || exclude.size() == 0) {
return unknownWords;
}
List<String> l = new ArrayList<>();
for(String uw : unknownWords) {
if(exclude == null || !exclude.contains(uw)) {
l.add(uw);
}
}
return l;
}
public String getUnknownSearchWord() {
return unknownSearchWordTrim;
}
public String getUnknownSearchPhrase() {
return unknownSearchPhrase;
}
public boolean isUnknownSearchWordPresent() {
return unknownSearchWordTrim.length() > 0;
}
public int getUnknownSearchWordLength() {
return unknownSearchWordTrim.length() ;
}
public QuadRect getRadiusBBoxToSearch(int radius) {
int radiusInMeters = getRadiusSearch(radius);
QuadRect cache1kmRect = get1km31Rect();
if(cache1kmRect == null) {
return null;
}
int max = (1 << 31) - 1;
double dx = (cache1kmRect.width() / 2) * radiusInMeters / 1000;
double dy = (cache1kmRect.height() / 2) * radiusInMeters / 1000;
double topLeftX = Math.max(0, cache1kmRect.left - dx);
double topLeftY = Math.max(0, cache1kmRect.top - dy);
double bottomRightX = Math.min(max, cache1kmRect.right + dx);
double bottomRightY = Math.min(max, cache1kmRect.bottom + dy);
return new QuadRect(topLeftX, topLeftY, bottomRightX, bottomRightY);
}
public QuadRect get1km31Rect() {
if(cache1kmRect != null) {
return cache1kmRect;
}
LatLon l = getLastTokenLocation();
if (l == null) {
return null;
}
float coeff = (float) (1000 / MapUtils.getTileDistanceWidth(SearchRequest.ZOOM_TO_SEARCH_POI));
double tx = MapUtils.getTileNumberX(SearchRequest.ZOOM_TO_SEARCH_POI, l.getLongitude());
double ty = MapUtils.getTileNumberY(SearchRequest.ZOOM_TO_SEARCH_POI, l.getLatitude());
double topLeftX = Math.max(0, tx - coeff);
double topLeftY = Math.max(0, ty - coeff);
int max = (1 << SearchRequest.ZOOM_TO_SEARCH_POI) - 1;
double bottomRightX = Math.min(max, tx + coeff);
double bottomRightY = Math.min(max, ty + coeff);
double pw = MapUtils.getPowZoom(31 - SearchRequest.ZOOM_TO_SEARCH_POI);
cache1kmRect = new QuadRect(topLeftX * pw, topLeftY * pw, bottomRightX * pw, bottomRightY * pw);
return cache1kmRect;
}
public Iterator<BinaryMapIndexReader> getRadiusOfflineIndexes(int meters, final SearchPhraseDataType dt) {
final QuadRect rect = meters > 0 ? getRadiusBBoxToSearch(meters) : null;
return getOfflineIndexes(rect, dt);
}
public Iterator<BinaryMapIndexReader> getOfflineIndexes(final QuadRect rect, final SearchPhraseDataType dt) {
List<BinaryMapIndexReader> list = indexes != null ? indexes : settings.getOfflineIndexes();
final Iterator<BinaryMapIndexReader> lit = list.iterator();
return new Iterator<BinaryMapIndexReader>() {
BinaryMapIndexReader next = null;
@Override
public boolean hasNext() {
while (lit.hasNext()) {
next = lit.next();
if(rect != null) {
if(dt == SearchPhraseDataType.POI) {
if(next.containsPoiData((int)rect.left, (int)rect.top, (int)rect.right, (int)rect.bottom)) {
return true;
}
} else if(dt == SearchPhraseDataType.ADDRESS) {
// containsAddressData not all maps supported
if(next.containsPoiData((int)rect.left, (int)rect.top, (int)rect.right, (int)rect.bottom) &&
next.containsAddressData()) {
return true;
}
} else if(dt == SearchPhraseDataType.ROUTING) {
if(next.containsRouteData((int)rect.left, (int)rect.top, (int)rect.right, (int)rect.bottom, 15)) {
return true;
}
} else {
if(next.containsMapData((int)rect.left, (int)rect.top, (int)rect.right, (int)rect.bottom, 15)) {
return true;
}
}
} else {
return true;
}
}
return false;
}
@Override
public BinaryMapIndexReader next() {
return next;
}
@Override
public void remove() {
}
};
}
public List<BinaryMapIndexReader> getOfflineIndexes() {
if(indexes != null) {
return indexes;
}
return settings.getOfflineIndexes();
}
public SearchSettings getSettings() {
return settings;
}
public int getRadiusLevel() {
return settings.getRadiusLevel();
}
public ObjectType[] getSearchTypes() {
return settings == null ? null : settings.getSearchTypes();
}
public boolean isCustomSearch() {
return getSearchTypes() != null;
}
public boolean isSearchTypeAllowed(ObjectType searchType) {
if (getSearchTypes() == null) {
return true;
} else {
for (ObjectType type : getSearchTypes()) {
if (type == searchType) {
return true;
}
}
return false;
}
}
public boolean isEmptyQueryAllowed() {
return settings.isEmptyQueryAllowed();
}
public boolean isSortByName() {
return settings.isSortByName();
}
public boolean isInAddressSearch() {
return settings.isInAddressSearch();
}
public SearchPhrase selectWord(SearchResult res) {
return selectWord(res, null, false);
}
public SearchPhrase selectWord(SearchResult res, List<String> unknownWords, boolean lastComplete) {
SearchPhrase sp = new SearchPhrase(this.settings, this.clt);
addResult(res, sp);
SearchResult prnt = res.parentSearchResult;
while(prnt != null) {
addResult(prnt, sp);
prnt = prnt.parentSearchResult;
}
sp.words.addAll(0, this.words);
if(unknownWords != null) {
sp.lastUnknownSearchWordComplete = lastComplete;
for (int i = 0; i < unknownWords.size(); i++) {
if (i == 0) {
sp.unknownSearchWordTrim = unknownWords.get(0);
} else {
sp.unknownWords.add(unknownWords.get(i));
}
}
}
return sp;
}
private void addResult(SearchResult res, SearchPhrase sp) {
SearchWord sw = new SearchWord(res.wordsSpan != null ? res.wordsSpan : res.localeName.trim(), res);
sp.words.add(0, sw);
}
public boolean isLastWord(ObjectType... p) {
for (int i = words.size() - 1; i >= 0; i--) {
SearchWord sw = words.get(i);
for(ObjectType o : p) {
if (sw.getType() == o) {
return true;
}
}
if (sw.getType() != ObjectType.UNKNOWN_NAME_FILTER) {
return false;
}
}
return false;
}
public ObjectType getExclusiveSearchType() {
SearchWord lastWord = getLastSelectedWord();
if (lastWord != null) {
return ObjectType.getExclusiveSearchType(lastWord.getType());
}
return null;
}
public NameStringMatcher getNameStringMatcher() {
if(sm != null) {
return sm;
}
sm = getNameStringMatcher(unknownSearchWordTrim, lastUnknownSearchWordComplete);
return sm;
}
public NameStringMatcher getNameStringMatcher(String word, boolean complete) {
return new NameStringMatcher(word,
(complete ?
StringMatcherMode.CHECK_EQUALS_FROM_SPACE :
StringMatcherMode.CHECK_STARTS_FROM_SPACE));
}
public boolean hasObjectType(ObjectType p) {
for(SearchWord s : words) {
if(s.getType() == p) {
return true;
}
}
return false;
}
public void syncWordsWithResults() {
for(SearchWord w : words) {
w.syncWordWithResult();
}
}
public String getText(boolean includeLastWord) {
StringBuilder sb = new StringBuilder();
for(SearchWord s : words) {
sb.append(s.getWord()).append(DELIMITER.trim() + " ");
}
if(includeLastWord) {
sb.append(unknownSearchPhrase);
}
return sb.toString();
}
public String getTextWithoutLastWord() {
StringBuilder sb = new StringBuilder();
List<SearchWord> words = new ArrayList<>(this.words);
if(Algorithms.isEmpty(unknownSearchWordTrim) && words.size() > 0) {
words.remove(words.size() - 1);
}
for(SearchWord s : words) {
sb.append(s.getWord()).append(DELIMITER.trim() + " ");
}
return sb.toString();
}
public String getStringRerpresentation() {
StringBuilder sb = new StringBuilder();
for(SearchWord s : words) {
sb.append(s.getWord()).append(" [" + s.getType() + "], ");
}
sb.append(unknownSearchPhrase);
return sb.toString();
}
@Override
public String toString() {
return getStringRerpresentation();
}
public boolean isNoSelectedType() {
return words.isEmpty();
}
public boolean isEmpty() {
return words.isEmpty() && unknownSearchPhrase.isEmpty();
}
public SearchWord getLastSelectedWord() {
if(words.isEmpty()) {
return null;
}
return words.get(words.size() - 1);
}
public LatLon getWordLocation() {
for(int i = words.size() - 1; i >= 0; i--) {
SearchWord sw = words.get(i);
if(sw.getLocation() != null) {
return sw.getLocation();
}
}
return null;
}
public LatLon getLastTokenLocation() {
for(int i = words.size() - 1; i >= 0; i--) {
SearchWord sw = words.get(i);
if(sw.getLocation() != null) {
return sw.getLocation();
}
}
// last token or myLocationOrVisibleMap if not selected
return settings.getOriginalLocation();
}
public void selectFile(BinaryMapIndexReader object) {
if(indexes == null) {
indexes = new ArrayList<>();
}
if(!this.indexes.contains(object)) {
this.indexes.add(object);
}
}
public void sortFiles() {
if(indexes == null) {
indexes = new ArrayList<>(getOfflineIndexes());
}
final LatLon ll = getLastTokenLocation();
if(ll != null) {
Collections.sort(indexes, new Comparator<BinaryMapIndexReader>() {
Map<BinaryMapIndexReader, LatLon> locations = new HashMap<>();
@Override
public int compare(BinaryMapIndexReader o1, BinaryMapIndexReader o2) {
LatLon rc1 = getLocation(o1);
LatLon rc2 = getLocation(o2);
double d1 = rc1 == null ? 10000000d : MapUtils.getDistance(rc1, ll);
double d2 = rc2 == null ? 10000000d : MapUtils.getDistance(rc2, ll);
return Double.compare(d1, d2);
}
private LatLon getLocation(BinaryMapIndexReader o1) {
if(locations.containsKey(o1)) {
return locations.get(o1);
}
LatLon rc1 = null;
if(o1.containsMapData()) {
rc1 = o1.getMapIndexes().get(0).getCenterLatLon();
} else {
rc1 = o1.getRegionCenter();
}
locations.put(o1, rc1);
return rc1;
}
});
}
}
public static class NameStringMatcher implements StringMatcher {
private CollatorStringMatcher sm;
public NameStringMatcher(String lastWordTrim, StringMatcherMode mode) {
sm = new CollatorStringMatcher(lastWordTrim, mode);
}
public boolean matches(Collection<String> map) {
if(map == null) {
return false;
}
for(String v : map) {
if(sm.matches(v)) {
return true;
}
}
return false;
}
@Override
public boolean matches(String name) {
return sm.matches(name);
}
}
public void countUnknownWordsMatch(SearchResult sr) {
countUnknownWordsMatch(sr, sr.localeName, sr.otherNames);
}
public void countUnknownWordsMatch(SearchResult sr, String localeName, Collection<String> otherNames) {
if(unknownWords.size() > 0) {
for(int i = 0; i < unknownWords.size(); i++) {
if(unknownWordsMatcher.size() == i) {
unknownWordsMatcher.add(new NameStringMatcher(unknownWords.get(i),
i < unknownWords.size() - 1 ? StringMatcherMode.CHECK_EQUALS_FROM_SPACE :
StringMatcherMode.CHECK_STARTS_FROM_SPACE));
}
NameStringMatcher ms = unknownWordsMatcher.get(i);
if(ms.matches(localeName) || ms.matches(otherNames)) {
if(sr.otherWordsMatch == null) {
sr.otherWordsMatch = new TreeSet<>();
}
sr.otherWordsMatch.add(unknownWords.get(i));
}
}
}
if(!sr.firstUnknownWordMatches) {
sr.firstUnknownWordMatches = localeName.equals(getUnknownSearchWord()) ||
getNameStringMatcher().matches(localeName) ||
getNameStringMatcher().matches(otherNames);
}
}
public int getRadiusSearch(int meters) {
return (1 << (getRadiusLevel() - 1)) * meters;
}
public static int icompare(int x, int y) {
return (x < y) ? -1 : ((x == y) ? 0 : 1);
}
public String getUnknownWordToSearchBuilding() {
List<String> unknownSearchWords = getUnknownSearchWords();
if(unknownSearchWords.size() > 0 && Algorithms.extractFirstIntegerNumber(getUnknownSearchWord()) == 0) {
for(String wrd : unknownSearchWords) {
if(Algorithms.extractFirstIntegerNumber(wrd) != 0) {
return wrd;
}
}
}
return getUnknownSearchWord();
}
public String getUnknownWordToSearch() {
List<String> unknownSearchWords = getUnknownSearchWords();
String wordToSearch = getUnknownSearchWord();
if (unknownSearchWords.size() > 0) {
List<String> searchWords = new ArrayList<>(unknownSearchWords);
searchWords.add(0, getUnknownSearchWord());
Collections.sort(searchWords, new Comparator<String>() {
private int lengthWithoutNumbers(String s) {
int len = 0;
for(int k = 0; k < s.length(); k++) {
if(s.charAt(k) >= '0' && s.charAt(k) <= '9') {
} else {
len++;
}
}
return len;
}
@Override
public int compare(String o1, String o2) {
int i1 = CommonWords.getCommonSearch(o1.toLowerCase());
int i2 = CommonWords.getCommonSearch(o2.toLowerCase());
if (i1 != i2) {
return icompare(i1, i2);
}
// compare length without numbers to not include house numbers
return -icompare(lengthWithoutNumbers(o1), lengthWithoutNumbers(o2));
}
});
wordToSearch = searchWords.get(0);
}
return wordToSearch;
}
}