package net.osmand.plus.render;
import gnu.trove.iterator.TIntObjectIterator;
import gnu.trove.list.TLongList;
import gnu.trove.list.array.TIntArrayList;
import gnu.trove.list.array.TLongArrayList;
import gnu.trove.map.hash.TIntObjectHashMap;
import gnu.trove.set.TLongSet;
import gnu.trove.set.hash.TLongHashSet;
import java.io.File;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import net.osmand.IProgress;
import net.osmand.NativeLibrary.NativeSearchResult;
import net.osmand.PlatformUtil;
import net.osmand.ResultMatcher;
import net.osmand.binary.BinaryMapDataObject;
import net.osmand.binary.BinaryMapIndexReader;
import net.osmand.binary.BinaryMapIndexReader.MapIndex;
import net.osmand.binary.BinaryMapIndexReader.SearchRequest;
import net.osmand.binary.BinaryMapIndexReader.TagValuePair;
import net.osmand.binary.BinaryMapRouteReaderAdapter.RouteRegion;
import net.osmand.binary.BinaryMapRouteReaderAdapter.RouteSubregion;
import net.osmand.binary.BinaryMapRouteReaderAdapter.RouteTypeRule;
import net.osmand.binary.RouteDataObject;
import net.osmand.data.QuadPointDouble;
import net.osmand.data.QuadRect;
import net.osmand.data.RotatedTileBox;
import net.osmand.map.MapTileDownloader;
import net.osmand.plus.OsmandApplication;
import net.osmand.plus.OsmandPlugin;
import net.osmand.plus.OsmandSettings;
import net.osmand.plus.OsmandSettings.CommonPreference;
import net.osmand.plus.R;
import net.osmand.plus.development.OsmandDevelopmentPlugin;
import net.osmand.plus.render.OsmandRenderer.RenderingContext;
import net.osmand.plus.views.OsmandMapLayer.DrawSettings;
import net.osmand.render.RenderingRuleProperty;
import net.osmand.render.RenderingRuleSearchRequest;
import net.osmand.render.RenderingRuleStorageProperties;
import net.osmand.render.RenderingRulesStorage;
import net.osmand.util.Algorithms;
import net.osmand.util.MapAlgorithms;
import net.osmand.util.MapUtils;
import org.apache.commons.logging.Log;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.os.Handler;
import android.os.Looper;
import android.widget.Toast;
public class MapRenderRepositories {
// It is needed to not draw object twice if user have map index that intersects by boundaries
public static boolean checkForDuplicateObjectIds = true;
private final static Log log = PlatformUtil.getLog(MapRenderRepositories.class);
private final OsmandApplication context;
private final static int zoomOnlyForBasemaps = 11;
static int zoomForBaseRouteRendering = 14;
private Handler handler;
private Map<String, BinaryMapIndexReader> files = new LinkedHashMap<String, BinaryMapIndexReader>();
private Set<String> nativeFiles = new HashSet<String>();
private OsmandRenderer renderer;
// lat/lon box of requested vector data
private QuadRect cObjectsBox = new QuadRect();
private int cObjectsZoom = 0;
// cached objects in order to render rotation without reloading data from db
private List<BinaryMapDataObject> cObjects = new LinkedList<BinaryMapDataObject>();
private NativeSearchResult cNativeObjects = null;
// currently rendered box (not the same as already rendered)
// this box is checked for interrupted process or
private RotatedTileBox requestedBox = null;
// location of rendered bitmap
private RotatedTileBox prevBmpLocation = null;
// already rendered bitmap
private Bitmap prevBmp;
// to track necessity of map download (1 (if basemap) + 2 (if normal map)
private int checkedRenderedState;
private RotatedTileBox checkedBox;
// location of rendered bitmap
private RotatedTileBox bmpLocation = null;
// already rendered bitmap
private Bitmap bmp;
// Field used in C++
private boolean interrupted = false;
private int renderedState = 0; // (1 (if basemap) + 2 (if normal map)
private RenderingContext currentRenderingContext;
private RenderingContext visibleRenderingContext;
private SearchRequest<BinaryMapDataObject> searchRequest;
private OsmandSettings prefs;
public MapRenderRepositories(OsmandApplication context) {
this.context = context;
this.renderer = new OsmandRenderer(context);
handler = new Handler(Looper.getMainLooper());
prefs = context.getSettings();
}
public Context getContext() {
return context;
}
public OsmandRenderer getRenderer() {
return renderer;
}
public RenderingContext getVisibleRenderingContext() {
return visibleRenderingContext;
}
public void initializeNewResource(final IProgress progress, File file, BinaryMapIndexReader reader) {
if (files.containsKey(file.getName())) {
closeConnection(file.getName());
}
LinkedHashMap<String, BinaryMapIndexReader> cpfiles = new LinkedHashMap<String, BinaryMapIndexReader>(files);
cpfiles.put(file.getName(), reader);
files = cpfiles;
}
public RotatedTileBox getBitmapLocation() {
return bmpLocation;
}
public RotatedTileBox getPrevBmpLocation() {
return prevBmpLocation;
}
public synchronized void closeConnection(String file) {
LinkedHashMap<String, BinaryMapIndexReader> cpfiles = new LinkedHashMap<String, BinaryMapIndexReader>(files);
BinaryMapIndexReader bmir = cpfiles.remove(file);
files = cpfiles;
if (nativeFiles.contains(file)) {
NativeOsmandLibrary lib = NativeOsmandLibrary.getLoadedLibrary();
if (lib != null) {
lib.closeMapFile(bmir != null ? bmir.getFile().getAbsolutePath() : file);
nativeFiles.remove(file);
clearCache();
}
}
if (bmir != null) {
try {
bmir.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public boolean containsLatLonMapData(double lat, double lon, int zoom) {
int x = MapUtils.get31TileNumberX(lon);
int y = MapUtils.get31TileNumberY(lat);
for (BinaryMapIndexReader reader : files.values()) {
if (reader.containsMapData(x, y, zoom)) {
return true;
}
}
return false;
}
public void clearAllResources() {
clearCache();
bmp = null;
bmpLocation = null;
for (String f : new ArrayList<String>(files.keySet())) {
closeConnection(f);
}
}
public boolean updateMapIsNeeded(RotatedTileBox box, DrawSettings drawSettings) {
if (box == null) {
return false;
}
if (requestedBox == null) {
log.info("RENDER MAP: update due to start");
return true;
}
if (drawSettings.isUpdateVectorRendering()) {
log.info("RENDER MAP: update due to request");
return true;
}
if (requestedBox.getZoom() != box.getZoom() ||
requestedBox.getMapDensity() != box.getMapDensity()) {
log.info("RENDER MAP: update due zoom/map density");
return true;
}
float deltaRotate = requestedBox.getRotate() - box.getRotate();
if (deltaRotate > 180) {
deltaRotate -= 360;
} else if (deltaRotate < -180) {
deltaRotate += 360;
}
if (Math.abs(deltaRotate) > 25) {
log.info("RENDER MAP: update due to rotation");
return true;
}
boolean upd = !requestedBox.containsTileBox(box);
if(upd) {
log.info("RENDER MAP: update due to tile box");
}
return upd;
}
public boolean isEmpty() {
return files.isEmpty();
}
public void interruptLoadingMap() {
interrupted = true;
if (currentRenderingContext != null) {
currentRenderingContext.interrupted = true;
}
if (searchRequest != null) {
searchRequest.setInterrupted(true);
}
log.info("RENDER MAP: Interrupt rendering map");
}
public boolean wasInterrupted() {
return interrupted;
}
private boolean checkWhetherInterrupted() {
if (interrupted || (currentRenderingContext != null && currentRenderingContext.interrupted)) {
requestedBox = bmpLocation;
return true;
}
return false;
}
public boolean basemapExists() {
for (BinaryMapIndexReader f : files.values()) {
if (f.isBasemap()) {
return true;
}
}
return false;
}
private boolean loadVectorDataNative(QuadRect dataBox, final int zoom, final RenderingRuleSearchRequest renderingReq,
NativeOsmandLibrary library) {
int leftX = MapUtils.get31TileNumberX(dataBox.left);
int rightX = MapUtils.get31TileNumberX(dataBox.right);
int bottomY = MapUtils.get31TileNumberY(dataBox.bottom);
int topY = MapUtils.get31TileNumberY(dataBox.top);
long now = System.currentTimeMillis();
// check that everything is initialized
checkInitialized(zoom, library, leftX, rightX, bottomY, topY);
NativeSearchResult resultHandler = library.searchObjectsForRendering(leftX, rightX, topY, bottomY, zoom, renderingReq,
checkForDuplicateObjectIds, this, "");
if (checkWhetherInterrupted()) {
resultHandler.deleteNativeResult();
return false;
}
if(cNativeObjects != null) {
cNativeObjects.deleteNativeResult();
}
cNativeObjects = resultHandler;
cObjectsBox = dataBox;
cObjectsZoom = zoom;
log.info(String.format("BLat=%s, TLat=%s, LLong=%s, RLong=%s, zoom=%s", //$NON-NLS-1$
dataBox.bottom, dataBox.top, dataBox.left, dataBox.right, zoom));
log.info(String.format("Native search: %s ms ", System.currentTimeMillis() - now)); //$NON-NLS-1$
return true;
}
public void checkInitialized(final int zoom, NativeOsmandLibrary library, int leftX, int rightX, int bottomY,
int topY) {
if(library == null) {
return;
}
for (String mapName : files.keySet()) {
BinaryMapIndexReader fr = files.get(mapName);
if (fr != null && (fr.containsMapData(leftX, topY, rightX, bottomY, zoom) ||
fr.containsRouteData(leftX, topY, rightX, bottomY, zoom))) {
if (!nativeFiles.contains(mapName)) {
long time = System.currentTimeMillis();
nativeFiles.add(mapName);
if (!library.initMapFile(fr.getFile().getAbsolutePath())) {
continue;
}
log.debug("Native resource " + mapName + " initialized " + (System.currentTimeMillis() - time) + " ms"); //$NON-NLS-1$ //$NON-NLS-2$
}
}
}
}
private void readRouteDataAsMapObjects(SearchRequest<BinaryMapDataObject> sr, BinaryMapIndexReader c,
final ArrayList<BinaryMapDataObject> tempResult, final TLongSet ids) {
final boolean basemap = c.isBasemap();
try {
for (RouteRegion reg : c.getRoutingIndexes()) {
List<RouteSubregion> parent = sr.getZoom() < 15 ? reg.getBaseSubregions() : reg.getSubregions();
List<RouteSubregion> searchRouteIndexTree = c.searchRouteIndexTree(sr, parent);
final MapIndex nmi = new MapIndex();
c.loadRouteIndexData(searchRouteIndexTree, new ResultMatcher<RouteDataObject>() {
@Override
public boolean publish(RouteDataObject r) {
if (basemap) {
renderedState |= 1;
} else {
renderedState |= 2;
}
if (checkForDuplicateObjectIds && !basemap) {
if (ids.contains(r.getId()) && r.getId() > 0) {
// do not add object twice
return false;
}
ids.add(r.getId());
}
int[] coordinantes = new int[r.getPointsLength() * 2];
int[] roTypes = r.getTypes();
for(int k = 0; k < roTypes.length; k++) {
int type = roTypes[k];
registerMissingType(nmi, r, type);
}
for(int k = 0; k < coordinantes.length/2; k++ ) {
coordinantes[2 * k] = r.getPoint31XTile(k);
coordinantes[2 * k + 1] = r.getPoint31YTile(k);
}
BinaryMapDataObject mo = new BinaryMapDataObject(coordinantes, roTypes, new int[0][], r.getId());
TIntObjectHashMap<String> names = r.getNames();
if(names != null) {
TIntObjectIterator<String> it = names.iterator();
while(it.hasNext()) {
it.advance();
registerMissingType(nmi, r, it.key());
mo.putObjectName(it.key(), it.value());
}
}
mo.setMapIndex(nmi);
tempResult.add(mo);
return false;
}
private void registerMissingType(final MapIndex nmi, RouteDataObject r, int type) {
if (!nmi.isRegisteredRule(type)) {
RouteTypeRule rr = r.region.quickGetEncodingRule(type);
String tag = rr.getTag();
int additional = ("highway".equals(tag) || "route".equals(tag) || "railway".equals(tag)
|| "aeroway".equals(tag) || "aerialway".equals(tag)) ? 0 : 1;
nmi.initMapEncodingRule(additional, type, rr.getTag(), rr.getValue());
}
}
@Override
public boolean isCancelled() {
return !interrupted;
}
});
}
} catch (IOException e) {
log.debug("Search failed " + c.getRegionNames(), e); //$NON-NLS-1$
}
}
private boolean loadVectorData(QuadRect dataBox, final int zoom, final RenderingRuleSearchRequest renderingReq) {
double cBottomLatitude = dataBox.bottom;
double cTopLatitude = dataBox.top;
double cLeftLongitude = dataBox.left;
double cRightLongitude = dataBox.right;
long now = System.currentTimeMillis();
System.gc(); // to clear previous objects
ArrayList<BinaryMapDataObject> tempResult = new ArrayList<BinaryMapDataObject>();
ArrayList<BinaryMapDataObject> basemapResult = new ArrayList<BinaryMapDataObject>();
int[] count = new int[]{0};
boolean[] ocean = new boolean[]{false};
boolean[] land = new boolean[]{false};
List<BinaryMapDataObject> coastLines = new ArrayList<BinaryMapDataObject>();
List<BinaryMapDataObject> basemapCoastLines = new ArrayList<BinaryMapDataObject>();
int leftX = MapUtils.get31TileNumberX(cLeftLongitude);
int rightX = MapUtils.get31TileNumberX(cRightLongitude);
int bottomY = MapUtils.get31TileNumberY(cBottomLatitude);
int topY = MapUtils.get31TileNumberY(cTopLatitude);
TLongSet ids = new TLongHashSet();
MapIndex mi = readMapObjectsForRendering(zoom, renderingReq, tempResult, basemapResult, ids, count, ocean,
land, coastLines, basemapCoastLines, leftX, rightX, bottomY, topY);
int renderRouteDataFile = 0;
if (renderingReq.searchRenderingAttribute("showRoadMapsAttribute")) {
renderRouteDataFile = renderingReq.getIntPropertyValue(renderingReq.ALL.R_ATTR_INT_VALUE);
}
if (checkWhetherInterrupted()) {
return false;
}
boolean objectsFromMapSectionRead = tempResult.size() > 0;
if (renderRouteDataFile >= 0 && zoom >= zoomOnlyForBasemaps ) {
searchRequest = BinaryMapIndexReader.buildSearchRequest(leftX, rightX, topY, bottomY, zoom, null);
for (BinaryMapIndexReader c : files.values()) {
// false positive case when we have 2 sep maps Country-roads & Country
if(c.getMapIndexes().size() == 0 || renderRouteDataFile == 1) {
readRouteDataAsMapObjects(searchRequest, c, tempResult, ids);
}
}
log.info(String.format("Route objects %s", tempResult.size() +""));
}
String coastlineTime = "";
boolean addBasemapCoastlines = true;
boolean emptyData = zoom > zoomOnlyForBasemaps && tempResult.isEmpty() && coastLines.isEmpty();
boolean basemapMissing = zoom <= zoomOnlyForBasemaps && basemapCoastLines.isEmpty() && mi == null;
boolean detailedLandData = zoom >= zoomForBaseRouteRendering && tempResult.size() > 0 && objectsFromMapSectionRead;
if (!coastLines.isEmpty()) {
long ms = System.currentTimeMillis();
boolean coastlinesWereAdded = processCoastlines(coastLines, leftX, rightX, bottomY, topY, zoom,
basemapCoastLines.isEmpty(), true, tempResult);
addBasemapCoastlines = (!coastlinesWereAdded && !detailedLandData) || zoom <= zoomOnlyForBasemaps;
coastlineTime = "(coastline " + (System.currentTimeMillis() - ms) + " ms )";
} else {
addBasemapCoastlines = !detailedLandData;
}
if (addBasemapCoastlines) {
long ms = System.currentTimeMillis();
boolean coastlinesWereAdded = processCoastlines(basemapCoastLines, leftX, rightX, bottomY, topY, zoom,
true, true, tempResult);
addBasemapCoastlines = !coastlinesWereAdded;
coastlineTime = "(coastline " + (System.currentTimeMillis() - ms) + " ms )";
}
if (addBasemapCoastlines && mi != null) {
BinaryMapDataObject o = new BinaryMapDataObject(new int[]{leftX, topY, rightX, topY, rightX, bottomY, leftX, bottomY, leftX,
topY}, new int[]{ocean[0] && !land[0] ? mi.coastlineEncodingType : (mi.landEncodingType)}, null, -1);
o.setMapIndex(mi);
tempResult.add(o);
}
if (emptyData || basemapMissing) {
// message
MapIndex mapIndex;
if (!tempResult.isEmpty()) {
mapIndex = tempResult.get(0).getMapIndex();
} else {
mapIndex = new MapIndex();
mapIndex.initMapEncodingRule(0, 1, "natural", "coastline");
mapIndex.initMapEncodingRule(0, 2, "name", "");
}
}
if (zoom <= zoomOnlyForBasemaps || emptyData) {
tempResult.addAll(basemapResult);
}
if (count[0] > 0) {
log.info(String.format("BLat=%s, TLat=%s, LLong=%s, RLong=%s, zoom=%s", //$NON-NLS-1$
cBottomLatitude, cTopLatitude, cLeftLongitude, cRightLongitude, zoom));
log.info(String.format("Searching: %s ms %s (%s results found)", System.currentTimeMillis() - now, coastlineTime, count[0])); //$NON-NLS-1$
}
cObjects = tempResult;
cObjectsBox = dataBox;
cObjectsZoom = zoom;
return true;
}
private MapIndex readMapObjectsForRendering(final int zoom, final RenderingRuleSearchRequest renderingReq,
ArrayList<BinaryMapDataObject> tempResult, ArrayList<BinaryMapDataObject> basemapResult,
TLongSet ids, int[] count, boolean[] ocean, boolean[] land, List<BinaryMapDataObject> coastLines,
List<BinaryMapDataObject> basemapCoastLines, int leftX, int rightX, int bottomY, int topY) {
BinaryMapIndexReader.SearchFilter searchFilter = new BinaryMapIndexReader.SearchFilter() {
@Override
public boolean accept(TIntArrayList types, BinaryMapIndexReader.MapIndex root) {
for (int j = 0; j < types.size(); j++) {
int type = types.get(j);
TagValuePair pair = root.decodeType(type);
if (pair != null) {
// TODO is it fast enough ?
for (int i = 1; i <= 3; i++) {
renderingReq.setIntFilter(renderingReq.ALL.R_MINZOOM, zoom);
renderingReq.setStringFilter(renderingReq.ALL.R_TAG, pair.tag);
renderingReq.setStringFilter(renderingReq.ALL.R_VALUE, pair.value);
if (renderingReq.search(i, false)) {
return true;
}
}
renderingReq.setStringFilter(renderingReq.ALL.R_TAG, pair.tag);
renderingReq.setStringFilter(renderingReq.ALL.R_VALUE, pair.value);
if (renderingReq.search(RenderingRulesStorage.TEXT_RULES, false)) {
return true;
}
}
}
return false;
}
};
if (zoom > 16) {
searchFilter = null;
}
MapIndex mi = null;
searchRequest = BinaryMapIndexReader.buildSearchRequest(leftX, rightX, topY, bottomY, zoom, searchFilter);
for (BinaryMapIndexReader c : files.values()) {
boolean basemap = c.isBasemap();
searchRequest.clearSearchResults();
List<BinaryMapDataObject> res;
try {
res = c.searchMapIndex(searchRequest);
} catch (IOException e) {
res = new ArrayList<BinaryMapDataObject>();
log.debug("Search failed " + c.getRegionNames(), e); //$NON-NLS-1$
}
if(res.size() > 0) {
if(basemap) {
renderedState |= 1;
} else {
renderedState |= 2;
}
}
for (BinaryMapDataObject r : res) {
if (checkForDuplicateObjectIds && !basemap) {
if (ids.contains(r.getId()) && r.getId() > 0) {
// do not add object twice
continue;
}
ids.add(r.getId());
}
count[0]++;
if (r.containsType(r.getMapIndex().coastlineEncodingType)) {
if (basemap) {
basemapCoastLines.add(r);
} else {
coastLines.add(r);
}
} else {
// do not mess coastline and other types
if (basemap) {
basemapResult.add(r);
} else {
tempResult.add(r);
}
}
if (checkWhetherInterrupted()) {
return null;
}
}
if (searchRequest.isOcean()) {
mi = c.getMapIndexes().get(0);
ocean[0] = true;
}
if (searchRequest.isLand()) {
mi = c.getMapIndexes().get(0);
land[0] = true;
}
}
return mi;
}
private void validateLatLonBox(QuadRect box) {
if (box.top > 90) {
box.top = 85.5f;
}
if (box.bottom < -90) {
box.bottom = -85.5f;
}
if (box.left <= -180) {
box.left = -179.5f;
}
if (box.right > 180) {
box.right = 180.0f;
}
}
public RotatedTileBox getCheckedBox() {
return checkedBox;
}
public int getCheckedRenderedState() {
// to track necessity of map download (1 (if basemap) + 2 (if normal map)
return checkedRenderedState;
}
public synchronized void loadMap(RotatedTileBox tileRect, MapTileDownloader mapTileDownloader) {
boolean prevInterrupted = interrupted;
interrupted = false;
// added to avoid zoomAnimation != 0 which produces wrong map position on the screen
tileRect.setZoomAndAnimation(tileRect.getZoom(), 0);
// prevent editing
requestedBox = new RotatedTileBox(tileRect);
log.info("RENDER MAP: new request " + tileRect );
if (currentRenderingContext != null) {
currentRenderingContext = null;
}
try {
// find selected rendering type
OsmandApplication app = ((OsmandApplication) context.getApplicationContext());
boolean nightMode = app.getDaynightHelper().isNightMode();
// boolean moreDetail = prefs.SHOW_MORE_MAP_DETAIL.get();
RenderingRulesStorage storage = app.getRendererRegistry().getCurrentSelectedRenderer();
RenderingRuleSearchRequest renderingReq = new RenderingRuleSearchRequest(storage);
renderingReq.setBooleanFilter(renderingReq.ALL.R_NIGHT_MODE, nightMode);
for (RenderingRuleProperty customProp : storage.PROPS.getCustomRules()) {
if (customProp.isBoolean()) {
if(customProp.getAttrName().equals(RenderingRuleStorageProperties.A_ENGINE_V1)) {
renderingReq.setBooleanFilter(customProp, true);
} else if (RenderingRuleStorageProperties.UI_CATEGORY_HIDDEN.equals(customProp.getCategory())) {
renderingReq.setBooleanFilter(customProp, false);
} else {
CommonPreference<Boolean> pref = prefs.getCustomRenderBooleanProperty(customProp.getAttrName());
renderingReq.setBooleanFilter(customProp, pref.get());
}
} else if (RenderingRuleStorageProperties.UI_CATEGORY_HIDDEN.equals(customProp.getCategory())) {
if (customProp.isString()) {
renderingReq.setStringFilter(customProp, "");
} else {
renderingReq.setIntFilter(customProp, 0);
}
} else {
CommonPreference<String> settings = prefs.getCustomRenderProperty(customProp.getAttrName());
String res = settings.get();
if (!Algorithms.isEmpty(res)) {
if (customProp.isString()) {
renderingReq.setStringFilter(customProp, res);
} else {
try {
renderingReq.setIntFilter(customProp, Integer.parseInt(res));
} catch (NumberFormatException e) {
e.printStackTrace();
}
}
} else {
if (customProp.isString()) {
renderingReq.setStringFilter(customProp, "");
}
}
}
}
renderingReq.saveState();
NativeOsmandLibrary nativeLib = !prefs.SAFE_MODE.get() ? NativeOsmandLibrary.getLibrary(storage, context) : null;
// calculate data box
QuadRect dataBox = requestedBox.getLatLonBounds();
int dataBoxZoom = requestedBox.getZoom();
long now = System.currentTimeMillis();
if (cObjectsBox.left > dataBox.left || cObjectsBox.top < dataBox.top || cObjectsBox.right < dataBox.right
|| cObjectsBox.bottom > dataBox.bottom || (nativeLib != null) == (cNativeObjects == null)
|| dataBoxZoom != cObjectsZoom || prevInterrupted) {
// increase data box in order for rotate
if ((dataBox.right - dataBox.left) > (dataBox.top - dataBox.bottom)) {
double wi = (dataBox.right - dataBox.left) * .05;
dataBox.left -= wi;
dataBox.right += wi;
} else {
double hi = (dataBox.top - dataBox.bottom) * .05;
dataBox.top += hi;
dataBox.bottom -= hi;
}
validateLatLonBox(dataBox);
renderedState = 0;
boolean loaded;
if(nativeLib != null) {
cObjects = new LinkedList<BinaryMapDataObject>();
loaded = loadVectorDataNative(dataBox, requestedBox.getZoom(), renderingReq, nativeLib);
} else {
cNativeObjects = null;
loaded = loadVectorData(dataBox, requestedBox.getZoom(), renderingReq);
}
if (!loaded || checkWhetherInterrupted()) {
return;
}
}
final long searchTime = System.currentTimeMillis() - now;
currentRenderingContext = new OsmandRenderer.RenderingContext(context);
renderingReq.clearState();
renderingReq.setIntFilter(renderingReq.ALL.R_MINZOOM, requestedBox.getZoom());
if(renderingReq.searchRenderingAttribute(RenderingRuleStorageProperties.A_DEFAULT_COLOR)) {
currentRenderingContext.defaultColor = renderingReq.getIntPropertyValue(renderingReq.ALL.R_ATTR_COLOR_VALUE);
}
renderingReq.clearState();
renderingReq.setIntFilter(renderingReq.ALL.R_MINZOOM, requestedBox.getZoom());
if(renderingReq.searchRenderingAttribute(RenderingRuleStorageProperties.A_SHADOW_RENDERING)) {
currentRenderingContext.shadowRenderingMode = renderingReq.getIntPropertyValue(renderingReq.ALL.R_ATTR_INT_VALUE);
currentRenderingContext.shadowRenderingColor = renderingReq.getIntPropertyValue(renderingReq.ALL.R_SHADOW_COLOR);
}
if(renderingReq.searchRenderingAttribute("polygonMinSizeToDisplay")) {
currentRenderingContext.polygonMinSizeToDisplay = renderingReq.getIntPropertyValue(renderingReq.ALL.R_ATTR_INT_VALUE);
}
final QuadPointDouble lt = requestedBox.getLeftTopTile(requestedBox.getZoom());
double cfd = MapUtils.getPowZoom(requestedBox.getZoomFloatPart())* requestedBox.getMapDensity();
lt.x *= cfd;
lt.y *= cfd;
// LatLon ltn = requestedBox.getLeftTopLatLon();
final double tileDivisor = MapUtils.getPowZoom(31 - requestedBox.getZoom()) / cfd;
currentRenderingContext.leftX = lt.x;
currentRenderingContext.topY = lt.y;
currentRenderingContext.zoom = requestedBox.getZoom();
currentRenderingContext.rotate = requestedBox.getRotate();
currentRenderingContext.width = requestedBox.getPixWidth();
currentRenderingContext.height = requestedBox.getPixHeight();
currentRenderingContext.nightMode = nightMode;
if(requestedBox.getZoom() <= zoomOnlyForBasemaps &&
"".equals(prefs.MAP_PREFERRED_LOCALE.get())) {
currentRenderingContext.preferredLocale = app.getLanguage();
currentRenderingContext.transliterate = !"ru".equals(app.getLanguage()) && !"uk".equals(app.getLanguage()) && !"be".equals(app.getLanguage())
&& !"bg".equals(app.getLanguage()) && !"mk".equals(app.getLanguage()) && !"sr".equals(app.getLanguage());
} else {
currentRenderingContext.preferredLocale = prefs.MAP_PREFERRED_LOCALE.get();
currentRenderingContext.transliterate = prefs.MAP_TRANSLITERATE_NAMES.get();
if(currentRenderingContext.preferredLocale.equals("en")) {
currentRenderingContext.transliterate = true;
}
}
final float mapDensity = (float) requestedBox.getMapDensity();
currentRenderingContext.setDensityValue(mapDensity);
//Text/icon scales according to mapDensity (so text is size of road)
// currentRenderingContext.textScale = (requestedBox.getDensity()*app.getSettings().TEXT_SCALE.get());
//Text/icon stays same for all sizes
currentRenderingContext.textScale = (requestedBox.getDensity() * app.getSettings().TEXT_SCALE.get())
/ mapDensity;
currentRenderingContext.screenDensityRatio = 1 / Math.max(1, requestedBox.getDensity()) ;
// init rendering context
currentRenderingContext.tileDivisor = tileDivisor;
if (checkWhetherInterrupted()) {
return;
}
now = System.currentTimeMillis();
Bitmap bmp;
boolean transparent = false;
RenderingRuleProperty rr = storage.PROPS.get("noPolygons");
if (rr != null) {
transparent = renderingReq.getIntPropertyValue(rr) > 0;
}
// 1. generate image step by step
Bitmap reuse = prevBmp;
this.prevBmp = this.bmp;
this.prevBmpLocation = this.bmpLocation;
// necessary for transparent, otherwise 2 times smaller
Config cfg = transparent ? Config.ARGB_8888 : Config.RGB_565;
if (reuse != null && reuse.getWidth() == currentRenderingContext.width && reuse.getHeight() == currentRenderingContext.height &&
cfg == reuse.getConfig()) {
bmp = reuse;
bmp.eraseColor(currentRenderingContext.defaultColor);
} else {
if(reuse != null){
log.warn(String.format("Create new image ? %d != %d (w) %d != %d (h) ", currentRenderingContext.width, reuse.getWidth(), currentRenderingContext.height, reuse.getHeight()));
}
bmp = Bitmap.createBitmap(currentRenderingContext.width, currentRenderingContext.height, cfg);
if(reuse != null) {
reuse.recycle();
}
}
this.bmp = bmp;
this.bmpLocation = tileRect;
if(nativeLib != null) {
renderer.generateNewBitmapNative(currentRenderingContext, nativeLib, cNativeObjects, bmp, renderingReq, mapTileDownloader);
} else {
renderer.generateNewBitmap(currentRenderingContext, cObjects, bmp, renderingReq, mapTileDownloader);
}
// Force to use rendering request in order to prevent Garbage Collector when it is used in C++
if(renderingReq != null){
log.info("Debug :" + renderingReq != null);
}
String renderingDebugInfo = currentRenderingContext.renderingDebugInfo;
currentRenderingContext.ended = true;
if (checkWhetherInterrupted()) {
// revert if it was interrupted
// (be smart a bit do not revert if road already drawn)
if(currentRenderingContext.lastRenderedKey < OsmandRenderer.DEFAULT_LINE_MAX) {
reuse = this.bmp;
this.bmp = this.prevBmp;
this.bmpLocation = this.prevBmpLocation;
this.prevBmp = reuse;
this.prevBmpLocation = null;
}
currentRenderingContext = null;
return;
} else {
visibleRenderingContext = currentRenderingContext;
this.checkedRenderedState = renderedState;
this.checkedBox = this.bmpLocation;
}
currentRenderingContext = null;
// 2. replace whole image
// keep cache
// this.prevBmp = null;
this.prevBmpLocation = null;
if (prefs.DEBUG_RENDERING_INFO.get() && OsmandPlugin.getEnabledPlugin(OsmandDevelopmentPlugin.class) != null) {
String timeInfo = "Searching: " + searchTime + " ms"; //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$
if (renderingDebugInfo != null) {
timeInfo += "\n" + renderingDebugInfo;
}
final String msg = timeInfo;
log.info(msg);
handler.post(new Runnable() {
@Override
public void run() {
Toast.makeText(context, msg, Toast.LENGTH_LONG).show();
}
});
}
} catch (RuntimeException e) {
log.error("Runtime memory exception", e); //$NON-NLS-1$
handler.post(new Runnable() {
@Override
public void run() {
Toast.makeText(context, R.string.rendering_exception, Toast.LENGTH_SHORT).show();
}
});
} catch (OutOfMemoryError e) {
log.error("Out of memory error", e); //$NON-NLS-1$
cObjects = new ArrayList<BinaryMapDataObject>();
cObjectsBox = new QuadRect();
handler.post(new Runnable() {
@Override
public void run() {
// ActivityManager activityManager = (ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE);
// ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
// activityManager.getMemoryInfo(memoryInfo);
// int avl = (int) (memoryInfo.availMem / (1 << 20));
int max = (int) (Runtime.getRuntime().maxMemory() / (1 << 20));
int avl = (int) (Runtime.getRuntime().freeMemory() / (1 << 20));
String s = " (" + avl + " MB available of " + max + ") ";
Toast.makeText(context, context.getString(R.string.rendering_out_of_memory) + s , Toast.LENGTH_SHORT).show();
}
});
} finally {
if(currentRenderingContext != null) {
currentRenderingContext.ended = true;
}
}
}
public Bitmap getBitmap() {
return bmp;
}
public Bitmap getPrevBitmap() {
return prevBmpLocation == null ? null : prevBmp ;
}
public synchronized void clearCache() {
cObjects = new ArrayList<BinaryMapDataObject>();
cObjectsBox = new QuadRect();
requestedBox = prevBmpLocation = null;
// Do not clear main bitmap to not cause a screen refresh
// prevBmp = null;
// bmp = null;
// bmpLocation = null;
}
public Map<String, BinaryMapIndexReader> getMetaInfoFiles() {
return files;
}
/// MULTI POLYGONS (coastline)
// returns true if coastlines were added!
private boolean processCoastlines(List<BinaryMapDataObject> coastLines, int leftX, int rightX,
int bottomY, int topY, int zoom, boolean doNotAddIfIncompleted, boolean addDebugIncompleted, List<BinaryMapDataObject> result) {
List<TLongList> completedRings = new ArrayList<TLongList>();
List<TLongList> uncompletedRings = new ArrayList<TLongList>();
MapIndex mapIndex = null;
long dbId = 0;
for (BinaryMapDataObject o : coastLines) {
int len = o.getPointsLength();
if (len < 2) {
continue;
}
mapIndex = o.getMapIndex();
dbId = o.getId() >> 1;
TLongList coordinates = new TLongArrayList(o.getPointsLength() / 2);
int px = o.getPoint31XTile(0);
int py = o.getPoint31YTile(0);
int x = px;
int y = py;
boolean pinside = leftX <= x && x <= rightX && y >= topY && y <= bottomY;
if (pinside) {
coordinates.add(combine2Points(x, y));
}
for (int i = 1; i < len; i++) {
x = o.getPoint31XTile(i);
y = o.getPoint31YTile(i);
boolean inside = leftX <= x && x <= rightX && y >= topY && y <= bottomY;
boolean lineEnded = calculateLineCoordinates(inside, x, y, pinside, px, py, leftX, rightX, bottomY, topY, coordinates);
if (lineEnded) {
combineMultipolygonLine(completedRings, uncompletedRings, coordinates);
// create new line if it goes outside
coordinates = new TLongArrayList();
}
px = x;
py = y;
pinside = inside;
}
combineMultipolygonLine(completedRings, uncompletedRings, coordinates);
}
if (completedRings.size() == 0 && uncompletedRings.size() == 0) {
return false;
}
if (uncompletedRings.size() > 0) {
unifyIncompletedRings(uncompletedRings, completedRings, leftX, rightX, bottomY, topY, dbId, zoom);
}
long mask = 0xffffffffL;
// draw uncompleted for debug purpose
for (int i = 0; i < uncompletedRings.size(); i++) {
TLongList ring = uncompletedRings.get(i);
int[] coordinates = new int[ring.size() * 2];
for (int j = 0; j < ring.size(); j++) {
coordinates[j * 2] = (int) (ring.get(j) >> 32);
coordinates[j * 2 + 1] = (int) (ring.get(j) & mask);
}
BinaryMapDataObject o = new BinaryMapDataObject(coordinates, new int[] { mapIndex.coastlineBrokenEncodingType }, null, dbId);
o.setMapIndex(mapIndex);
result.add(o);
}
if(!doNotAddIfIncompleted && uncompletedRings.size() > 0){
return false;
}
boolean clockwiseFound = false;
for (int i = 0; i < completedRings.size(); i++) {
TLongList ring = completedRings.get(i);
int[] coordinates = new int[ring.size() * 2];
for (int j = 0; j < ring.size(); j++) {
coordinates[j * 2] = (int) (ring.get(j) >> 32);
coordinates[j * 2 + 1] = (int) (ring.get(j) & mask);
}
boolean clockwise = MapAlgorithms.isClockwiseWay(ring);
clockwiseFound = clockwiseFound || clockwise;
BinaryMapDataObject o = new BinaryMapDataObject(coordinates, new int[] { clockwise ? mapIndex.coastlineEncodingType
: mapIndex.landEncodingType }, null, dbId);
o.setMapIndex(mapIndex);
o.setArea(true);
result.add(o);
}
if (!clockwiseFound && uncompletedRings.size() == 0) {
// add complete water tile
BinaryMapDataObject o = new BinaryMapDataObject(new int[] { leftX, topY, rightX, topY, rightX, bottomY, leftX, bottomY, leftX,
topY }, new int[] { mapIndex.coastlineEncodingType }, null, dbId);
o.setMapIndex(mapIndex);
log.info("!!! Isolated islands !!!"); //$NON-NLS-1$
result.add(o);
}
return true;
}
private boolean eq(long i1, long i2){
return i1 == i2;
}
private void combineMultipolygonLine(List<TLongList> completedRings, List<TLongList> incompletedRings, TLongList coordinates) {
if (coordinates.size() > 0) {
if (eq(coordinates.get(0), coordinates.get(coordinates.size() - 1))) {
completedRings.add(coordinates);
} else {
boolean add = true;
for (int k = 0; k < incompletedRings.size();) {
boolean remove = false;
TLongList i = incompletedRings.get(k);
if (eq(coordinates.get(0), i.get(i.size() - 1))) {
i.addAll(coordinates.subList(1, coordinates.size()));
remove = true;
coordinates = i;
} else if (eq(coordinates.get(coordinates.size() - 1), i.get(0))) {
coordinates.addAll(i.subList(1, i.size()));
remove = true;
}
if (remove) {
incompletedRings.remove(k);
} else {
k++;
}
if (eq(coordinates.get(0), coordinates.get(coordinates.size() - 1))) {
completedRings.add(coordinates);
add = false;
break;
}
}
if (add) {
incompletedRings.add(coordinates);
}
}
}
}
private void unifyIncompletedRings(List<TLongList> toProcces, List<TLongList> completedRings, int leftX, int rightX, int bottomY, int topY, long dbId, int zoom) {
int mask = 0xffffffff;
List<TLongList> uncompletedRings = new ArrayList<TLongList>(toProcces);
toProcces.clear();
Set<Integer> nonvisitedRings = new LinkedHashSet<Integer>();
for (int j = 0; j < uncompletedRings.size(); j++) {
TLongList i = uncompletedRings.get(j);
int x = (int) (i.get(i.size() - 1) >> 32);
int y = (int) (i.get(i.size() - 1) & mask);
int sx = (int) (i.get(0) >> 32);
int sy = (int) (i.get(0) & mask);
boolean st = y == topY || x == rightX || y == bottomY || x == leftX;
boolean end = sy == topY || sx == rightX || sy == bottomY || sx == leftX;
// something goes wrong
// These exceptions are used to check logic about processing multipolygons
// However this situation could happen because of broken multipolygons (so it should data causes app error)
// that's why these exceptions could be replaced with return; statement.
if (!end || !st) {
float dx = (float) MapUtils.get31LongitudeX(x);
float dsx = (float) MapUtils.get31LongitudeX(sx);
float dy = (float) MapUtils.get31LatitudeY(y);
float dsy = (float) MapUtils.get31LatitudeY(sy);
String str;
if (!end) {
str = " Starting point (to close) not found : end_x = {0}, end_y = {1}, start_x = {2}, start_y = {3} : bounds {4} {5} - {6} {7}"; //$NON-NLS-1$
System.err
.println(MessageFormat.format(dbId + str, dx, dy, dsx, dsy, leftX + "", topY + "", rightX + "", bottomY + "")); //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$//$NON-NLS-4$
}
if (!st) {
str = " End not found : end_x = {0}, end_y = {1}, start_x = {2}, start_y = {3} : bounds {4} {5} - {6} {7}"; //$NON-NLS-1$
System.err
.println(MessageFormat.format(dbId + str, dx, dy, dsx, dsy, leftX + "", topY + "", rightX + "", bottomY + "")); //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$//$NON-NLS-4$
}
toProcces.add(i);
} else {
nonvisitedRings.add(j);
}
}
for (int j = 0; j < uncompletedRings.size(); j++) {
TLongList i = uncompletedRings.get(j);
if (!nonvisitedRings.contains(j)) {
continue;
}
int x = (int) (i.get(i.size() - 1) >> 32);
int y = (int) (i.get(i.size() - 1) & mask);
// 31 - (zoom + 8)
int EVAL_DELTA = 6 << (23 - zoom);
int UNDEFINED_MIN_DIFF = -1 - EVAL_DELTA;
while (true) {
int st = 0; // st already checked to be one of the four
if (y == topY) {
st = 0;
} else if (x == rightX) {
st = 1;
} else if (y == bottomY) {
st = 2;
} else if (x == leftX) {
st = 3;
}
int nextRingIndex = -1;
// BEGIN go clockwise around rectangle
for (int h = st; h < st + 4; h++) {
// BEGIN find closest nonvisited start (including current)
int mindiff = UNDEFINED_MIN_DIFF;
for (Integer ni : nonvisitedRings) {
TLongList cni = uncompletedRings.get(ni);
int csx = (int) (cni.get(0) >> 32);
int csy = (int) (cni.get(0) & mask);
if (h % 4 == 0) {
// top
if (csy == topY && csx >= safelyAddDelta(x, -EVAL_DELTA)) {
if (mindiff == UNDEFINED_MIN_DIFF || (csx - x) <= mindiff) {
mindiff = (csx - x);
nextRingIndex = ni;
}
}
} else if (h % 4 == 1) {
// right
if (csx == rightX && csy >= safelyAddDelta(y, -EVAL_DELTA)) {
if (mindiff == UNDEFINED_MIN_DIFF || (csy - y) <= mindiff) {
mindiff = (csy - y);
nextRingIndex = ni;
}
}
} else if (h % 4 == 2) {
// bottom
if (csy == bottomY && csx <= safelyAddDelta(x, EVAL_DELTA)) {
if (mindiff == UNDEFINED_MIN_DIFF || (x - csx) <= mindiff) {
mindiff = (x - csx);
nextRingIndex = ni;
}
}
} else if (h % 4 == 3) {
// left
if (csx == leftX && csy <= safelyAddDelta(y, EVAL_DELTA)) {
if (mindiff == UNDEFINED_MIN_DIFF || (y - csy) <= mindiff) {
mindiff = (y - csy);
nextRingIndex = ni;
}
}
}
} // END find closest start (including current)
// we found start point
if (mindiff != UNDEFINED_MIN_DIFF) {
break;
} else {
if (h % 4 == 0) {
// top
y = topY;
x = rightX;
} else if (h % 4 == 1) {
// right
y = bottomY;
x = rightX;
} else if (h % 4 == 2) {
// bottom
y = bottomY;
x = leftX;
} else if (h % 4 == 3) {
y = topY;
x = leftX;
}
i.add((((long) x) << 32) | ((long) y));
}
} // END go clockwise around rectangle
if (nextRingIndex == -1) {
// it is impossible (current start should always be found)
} else if (nextRingIndex == j) {
i.add(i.get(0));
nonvisitedRings.remove(j);
break;
} else {
i.addAll(uncompletedRings.get(nextRingIndex));
nonvisitedRings.remove(nextRingIndex);
// get last point and start again going clockwise
x = (int) (i.get(i.size() - 1) >> 32);
y = (int) (i.get(i.size() - 1) & mask);
}
}
completedRings.add(i);
}
}
private int safelyAddDelta(int number, int delta) {
int res = number + delta;
if (delta > 0 && res < number) {
return Integer.MAX_VALUE;
} else if (delta < 0 && res > number) {
return Integer.MIN_VALUE;
}
return res;
}
private static long combine2Points(int x, int y) {
return (((long) x ) <<32) | ((long)y );
}
private boolean calculateLineCoordinates(boolean inside, int x, int y, boolean pinside, int px, int py, int leftX, int rightX,
int bottomY, int topY, TLongList coordinates) {
boolean lineEnded = false;
if (pinside) {
if (!inside) {
long is = MapAlgorithms.calculateIntersection(x, y, px, py, leftX, rightX, bottomY, topY);
if (is == -1) {
// it is an error (!)
is = combine2Points(px, py);
}
coordinates.add(is);
lineEnded = true;
} else {
coordinates.add(combine2Points(x, y));
}
} else {
long is = MapAlgorithms.calculateIntersection(x, y, px, py, leftX, rightX, bottomY, topY);
if (inside) {
// assert is != -1;
coordinates.add(is);
coordinates.add(combine2Points(x, y));
} else if (is != -1) {
int bx = (int) (is >> 32);
int by = (int) (is & 0xffffffff);
coordinates.add(is);
is = MapAlgorithms.calculateIntersection(x, y, bx, by, leftX, rightX, bottomY, topY);
coordinates.add(is);
lineEnded = true;
}
}
return lineEnded;
}
}