vtm/src/org/oscim/view/renderer/MapRenderer.java
Hannes Janetzek 5bf9deef89 - prevent possible concurrency between updateMap 'clear' tiles and onDrawFrame
- no need to sync tile in uploadTileData anymore, now that proxies are locked properly
- cleanups
2013-10-09 01:22:48 +02:00

577 lines
14 KiB
Java

/*
* Copyright 2012 Hannes Janetzek
*
* This program is free software: you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.oscim.view.renderer;
import java.util.ArrayList;
import java.util.Collections;
import org.oscim.core.MercatorProjection;
import org.oscim.core.Tile;
import org.oscim.database.MapInfo;
import org.oscim.theme.RenderTheme;
import org.oscim.utils.GlConfigChooser;
import org.oscim.view.MapPosition;
import org.oscim.view.MapView;
import org.oscim.view.generator.JobTile;
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.util.FloatMath;
import android.util.Log;
// FIXME to many 'Renderer', this one needs a better name.. TileLoader?
public class MapRenderer extends GLSurfaceView {
private final static String TAG = "MapRenderer";
private GLRenderer mRenderer;
private static final int MAX_TILES_IN_QUEUE = 40;
private static MapView mMapView;
// new jobs for the MapWorkers
private static ArrayList<JobTile> mJobList;
// all tiles currently referenced
private static ArrayList<MapTile> mTiles;
// tiles that have new data to upload, see passTile()
private static ArrayList<MapTile> mTilesLoaded;
// current center tile, values used to check if position has
// changed for updating current tile list
private static long mTileX, mTileY;
private static float mPrevScale;
private static byte mPrevZoom;
private static boolean mInitial;
// private static MapPosition mCurPosition, mDrawPosition;
private static int mWidth = 0, mHeight = 0;
// used for passing tiles to be rendered from TileLoader(Main-Thread) to
// GLThread
static final class TilesData {
int cnt = 0;
final MapTile[] tiles;
TilesData(int numTiles) {
tiles = new MapTile[numTiles];
}
}
private static TilesData mCurrentTiles;
// maps zoom-level to available zoom-levels in MapDatabase
// e.g. 16->16, 15->16, 14->13, 13->13, 12->13,....
private static int[] mZoomLevels;
public MapRenderer(Context context, MapView mapView) {
super(context);
mMapView = mapView;
Log.d(TAG, "init GLSurfaceLayer");
setEGLConfigChooser(new GlConfigChooser());
setEGLContextClientVersion(2);
// setDebugFlags(DEBUG_CHECK_GL_ERROR | DEBUG_LOG_GL_CALLS);
mRenderer = new GLRenderer(mMapView);
setRenderer(mRenderer);
// if (!debugFrameTime)
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
mJobList = new ArrayList<JobTile>();
mTiles = new ArrayList<MapTile>();
mTilesLoaded = new ArrayList<MapTile>(30);
VertexPool.init();
QuadTree.init();
mInitial = true;
}
/**
* Update list of visible tiles and passes them to MapRenderer, when not
* available tiles are created and added to JobQueue (mapView.addJobs) for
* loading by TileGenerator class
*
* @param clear
* whether to clear and reload all tiles
*/
public void updateMap(boolean clear) {
boolean changedPos = false;
if (mMapView == null)
return;
MapPosition mapPosition = mMapView.getMapPosition().getMapPosition();
if (mapPosition == null) {
Log.d(TAG, "X no map position");
return;
}
if (clear) {
// make sure onDrawFrame is not running
GLRenderer.lock.lock();
// remove all tiles references
Log.d(TAG, "CLEAR");
for (MapTile t : mTiles)
clearTile(t);
mTiles.clear();
mTilesLoaded.clear();
QuadTree.init();
mInitial = true;
GLRenderer.lock.unlock();
}
if (mInitial) {
// set up TileData arrays that are passed to gl-thread
int numTiles = (mWidth / (Tile.TILE_SIZE / 2) + 2)
* (mHeight / (Tile.TILE_SIZE / 2) + 2);
mRenderer.clearTiles(numTiles);
mCurrentTiles = new TilesData(numTiles);
MapInfo mapInfo = mMapView.getMapDatabase().getMapInfo();
if (mapInfo != null)
mZoomLevels = mapInfo.zoomLevel;
}
byte zoomLevel = mapPosition.zoomLevel;
float scale = mapPosition.scale;
long tileX = MercatorProjection.pixelXToTileX(mapPosition.x, zoomLevel)
* Tile.TILE_SIZE;
long tileY = MercatorProjection.pixelYToTileY(mapPosition.y, zoomLevel)
* Tile.TILE_SIZE;
int zdir = 0;
if (mInitial || mPrevZoom != zoomLevel) {
changedPos = true;
} else if (tileX != mTileX || tileY != mTileY) {
if (mPrevScale - scale > 0 && scale > 1.2)
zdir = 1;
changedPos = true;
} else if (mPrevScale - scale > 0.2 || mPrevScale - scale < -0.2) {
if (mPrevScale - scale > 0 && scale > 1.2)
zdir = 1;
changedPos = true;
}
if (mInitial) {
mInitial = false;
}
mTileX = tileX;
mTileY = tileY;
mPrevZoom = zoomLevel;
// GLRenderer.updatePosition(mapPosition);
if (!MapView.debugFrameTime)
requestRender();
if (changedPos) {
mPrevScale = scale;
updateVisibleList(mapPosition, zdir);
if (!MapView.debugFrameTime)
requestRender();
int remove = mTiles.size() - GLRenderer.CACHE_TILES;
if (remove > 50)
limitCache(mapPosition, remove);
}
limitLoadQueue();
}
/**
* set mCurrentTiles for the visible tiles and pass it to GLRenderer, add
* jobs for not yet loaded tiles
*
* @param mapPosition
* the current MapPosition
* @param zdir
* zoom direction
*/
private static void updateVisibleList(MapPosition mapPosition, int zdir) {
double x = mapPosition.x;
double y = mapPosition.y;
byte zoomLevel = mapPosition.zoomLevel;
float scale = mapPosition.scale;
double add = 1.0f / scale;
int offsetX = (int) ((mWidth >> 1) * add) + Tile.TILE_SIZE;
int offsetY = (int) ((mHeight >> 1) * add) + Tile.TILE_SIZE;
long pixelRight = (long) x + offsetX;
long pixelBottom = (long) y + offsetY;
long pixelLeft = (long) x - offsetX;
long pixelTop = (long) y - offsetY;
int tileLeft = MercatorProjection.pixelXToTileX(pixelLeft, zoomLevel);
int tileTop = MercatorProjection.pixelYToTileY(pixelTop, zoomLevel);
int tileRight = MercatorProjection.pixelXToTileX(pixelRight, zoomLevel);
int tileBottom = MercatorProjection.pixelYToTileY(pixelBottom, zoomLevel);
mJobList.clear();
// set non processed tiles to isLoading == false
mMapView.addJobs(null);
int tiles = 0;
int max = mCurrentTiles.tiles.length - 1;
boolean fetchChildren = false;
boolean fetchParent = false;
boolean fetchProxy = false;
if (mZoomLevels != null) {
// check MapDatabase zoom-level-mapping
if (mZoomLevels[zoomLevel] == 0) {
mCurrentTiles.cnt = 0;
mCurrentTiles = GLRenderer.updateTiles(mCurrentTiles);
return;
}
if (mZoomLevels[zoomLevel] > zoomLevel) {
fetchChildren = true;
fetchProxy = true;
} else if (mZoomLevels[zoomLevel] < zoomLevel) {
fetchParent = true;
fetchProxy = true;
}
}
for (int yy = tileTop; yy <= tileBottom; yy++) {
for (int xx = tileLeft; xx <= tileRight; xx++) {
if (tiles == max)
break;
MapTile tile = QuadTree.getTile(xx, yy, zoomLevel);
if (tile == null) {
tile = new MapTile(xx, yy, zoomLevel);
QuadTree.add(tile);
mTiles.add(tile);
}
if (!fetchProxy && !tile.isActive()) {
mJobList.add(tile);
}
mCurrentTiles.tiles[tiles++] = tile;
if (fetchChildren) {
byte z = (byte) (zoomLevel + 1);
for (int i = 0; i < 4; i++) {
int cx = (xx << 1) + (i % 2);
int cy = (yy << 1) + (i >> 1);
MapTile c = QuadTree.getTile(cx, cy, z);
if (c == null) {
c = new MapTile(cx, cy, z);
QuadTree.add(c);
mTiles.add(c);
}
if (!c.isActive()) {
mJobList.add(c);
}
}
}
if (fetchParent || (!fetchProxy && zdir > 0 && zoomLevel > 0)) {
// prefetch parent
MapTile p = tile.rel.parent.tile;
if (p == null) {
p = new MapTile(xx >> 1, yy >> 1, (byte) (zoomLevel - 1));
QuadTree.add(p);
mTiles.add(p);
mJobList.add(p);
} else if (!p.isActive()) {
if (!mJobList.contains(p))
mJobList.add(p);
}
}
}
}
// pass new tile list to glThread
mCurrentTiles.cnt = tiles;
mCurrentTiles = GLRenderer.updateTiles(mCurrentTiles);
// note: this sets isLoading == true for all job tiles
if (mJobList.size() > 0) {
updateTileDistances(mJobList, mapPosition);
Collections.sort(mJobList);
mMapView.addJobs(mJobList);
}
}
private static void clearTile(MapTile t) {
t.newData = false;
t.isLoading = false;
t.isReady = false;
LineRenderer.clear(t.lineLayers);
PolygonRenderer.clear(t.polygonLayers);
t.labels = null;
t.lineLayers = null;
t.polygonLayers = null;
if (t.vbo != null) {
GLRenderer.addVBO(t.vbo);
t.vbo = null;
}
if (t.texture != null)
t.texture.tile = null;
QuadTree.remove(t);
}
private static void updateTileDistances(ArrayList<?> tiles, MapPosition mapPosition) {
int h = (Tile.TILE_SIZE >> 1);
byte zoom = mapPosition.zoomLevel;
long x = (long) mapPosition.x;
long y = (long) mapPosition.y;
int diff;
long dx, dy;
// TODO this could need some fixing, and optimization
// to consider move/zoom direction
for (int i = 0, n = tiles.size(); i < n; i++) {
JobTile t = (JobTile) tiles.get(i);
diff = (t.zoomLevel - zoom);
if (diff == 0) {
dx = (t.pixelX + h) - x;
dy = (t.pixelY + h) - y;
// t.distance = ((dx > 0 ? dx : -dx) + (dy > 0 ? dy : -dy)) *
// 0.25f;
t.distance = FloatMath.sqrt((dx * dx + dy * dy)) * 0.25f;
} else if (diff > 0) {
// tile zoom level is child of current
if (diff < 3) {
dx = ((t.pixelX + h) >> diff) - x;
dy = ((t.pixelY + h) >> diff) - y;
}
else {
dx = ((t.pixelX + h) >> (diff >> 1)) - x;
dy = ((t.pixelY + h) >> (diff >> 1)) - y;
}
// t.distance = ((dx > 0 ? dx : -dx) + (dy > 0 ? dy : -dy));
t.distance = FloatMath.sqrt((dx * dx + dy * dy));
} else {
// tile zoom level is parent of current
dx = ((t.pixelX + h) << -diff) - x;
dy = ((t.pixelY + h) << -diff) - y;
// t.distance = ((dx > 0 ? dx : -dx) + (dy > 0 ? dy : -dy)) *
// (-diff * 0.5f);
t.distance = FloatMath.sqrt((dx * dx + dy * dy)) * (-diff * 0.5f);
}
}
}
private static void limitCache(MapPosition mapPosition, int remove) {
int removes = remove;
int size = mTiles.size();
// remove orphaned tiles
for (int i = 0; i < size;) {
MapTile t = mTiles.get(i);
// make sure tile cannot be used by GL or MapWorker Thread
if (t.isLocked() || t.isActive()) {
i++;
} else {
// Log.d(TAG, "remove empty tile" + cur);
clearTile(t);
mTiles.remove(i);
removes--;
size--;
}
}
// Log.d(TAG, "remove tiles: " + removes + " " + size);
if (removes <= 0)
return;
updateTileDistances(mTiles, mapPosition);
Collections.sort(mTiles);
for (int i = 1; i <= removes; i++) {
MapTile t = mTiles.remove(size - i);
synchronized (t) {
if (t.isLocked()) {
// dont remove tile used by renderthread
Log.d(TAG, "X not removing " + t + " " + t.isLocked + " "
+ t.distance);
mTiles.add(t);
// } else if (t.isLoading) {
// // FIXME if we add tile back on next limit cache
// // this will be removed. clearTile could interfere with
// // MapGenerator... clear in passTile().
// Log.d(TAG, "X cancel loading " + t + " " + t.distance);
// t.isLoading = false;
// // mTiles.add(t);
} else {
clearTile(t);
}
}
}
}
private static void limitLoadQueue() {
int size = mTilesLoaded.size();
if (size < MAX_TILES_IN_QUEUE)
return;
synchronized (mTilesLoaded) {
// remove tiles uploaded to vbo
for (int i = 0; i < size;) {
MapTile t = mTilesLoaded.get(i);
// rel == null means tile is already removed by limitCache
if (!t.newData || t.rel == null) {
mTilesLoaded.remove(i);
size--;
continue;
}
i++;
}
if (size < MAX_TILES_IN_QUEUE)
return;
// Log.d(TAG, "queue: " + mTilesLoaded.size() + " " + size + " "
// + (size - MAX_TILES_IN_QUEUE / 2));
// clear loaded but not used tiles
for (int i = 0, n = size - MAX_TILES_IN_QUEUE / 2; i < n; n--) {
MapTile t = mTilesLoaded.get(i);
synchronized (t) {
if (t.rel == null) {
mTilesLoaded.remove(i);
continue;
}
if (t.isLocked()) {
// Log.d(TAG, "keep unused tile data: " + t + " " +
// t.isActive);
i++;
continue;
}
// Log.d(TAG, "remove unused tile data: " + t);
mTilesLoaded.remove(i);
mTiles.remove(t);
clearTile(t);
}
}
}
}
/**
* called from MapWorker Thread when tile is loaded by TileGenerator
*
* @param jobTile
* ...
* @return ...
*/
public synchronized boolean passTile(JobTile jobTile) {
MapTile tile = (MapTile) jobTile;
if (!tile.isLoading) {
// no one should be able to use this tile now, mapgenerator passed
// it, glthread does nothing until newdata is set.
Log.d(TAG, "passTile: canceled " + tile);
synchronized (mTilesLoaded) {
mTilesLoaded.add(tile);
}
return true;
}
mRenderer.setVBO(tile);
if (tile.vbo == null) {
Log.d(TAG, "no VBOs left for " + tile);
tile.isLoading = false;
return false;
}
tile.newData = true;
tile.isLoading = false;
if (!MapView.debugFrameTime)
requestRender();
synchronized (mTilesLoaded) {
mTilesLoaded.add(tile);
}
return true;
}
public void setRenderTheme(RenderTheme t) {
if (mRenderer != null)
mRenderer.setRenderTheme(t);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
Log.d(TAG, "onSizeChanged" + w + " " + h);
mWidth = w;
mHeight = h;
if (mWidth > 0 && mHeight > 0)
mInitial = true;
super.onSizeChanged(w, h, oldw, oldh);
}
}