package net.osmand.plus.poi;
import android.content.Context;
import android.support.annotation.NonNull;
import net.osmand.CollatorStringMatcher;
import net.osmand.CollatorStringMatcher.StringMatcherMode;
import net.osmand.Location;
import net.osmand.ResultMatcher;
import net.osmand.binary.BinaryMapIndexReader.SearchPoiTypeFilter;
import net.osmand.data.Amenity;
import net.osmand.data.LatLon;
import net.osmand.osm.AbstractPoiType;
import net.osmand.osm.MapPoiTypes;
import net.osmand.osm.PoiCategory;
import net.osmand.osm.PoiFilter;
import net.osmand.osm.PoiType;
import net.osmand.plus.OsmAndFormatter;
import net.osmand.plus.OsmandApplication;
import net.osmand.plus.R;
import net.osmand.search.core.CustomSearchPoiFilter;
import net.osmand.util.Algorithms;
import net.osmand.util.MapUtils;
import net.osmand.util.OpeningHoursParser;
import net.osmand.util.OpeningHoursParser.OpeningHours;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;
public class PoiUIFilter implements SearchPoiTypeFilter, Comparable<PoiUIFilter>, CustomSearchPoiFilter {
public final static String STD_PREFIX = "std_"; //$NON-NLS-1$
public final static String USER_PREFIX = "user_"; //$NON-NLS-1$
public final static String CUSTOM_FILTER_ID = USER_PREFIX + "custom_id"; //$NON-NLS-1$
public final static String BY_NAME_FILTER_ID = USER_PREFIX + "by_name"; //$NON-NLS-1$
private Map<PoiCategory, LinkedHashSet<String>> acceptedTypes = new LinkedHashMap<>();
private Map<String, PoiType> poiAdditionals = new HashMap<>();
protected String filterId;
protected String standardIconId = "";
protected String name;
protected boolean isStandardFilter;
protected final OsmandApplication app;
protected int distanceInd = 0;
// in kilometers
protected double[] distanceToSearchValues = new double[] { 1, 2, 5, 10, 20, 50, 100, 200, 500 };
private final MapPoiTypes poiTypes;
protected String filterByName = null;
protected String savedFilterByName = null;
protected List<Amenity> currentSearchResult = null;
// constructor for standard filters
public PoiUIFilter(AbstractPoiType type, OsmandApplication application, String idSuffix) {
this.app = application;
isStandardFilter = true;
standardIconId = (type == null ? null : type.getKeyName());
filterId = STD_PREFIX + standardIconId + idSuffix;
poiTypes = application.getPoiTypes();
name = type == null ? application.getString(R.string.poi_filter_closest_poi) : (type.getTranslation() + idSuffix); //$NON-NLS-1$
if (type == null) {
initSearchAll();
updatePoiAdditionals();
} else {
if (type.isAdditional()) {
setSavedFilterByName(type.getKeyName().replace('_', ':'));
}
updateTypesToAccept(type);
}
}
// search by name standard
protected PoiUIFilter(OsmandApplication application) {
this.app = application;
isStandardFilter = true;
filterId = STD_PREFIX; // overridden
poiTypes = application.getPoiTypes();
}
// constructor for user defined filters
public PoiUIFilter(String name, String filterId,
Map<PoiCategory, LinkedHashSet<String>> acceptedTypes, OsmandApplication app) {
this.app = app;
isStandardFilter = false;
poiTypes = app.getPoiTypes();
if (filterId == null) {
filterId = USER_PREFIX + name.replace(' ', '_').toLowerCase();
}
this.filterId = filterId;
this.name = name;
if (acceptedTypes == null) {
initSearchAll();
} else {
this.acceptedTypes.putAll(acceptedTypes);
}
updatePoiAdditionals();
}
public PoiUIFilter(Set<PoiUIFilter> filtersToMerge, OsmandApplication app) {
this(app);
combineWithPoiFilters(filtersToMerge);
filterId = PoiUIFilter.STD_PREFIX + "combined";
name = app.getPoiFilters().getFiltersName(filtersToMerge);
}
public String getFilterByName() {
return filterByName;
}
public void setFilterByName(String filterByName) {
this.filterByName = filterByName;
updateFilterResults();
}
public void updateFilterResults() {
List<Amenity> prev = currentSearchResult;
if (prev != null) {
AmenityNameFilter nameFilter = getNameFilter(filterByName);
List<Amenity> newResults = new ArrayList<Amenity>();
for (Amenity a : prev) {
if (nameFilter.accept(a)) {
newResults.add(a);
}
}
currentSearchResult = newResults;
}
}
public void setSavedFilterByName(String filterByName) {
this.filterByName = filterByName;
this.savedFilterByName = filterByName;
}
public String getSavedFilterByName() {
return savedFilterByName;
}
public List<Amenity> searchAgain(double lat, double lon) {
List<Amenity> amenityList;
if (currentSearchResult != null) {
amenityList = currentSearchResult;
} else {
amenityList = searchAmenities(lat, lon, null);
}
MapUtils.sortListOfMapObject(amenityList, lat, lon);
return amenityList;
}
public List<Amenity> searchFurther(double latitude, double longitude, ResultMatcher<Amenity> matcher) {
if (distanceInd < distanceToSearchValues.length - 1) {
distanceInd++;
}
List<Amenity> amenityList = searchAmenities(latitude, longitude, matcher);
MapUtils.sortListOfMapObject(amenityList, latitude, longitude);
return amenityList;
}
private void initSearchAll() {
for (PoiCategory t : poiTypes.getCategories(false)) {
acceptedTypes.put(t, null);
}
distanceToSearchValues = new double[]{0.5, 1, 2, 5, 10, 20, 50, 100};
}
public boolean isSearchFurtherAvailable() {
return distanceInd < distanceToSearchValues.length - 1;
}
public String getSearchArea(boolean next) {
int distInd = distanceInd;
if (next && (distanceInd < distanceToSearchValues.length - 1)) {
//This is workaround for the SearchAmenityTask.onPreExecute() case
distInd = distanceInd + 1;
}
double val = distanceToSearchValues[distInd];
if (val >= 1) {
return " < " + OsmAndFormatter.getFormattedDistance(((int) val * 1000), app); //$NON-NLS-1$//$NON-NLS-2$
} else {
return " < " + OsmAndFormatter.getFormattedDistance(500, app); //$NON-NLS-1$
}
}
public void clearPreviousZoom() {
distanceInd = 0;
}
public void clearCurrentResults() {
if (currentSearchResult != null) {
currentSearchResult = new ArrayList<>();
}
}
public List<Amenity> initializeNewSearch(double lat, double lon, int firstTimeLimit, ResultMatcher<Amenity> matcher) {
clearPreviousZoom();
List<Amenity> amenityList = searchAmenities(lat, lon, matcher);
MapUtils.sortListOfMapObject(amenityList, lat, lon);
if (firstTimeLimit > 0) {
while (amenityList.size() > firstTimeLimit) {
amenityList.remove(amenityList.size() - 1);
}
}
if (amenityList.size() == 0 && isAutomaticallyIncreaseSearch()) {
int step = 5;
while (amenityList.size() == 0 && step-- > 0 && isSearchFurtherAvailable()) {
if (matcher != null && matcher.isCancelled()) {
break;
}
amenityList = searchFurther(lat, lon, matcher);
}
}
return amenityList;
}
public boolean isAutomaticallyIncreaseSearch() {
return true;
}
private List<Amenity> searchAmenities(double lat, double lon, ResultMatcher<Amenity> matcher) {
double baseDistY = MapUtils.getDistance(lat, lon, lat - 1, lon);
double baseDistX = MapUtils.getDistance(lat, lon, lat, lon - 1);
double distance = distanceToSearchValues[distanceInd] * 1000;
double topLatitude = Math.min(lat + (distance / baseDistY), 84.);
double bottomLatitude = Math.max(lat - (distance / baseDistY), -84.);
double leftLongitude = Math.max(lon - (distance / baseDistX), -180);
double rightLongitude = Math.min(lon + (distance / baseDistX), 180);
return searchAmenitiesInternal(lat, lon, topLatitude, bottomLatitude, leftLongitude, rightLongitude, -1, matcher);
}
public List<Amenity> searchAmenities(double top, double left, double bottom, double right, int zoom,
ResultMatcher<Amenity> matcher) {
List<Amenity> results = new ArrayList<Amenity>();
List<Amenity> tempResults = currentSearchResult;
if (tempResults != null) {
for (Amenity a : tempResults) {
LatLon l = a.getLocation();
if (l != null && l.getLatitude() <= top && l.getLatitude() >= bottom && l.getLongitude() >= left
&& l.getLongitude() <= right) {
if (matcher == null || matcher.publish(a)) {
results.add(a);
}
}
}
}
List<Amenity> amenities = searchAmenitiesInternal(top / 2 + bottom / 2, left / 2 + right / 2,
top, bottom, left, right, zoom, matcher);
results.addAll(amenities);
return results;
}
public List<Amenity> searchAmenitiesOnThePath(List<Location> locs, int poiSearchDeviationRadius) {
return app.getResourceManager().searchAmenitiesOnThePath(locs, poiSearchDeviationRadius, this, wrapResultMatcher(null));
}
protected List<Amenity> searchAmenitiesInternal(double lat, double lon, double topLatitude,
double bottomLatitude, double leftLongitude, double rightLongitude, int zoom, final ResultMatcher<Amenity> matcher) {
return app.getResourceManager().searchAmenities(this,
topLatitude, leftLongitude, bottomLatitude, rightLongitude, zoom, wrapResultMatcher(matcher));
}
public AmenityNameFilter getNameFilter(String filter) {
if (Algorithms.isEmpty(filter)) {
return new AmenityNameFilter() {
@Override
public boolean accept(Amenity a) {
return true;
}
};
}
StringBuilder nmFilter = new StringBuilder();
String[] items = filter.split(" ");
boolean allTime = false;
boolean open = false;
List<PoiType> poiAdditionalsFilter = null;
for (String s : items) {
s = s.trim();
if (!Algorithms.isEmpty(s)) {
if (getNameToken24H().equalsIgnoreCase(s)) {
allTime = true;
} else if (getNameTokenOpen().equalsIgnoreCase(s)) {
open = true;
} else if (poiAdditionals.containsKey(s.toLowerCase())) {
if (poiAdditionalsFilter == null) {
poiAdditionalsFilter = new ArrayList<>();
}
PoiType pt = poiAdditionals.get(s.toLowerCase());
if (pt != null) {
poiAdditionalsFilter.add(pt);
}
} else {
nmFilter.append(s).append(" ");
}
}
}
return getNameFilterInternal(nmFilter, allTime, open, poiAdditionalsFilter);
}
private AmenityNameFilter getNameFilterInternal(StringBuilder nmFilter,
final boolean allTime, final boolean open, final List<PoiType> poiAdditionals) {
final CollatorStringMatcher sm =
nmFilter.length() > 0 ?
new CollatorStringMatcher(nmFilter.toString().trim(), StringMatcherMode.CHECK_CONTAINS) : null;
return new AmenityNameFilter() {
@Override
public boolean accept(Amenity a) {
if (sm != null) {
String lower = OsmAndFormatter.getPoiStringWithoutType(a,
app.getSettings().MAP_PREFERRED_LOCALE.get(), app.getSettings().MAP_TRANSLITERATE_NAMES.get());
if (!sm.matches(lower)) {
return false;
}
}
if (poiAdditionals != null) {
Map<PoiType, PoiType> textPoiAdditionalsMap = new HashMap<>();
Map<String, List<PoiType>> poiAdditionalCategoriesMap = new HashMap<>();
for (PoiType pt : poiAdditionals) {
String category = pt.getPoiAdditionalCategory();
List<PoiType> types = poiAdditionalCategoriesMap.get(category);
if (types == null) {
types = new ArrayList<>();
poiAdditionalCategoriesMap.put(category, types);
}
types.add(pt);
String osmTag = pt.getOsmTag();
if (osmTag.length() < pt.getKeyName().length()) {
PoiType textPoiType = poiTypes.getTextPoiAdditionalByKey(osmTag);
if (textPoiType != null) {
textPoiAdditionalsMap.put(pt, textPoiType);
}
}
}
for (List<PoiType> types : poiAdditionalCategoriesMap.values()) {
boolean acceptedAnyInCategory = false;
for (PoiType p : types) {
String inf = a.getAdditionalInfo(p.getKeyName());
if (inf != null) {
acceptedAnyInCategory = true;
break;
} else {
PoiType textPoiType = textPoiAdditionalsMap.get(p);
if (textPoiType != null) {
inf = a.getAdditionalInfo(textPoiType.getKeyName());
if (!Algorithms.isEmpty(inf)) {
String[] items = inf.split(";");
String val = p.getOsmValue().trim().toLowerCase();
for (String item : items) {
if (item.trim().toLowerCase().equals(val)) {
acceptedAnyInCategory = true;
break;
}
}
if (acceptedAnyInCategory) {
break;
}
}
}
}
}
if (!acceptedAnyInCategory) {
return false;
}
}
}
if (allTime) {
if (!"24/7".equalsIgnoreCase(a.getOpeningHours()) && !"Mo-Su 00:00-24:00".equalsIgnoreCase(a.getOpeningHours())) {
return false;
}
}
if (open) {
OpeningHours rs = OpeningHoursParser.parseOpenedHours(a.getOpeningHours());
if (rs != null) {
Calendar inst = Calendar.getInstance();
inst.setTimeInMillis(System.currentTimeMillis());
boolean work = rs.isOpenedForTime(inst);
if (!work) {
return false;
}
} else {
return false;
}
}
return true;
}
};
}
public String getNameToken24H() {
return app.getString(R.string.shared_string_is_open_24_7).replace(' ', '_').toLowerCase();
}
public String getNameTokenOpen() {
return app.getString(R.string.shared_string_is_open).replace(' ', '_').toLowerCase();
}
@Override
public Object getIconResource() {
return getIconId();
}
@Override
public ResultMatcher<Amenity> wrapResultMatcher(final ResultMatcher<Amenity> matcher) {
final AmenityNameFilter nm = getNameFilter(filterByName);
return new ResultMatcher<Amenity>() {
@Override
public boolean publish(Amenity a) {
if (nm.accept(a)) {
if (matcher == null || matcher.publish(a)) {
return true;
}
}
return false;
}
@Override
public boolean isCancelled() {
return matcher != null && matcher.isCancelled();
}
};
}
@Override
public String getName() {
return name;
}
public String getGeneratedName(int chars) {
if (!filterId.equals(CUSTOM_FILTER_ID) ||
areAllTypesAccepted() || acceptedTypes.isEmpty()) {
return getName();
}
StringBuilder res = new StringBuilder();
for (PoiCategory p : acceptedTypes.keySet()) {
LinkedHashSet<String> set = acceptedTypes.get(p);
if (set == null) {
if (res.length() > 0) {
res.append(", ");
}
res.append(p.getTranslation());
}
if (res.length() > chars) {
return res.toString();
}
}
for (PoiCategory p : acceptedTypes.keySet()) {
LinkedHashSet<String> set = acceptedTypes.get(p);
if (set != null) {
for (String st : set) {
if (res.length() > 0) {
res.append(", ");
}
PoiType pt = poiTypes.getPoiTypeByKey(st);
if (pt != null) {
res.append(pt.getTranslation());
if (res.length() > chars) {
return res.toString();
}
}
}
}
}
return res.toString();
}
/**
* @param type
* @return null if all subtypes are accepted/ empty list if type is not accepted at all
*/
public Set<String> getAcceptedSubtypes(PoiCategory type) {
if (!acceptedTypes.containsKey(type)) {
return Collections.emptySet();
}
return acceptedTypes.get(type);
}
public boolean isTypeAccepted(PoiCategory t) {
return acceptedTypes.containsKey(t);
}
public void clearFilter() {
acceptedTypes = new LinkedHashMap<>();
poiAdditionals.clear();
filterByName = null;
clearCurrentResults();
}
public boolean areAllTypesAccepted() {
if (poiTypes.getCategories(false).size() == acceptedTypes.size()) {
for (PoiCategory a : acceptedTypes.keySet()) {
if (acceptedTypes.get(a) != null) {
return false;
}
}
return true;
}
return false;
}
public void updateTypesToAccept(AbstractPoiType pt) {
pt.putTypes(acceptedTypes);
if (pt instanceof PoiType && ((PoiType) pt).isAdditional() && ((PoiType) pt).getParentType() != null) {
fillPoiAdditionals(((PoiType) pt).getParentType(), true);
} else {
fillPoiAdditionals(pt, true);
}
addOtherPoiAdditionals();
}
private void fillPoiAdditionals(AbstractPoiType pt, boolean allFromCategory) {
for (PoiType add : pt.getPoiAdditionals()) {
poiAdditionals.put(add.getKeyName().replace('_', ':').replace(' ', ':'), add);
poiAdditionals.put(add.getTranslation().replace(' ', ':').toLowerCase(), add);
}
if (pt instanceof PoiCategory && allFromCategory) {
for (PoiFilter pf : ((PoiCategory) pt).getPoiFilters()) {
fillPoiAdditionals(pf, true);
}
for (PoiType ps : ((PoiCategory) pt).getPoiTypes()) {
fillPoiAdditionals(ps, false);
}
} else if (pt instanceof PoiFilter && !(pt instanceof PoiCategory)) {
for (PoiType ps : ((PoiFilter) pt).getPoiTypes()) {
fillPoiAdditionals(ps, false);
}
}
}
private void updatePoiAdditionals() {
Iterator<Entry<PoiCategory, LinkedHashSet<String>>> e = acceptedTypes.entrySet().iterator();
poiAdditionals.clear();
while (e.hasNext()) {
Entry<PoiCategory, LinkedHashSet<String>> pc = e.next();
fillPoiAdditionals(pc.getKey(), pc.getValue() == null);
if (pc.getValue() != null) {
for (String s : pc.getValue()) {
PoiType subtype = poiTypes.getPoiTypeByKey(s);
if (subtype != null) {
fillPoiAdditionals(subtype, false);
}
}
}
}
addOtherPoiAdditionals();
}
private void addOtherPoiAdditionals() {
for (PoiType add : poiTypes.getOtherMapCategory().getPoiAdditionalsCategorized()) {
poiAdditionals.put(add.getKeyName().replace('_', ':').replace(' ', ':'), add);
poiAdditionals.put(add.getTranslation().replace(' ', ':').toLowerCase(), add);
}
}
public void combineWithPoiFilter(PoiUIFilter f) {
acceptedTypes.putAll(f.acceptedTypes);
poiAdditionals.putAll(f.poiAdditionals);
}
public void combineWithPoiFilters(Set<PoiUIFilter> filters) {
for (PoiUIFilter f : filters) {
combineWithPoiFilter(f);
}
}
public static void combineStandardPoiFilters(Set<PoiUIFilter> filters, OsmandApplication app) {
Set<PoiUIFilter> standardFilters = new TreeSet<>();
for (PoiUIFilter filter : filters) {
if (((filter.isStandardFilter() && filter.filterId.startsWith(PoiUIFilter.STD_PREFIX))
|| filter.filterId.startsWith(PoiUIFilter.CUSTOM_FILTER_ID))
&& (filter.getFilterByName() == null)
&& (filter.getSavedFilterByName() == null)) {
standardFilters.add(filter);
}
}
if (standardFilters.size() > 1) {
PoiUIFilter standardFiltersCombined = new PoiUIFilter(standardFilters, app);
filters.removeAll(standardFilters);
filters.add(standardFiltersCombined);
}
}
public void replaceWithPoiFilter(PoiUIFilter f) {
clearFilter();
combineWithPoiFilter(f);
}
public int getAcceptedTypesCount() {
return acceptedTypes.size();
}
public Map<PoiCategory, LinkedHashSet<String>> getAcceptedTypes() {
return new LinkedHashMap<>(acceptedTypes);
}
public void selectSubTypesToAccept(PoiCategory t, LinkedHashSet<String> accept) {
acceptedTypes.put(t, accept);
updatePoiAdditionals();
}
public void setTypeToAccept(PoiCategory poiCategory, boolean b) {
if (b) {
acceptedTypes.put(poiCategory, null);
} else {
acceptedTypes.remove(poiCategory);
}
updatePoiAdditionals();
}
public String getFilterId() {
return filterId;
}
public Map<String, PoiType> getPoiAdditionals() {
return poiAdditionals;
}
public String getIconId() {
if (filterId.startsWith(STD_PREFIX)) {
return standardIconId;
} else if (filterId.startsWith(USER_PREFIX)) {
return filterId.substring(USER_PREFIX.length()).toLowerCase();
}
return filterId;
}
public boolean isStandardFilter() {
return isStandardFilter;
}
public void setStandardFilter(boolean isStandardFilter) {
this.isStandardFilter = isStandardFilter;
}
public Context getApplication() {
return app;
}
@Override
public boolean accept(PoiCategory type, String subtype) {
if (type == null) {
return true;
}
if (!poiTypes.isRegisteredType(type)) {
type = poiTypes.getOtherPoiCategory();
}
if (!acceptedTypes.containsKey(type)) {
return false;
}
LinkedHashSet<String> set = acceptedTypes.get(type);
if (set == null) {
return true;
}
return set.contains(subtype);
}
@Override
public boolean isEmpty() {
return acceptedTypes.isEmpty() &&
(currentSearchResult == null || currentSearchResult.isEmpty());
}
@Override
public int compareTo(@NonNull PoiUIFilter another) {
if (another.filterId.equals(this.filterId)) {
String thisFilterByName = this.filterByName == null ? "" : this.filterByName;
String anotherFilterByName = another.filterByName == null ? "" : another.filterByName;
return thisFilterByName.compareToIgnoreCase(anotherFilterByName);
} else {
return this.name.compareTo(another.name);
}
}
public interface AmenityNameFilter {
public boolean accept(Amenity a);
}
}