package net.osmand.plus.resources;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import net.osmand.Collator;
import net.osmand.CollatorStringMatcher.StringMatcherMode;
import net.osmand.OsmAndCollator;
import net.osmand.PlatformUtil;
import net.osmand.ResultMatcher;
import net.osmand.binary.BinaryMapAddressReaderAdapter;
import net.osmand.binary.BinaryMapIndexReader;
import net.osmand.binary.BinaryMapIndexReader.SearchRequest;
import net.osmand.binary.GeocodingUtilities;
import net.osmand.binary.GeocodingUtilities.GeocodingResult;
import net.osmand.data.Building;
import net.osmand.data.City;
import net.osmand.data.LatLon;
import net.osmand.data.MapObject;
import net.osmand.data.QuadRect;
import net.osmand.data.QuadTree;
import net.osmand.data.Street;
import net.osmand.plus.OsmandSettings.OsmandPreference;
import net.osmand.plus.resources.ResourceManager.BinaryMapReaderResource;
import net.osmand.plus.resources.ResourceManager.BinaryMapReaderResourceType;
import net.osmand.util.MapUtils;
import org.apache.commons.logging.Log;
public class RegionAddressRepositoryBinary implements RegionAddressRepository {
private static final Log log = PlatformUtil.getLog(RegionAddressRepositoryBinary.class);
private LinkedHashMap<Long, City> cities = new LinkedHashMap<Long, City>();
private int POSTCODE_MIN_QUERY_LENGTH = 2;
private int ZOOM_QTREE = 10;
private QuadTree<City> citiesQtree = new QuadTree<City>(new QuadRect(0, 0, 1 << (ZOOM_QTREE + 1),
1 << (ZOOM_QTREE + 1)), 8, 0.55f);
private final Map<String, City> postCodes;
private final Collator collator;
private OsmandPreference<String> langSetting;
private OsmandPreference<Boolean> transliterateSetting;
private BinaryMapReaderResource resource;
public RegionAddressRepositoryBinary(ResourceManager mgr, BinaryMapReaderResource resource ) {
this.resource = resource;
langSetting = mgr.getContext().getSettings().MAP_PREFERRED_LOCALE;
transliterateSetting = mgr.getContext().getSettings().MAP_TRANSLITERATE_NAMES;
this.collator = OsmAndCollator.primaryCollator();
this.postCodes = new TreeMap<String, City>(OsmAndCollator.primaryCollator());
}
@Override
public void close() {
}
@Override
public synchronized void preloadCities(ResultMatcher<City> resultMatcher) {
if (cities.isEmpty()) {
try {
List<City> cs = getOpenFile().getCities(BinaryMapIndexReader.buildAddressRequest(resultMatcher),
BinaryMapAddressReaderAdapter.CITY_TOWN_TYPE);
LinkedHashMap<Long, City> ncities = new LinkedHashMap<Long, City>();
for (City c : cs) {
ncities.put(c.getId(), c);
LatLon loc = c.getLocation();
if (loc != null) {
int y31 = MapUtils.get31TileNumberY(loc.getLatitude());
int x31 = MapUtils.get31TileNumberX(loc.getLongitude());
int dz = (31 - ZOOM_QTREE);
citiesQtree.insert(c, new QuadRect((x31 >> dz) - 1, (y31 >> dz) - 1, (x31 >> dz) + 1, (y31 >> dz) + 1));
}
}
cities = ncities;
} catch (IOException e) {
log.error("Disk operation failed", e); //$NON-NLS-1$
}
}
}
private BinaryMapIndexReader getOpenFile() {
return resource.getReader(BinaryMapReaderResourceType.ADDRESS);
}
public City getClosestCity(LatLon l, List<City> cache) {
City closest = null;
if (l != null) {
int y31 = MapUtils.get31TileNumberY(l.getLatitude());
int x31 = MapUtils.get31TileNumberX(l.getLongitude());
int dz = (31 - ZOOM_QTREE);
if (cache == null) {
cache = new ArrayList<City>();
}
cache.clear();
citiesQtree.queryInBox(new QuadRect((x31 >> dz) - 1, (y31 >> dz) - 1, (x31 >> dz) + 1, (y31 >> dz) + 1),
cache);
int min = -1;
for (City c : cache) {
double d = MapUtils.getDistance(l, c.getLocation());
if (min == -1 || d < min) {
min = (int) d;
closest = c;
}
}
}
return closest;
}
@Override
public synchronized void preloadBuildings(Street street, ResultMatcher<Building> resultMatcher) {
if (street.getBuildings().isEmpty() && street.getIntersectedStreets().isEmpty()) {
try {
getOpenFile().preloadBuildings(street, BinaryMapIndexReader.buildAddressRequest(resultMatcher));
street.sortBuildings();
} catch (IOException e) {
log.error("Disk operation failed", e); //$NON-NLS-1$
}
}
}
@Override
public void addCityToPreloadedList(City city) {
if (!cities.containsKey(city.getId())) {
LinkedHashMap<Long, City> ncities = new LinkedHashMap<Long, City>(cities);
ncities.put(city.getId(), city);
cities = ncities;
}
}
@Override
public List<City> getLoadedCities() {
return new ArrayList<City>(cities.values());
}
@Override
public synchronized void preloadStreets(City o, ResultMatcher<Street> resultMatcher) {
Collection<Street> streets = o.getStreets();
if (!streets.isEmpty()) {
return;
}
try {
getOpenFile().preloadStreets(o, BinaryMapIndexReader.buildAddressRequest(resultMatcher));
} catch (IOException e) {
log.error("Disk operation failed", e); //$NON-NLS-1$
}
}
// // not use contains 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};
public synchronized List<MapObject> searchMapObjectsByName(String name, ResultMatcher<MapObject> resultMatcher, List<Integer> typeFilter) {
SearchRequest<MapObject> req = BinaryMapIndexReader.buildAddressByNameRequest(resultMatcher, name,
StringMatcherMode.CHECK_STARTS_FROM_SPACE);
try {
getOpenFile().searchAddressDataByName(req, typeFilter);
} catch (IOException e) {
log.error("Disk operation failed", e); //$NON-NLS-1$
}
return req.getSearchResults();
}
@Override
public synchronized List<MapObject> searchMapObjectsByName(String name, ResultMatcher<MapObject> resultMatcher) {
return searchMapObjectsByName(name, resultMatcher, null);
}
private List<City> fillWithCities(String name, final ResultMatcher<City> resultMatcher, final List<Integer> typeFilter) throws IOException {
List<City> result = new ArrayList<City>();
ResultMatcher<MapObject> matcher = new ResultMatcher<MapObject>() {
List<City> cache = new ArrayList<City>();
@Override
public boolean publish(MapObject o) {
City c = (City) o;
City.CityType type = c.getType();
if (type != null && type.ordinal() >= City.CityType.VILLAGE.ordinal()) {
if (c.getLocation() != null) {
City ct = getClosestCity(c.getLocation(), cache);
c.setClosestCity(ct);
}
}
return resultMatcher.publish(c);
}
@Override
public boolean isCancelled() {
return resultMatcher.isCancelled();
}
};
List<MapObject> foundCities = searchMapObjectsByName(name, matcher, typeFilter);
for (MapObject o : foundCities) {
result.add((City) o);
if (resultMatcher.isCancelled()) {
return result;
}
}
return result;
}
private List<Integer> getCityTypeFilter(String name, boolean searchVillages) {
List<Integer> cityTypes = new ArrayList<>();
cityTypes.add(BinaryMapAddressReaderAdapter.CITY_TOWN_TYPE);
if (searchVillages) {
cityTypes.add(BinaryMapAddressReaderAdapter.VILLAGES_TYPE);
if (name.length() >= POSTCODE_MIN_QUERY_LENGTH) {
cityTypes.add(BinaryMapAddressReaderAdapter.POSTCODES_TYPE);
}
}
return cityTypes;
}
@Override
public synchronized List<City> fillWithSuggestedCities(String name, final ResultMatcher<City> resultMatcher, boolean searchVillages, LatLon currentLocation) {
List<City> citiesToFill = new ArrayList<>(cities.values());
try {
citiesToFill.addAll(fillWithCities(name, resultMatcher, getCityTypeFilter(name, searchVillages)));
} catch (IOException e) {
log.error("Disk operation failed", e); //$NON-NLS-1$
}
return citiesToFill;
}
@Override
public String getLang() {
return langSetting.get();
}
@Override
public boolean isTransliterateNames() {
return transliterateSetting.get();
}
@Override
public List<Street> getStreetsIntersectStreets(Street st) {
preloadBuildings(st, null);
return st.getIntersectedStreets();
}
@Override
public Building getBuildingByName(Street street, String name) {
preloadBuildings(street, null);
String lang = getLang();
boolean transliterateNames = isTransliterateNames();
for (Building b : street.getBuildings()) {
String bName = b.getName(lang, transliterateNames);
if (bName.equals(name)) {
return b;
}
}
return null;
}
@Override
public String getName() {
String fileName = getFileName();
if (fileName.indexOf('.') != -1) {
return fileName.substring(0, fileName.indexOf('.'));
}
return fileName;
}
@Override
public String getCountryName() {
return resource.getShallowReader().getCountryName();
}
@Override
public String getFileName() {
return resource.getFileName();
}
@Override
public String toString() {
return getName() + " repository";
}
@Override
public City getCityById(final long id, String name) {
if (id == -1) {
// do not preload cities for that case
return null;
}
if (id < -1 && name != null) {
name = name.toUpperCase();
}
final String cmpName = name;
preloadCities(null);
if (!cities.containsKey(id)) {
try {
getOpenFile().getCities(BinaryMapIndexReader.buildAddressRequest(new ResultMatcher<City>() {
boolean canceled = false;
@Override
public boolean isCancelled() {
return canceled;
}
@Override
public boolean publish(City object) {
if (id < -1) {
if (object.getName().toUpperCase().equals(cmpName)) {
addCityToPreloadedList(object);
canceled = true;
}
} else if (object.getId() != null && object.getId().longValue() == id) {
addCityToPreloadedList((City) object);
canceled = true;
}
return false;
}
}), id < -1 ? BinaryMapAddressReaderAdapter.POSTCODES_TYPE : BinaryMapAddressReaderAdapter.VILLAGES_TYPE);
} catch (IOException e) {
log.error("Disk operation failed", e); //$NON-NLS-1$
}
}
return cities.get(id);
}
@Override
public Street getStreetByName(City o, String name) {
name = name.toLowerCase();
preloadStreets(o, null);
Collection<Street> streets = o.getStreets();
String lang = getLang();
boolean transliterateNames = isTransliterateNames();
for (Street s : streets) {
String sName = s.getName(lang, transliterateNames).toLowerCase();
if (collator.equals(sName, name)) {
return s;
}
}
return null;
}
@Override
public void clearCache() {
cities = new LinkedHashMap<Long, City>();
citiesQtree.clear();
postCodes.clear();
}
@Override
public LatLon getEstimatedRegionCenter() {
return resource.getShallowReader().getRegionCenter();
}
}