fixing MapTile state logic
This commit is contained in:
parent
5f9a9cc909
commit
71715dccd9
@ -16,6 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.oscim.layers.tile;
|
package org.oscim.layers.tile;
|
||||||
|
|
||||||
|
import static org.oscim.layers.tile.MapTile.State.CANCEL;
|
||||||
import static org.oscim.layers.tile.MapTile.State.LOADING;
|
import static org.oscim.layers.tile.MapTile.State.LOADING;
|
||||||
import static org.oscim.layers.tile.MapTile.State.NONE;
|
import static org.oscim.layers.tile.MapTile.State.NONE;
|
||||||
|
|
||||||
@ -37,9 +38,6 @@ public class JobQueue {
|
|||||||
* the jobs to be added to this queue.
|
* the jobs to be added to this queue.
|
||||||
*/
|
*/
|
||||||
public synchronized void setJobs(MapTile[] tiles) {
|
public synchronized void setJobs(MapTile[] tiles) {
|
||||||
for (MapTile t : tiles)
|
|
||||||
t.state = LOADING;
|
|
||||||
|
|
||||||
mJobs = tiles;
|
mJobs = tiles;
|
||||||
mCurrentJob = 0;
|
mCurrentJob = 0;
|
||||||
}
|
}
|
||||||
@ -55,12 +53,12 @@ public class JobQueue {
|
|||||||
MapTile[] tiles = mJobs;
|
MapTile[] tiles = mJobs;
|
||||||
|
|
||||||
for (int i = mCurrentJob, n = mJobs.length; i < n; i++) {
|
for (int i = mCurrentJob, n = mJobs.length; i < n; i++) {
|
||||||
|
MapTile t = tiles[i];
|
||||||
if (tiles[i].state == LOADING)
|
if (t.state(LOADING | CANCEL)) {
|
||||||
tiles[i].state = NONE;
|
t.setState(NONE);
|
||||||
else
|
} else {
|
||||||
log.debug("wrong tile in queue {} {}", tiles[i], tiles[i].state);
|
log.error("Wrong tile in queue {} {}", t, t.state());
|
||||||
|
}
|
||||||
tiles[i] = null;
|
tiles[i] = null;
|
||||||
}
|
}
|
||||||
mCurrentJob = 0;
|
mCurrentJob = 0;
|
||||||
|
@ -16,9 +16,13 @@
|
|||||||
*/
|
*/
|
||||||
package org.oscim.layers.tile;
|
package org.oscim.layers.tile;
|
||||||
|
|
||||||
|
import static org.oscim.layers.tile.MapTile.State.CANCEL;
|
||||||
import static org.oscim.layers.tile.MapTile.State.DEADBEEF;
|
import static org.oscim.layers.tile.MapTile.State.DEADBEEF;
|
||||||
|
import static org.oscim.layers.tile.MapTile.State.LOADING;
|
||||||
import static org.oscim.layers.tile.MapTile.State.NEW_DATA;
|
import static org.oscim.layers.tile.MapTile.State.NEW_DATA;
|
||||||
|
import static org.oscim.layers.tile.MapTile.State.NONE;
|
||||||
import static org.oscim.layers.tile.MapTile.State.READY;
|
import static org.oscim.layers.tile.MapTile.State.READY;
|
||||||
|
import static org.oscim.layers.tile.MapTile.State.TIMEOUT;
|
||||||
|
|
||||||
import org.oscim.core.Tile;
|
import org.oscim.core.Tile;
|
||||||
import org.oscim.layers.tile.vector.VectorTileLoader;
|
import org.oscim.layers.tile.vector.VectorTileLoader;
|
||||||
@ -44,34 +48,38 @@ public class MapTile extends Tile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static final class State {
|
public static final class State {
|
||||||
public final static byte NONE = 0;
|
public final static byte NONE = (1 << 0);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* STATE_LOADING means the tile is about to be loaded / loading.
|
* STATE_LOADING means the tile is about to be loaded / loading.
|
||||||
* Tile belongs to TileLoader thread.
|
* Tile belongs to TileLoader thread.
|
||||||
*/
|
*/
|
||||||
public final static byte LOADING = (1 << 0);
|
public final static byte LOADING = (1 << 1);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* STATE_NEW_DATA: tile data is prepared for rendering.
|
* STATE_NEW_DATA: tile data is prepared for rendering.
|
||||||
* While 'locked' it belongs to GL Thread.
|
* While 'locked' it belongs to GL Thread.
|
||||||
*/
|
*/
|
||||||
public final static byte NEW_DATA = (1 << 1);
|
public final static byte NEW_DATA = (1 << 2);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* STATE_READY: tile data is uploaded to GL.
|
* STATE_READY: tile data is uploaded to GL.
|
||||||
* While 'locked' it belongs to GL Thread.
|
* While 'locked' it belongs to GL Thread.
|
||||||
*/
|
*/
|
||||||
public final static byte READY = (1 << 2);
|
public final static byte READY = (1 << 3);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* STATE_CANCEL: tile is removed from TileManager,
|
* STATE_CANCEL: tile is removed from TileManager,
|
||||||
* but may still be processed by TileLoader.
|
* but may still be processed by TileLoader.
|
||||||
*/
|
*/
|
||||||
public final static byte CANCEL = (1 << 3);
|
public final static byte CANCEL = (1 << 4);
|
||||||
|
|
||||||
public final static byte DEADBEEF = (1 << 4);
|
public final static byte TIMEOUT = (1 << 5);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dont touch if you find some.
|
||||||
|
*/
|
||||||
|
public final static byte DEADBEEF = (1 << 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
public final static int PROXY_CHILD00 = (1 << 0);
|
public final static int PROXY_CHILD00 = (1 << 0);
|
||||||
@ -83,7 +91,7 @@ public class MapTile extends Tile {
|
|||||||
public final static int PROXY_HOLDER = (1 << 6);
|
public final static int PROXY_HOLDER = (1 << 6);
|
||||||
|
|
||||||
/** Tile state */
|
/** Tile state */
|
||||||
byte state;
|
byte state = State.NONE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* absolute tile coordinates: tileX,Y / Math.pow(2, zoomLevel)
|
* absolute tile coordinates: tileX,Y / Math.pow(2, zoomLevel)
|
||||||
@ -176,6 +184,11 @@ public class MapTile extends Tile {
|
|||||||
* used. This function should only be called through {@link TileManager}
|
* used. This function should only be called through {@link TileManager}
|
||||||
*/
|
*/
|
||||||
void lock() {
|
void lock() {
|
||||||
|
if (state == DEADBEEF) {
|
||||||
|
log.debug("Locking dead tile {}", this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (locked++ > 0)
|
if (locked++ > 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@ -218,6 +231,12 @@ public class MapTile extends Tile {
|
|||||||
if (--locked > 0)
|
if (--locked > 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (state == DEADBEEF) {
|
||||||
|
log.debug("Unlock dead tile {}", this);
|
||||||
|
clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
TileNode parent = node.parent;
|
TileNode parent = node.parent;
|
||||||
if ((proxy & PROXY_PARENT) != 0)
|
if ((proxy & PROXY_PARENT) != 0)
|
||||||
parent.item.refs--;
|
parent.item.refs--;
|
||||||
@ -259,7 +278,7 @@ public class MapTile extends Tile {
|
|||||||
data.dispose();
|
data.dispose();
|
||||||
data = data.next;
|
data = data.next;
|
||||||
}
|
}
|
||||||
state = DEADBEEF;
|
state = NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -367,4 +386,75 @@ public class MapTile extends Tile {
|
|||||||
|
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String state() {
|
||||||
|
switch (state) {
|
||||||
|
case State.NONE:
|
||||||
|
return "None";
|
||||||
|
case State.LOADING:
|
||||||
|
return "Loading";
|
||||||
|
case State.NEW_DATA:
|
||||||
|
return "Data";
|
||||||
|
case State.READY:
|
||||||
|
return "Ready";
|
||||||
|
case State.CANCEL:
|
||||||
|
return "Cancel";
|
||||||
|
case State.TIMEOUT:
|
||||||
|
return "Timeout";
|
||||||
|
case State.DEADBEEF:
|
||||||
|
return "Dead";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
void setState(byte newState) {
|
||||||
|
if (state == newState)
|
||||||
|
return;
|
||||||
|
|
||||||
|
switch (newState) {
|
||||||
|
case NONE:
|
||||||
|
if (state(LOADING | CANCEL)) {
|
||||||
|
state = newState;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("None"
|
||||||
|
+ " <= " + state() + " " + this);
|
||||||
|
case LOADING:
|
||||||
|
if (state == NONE) {
|
||||||
|
state = newState;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Loading"
|
||||||
|
+ " <= " + state() + " " + this);
|
||||||
|
case NEW_DATA:
|
||||||
|
if (state == LOADING) {
|
||||||
|
state = newState;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("NewData"
|
||||||
|
+ " <= " + state() + " " + this);
|
||||||
|
|
||||||
|
case READY:
|
||||||
|
if (state == NEW_DATA) {
|
||||||
|
state = newState;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Ready"
|
||||||
|
+ " <= " + state() + " " + this);
|
||||||
|
|
||||||
|
case CANCEL:
|
||||||
|
if (state == LOADING) {
|
||||||
|
state = newState;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Cancel" +
|
||||||
|
" <= " + state() + " " + this);
|
||||||
|
case TIMEOUT:
|
||||||
|
// TODO
|
||||||
|
break;
|
||||||
|
case DEADBEEF:
|
||||||
|
state = newState;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ import static org.oscim.layers.tile.MapTile.State.CANCEL;
|
|||||||
import static org.oscim.layers.tile.MapTile.State.DEADBEEF;
|
import static org.oscim.layers.tile.MapTile.State.DEADBEEF;
|
||||||
import static org.oscim.layers.tile.MapTile.State.LOADING;
|
import static org.oscim.layers.tile.MapTile.State.LOADING;
|
||||||
import static org.oscim.layers.tile.MapTile.State.NEW_DATA;
|
import static org.oscim.layers.tile.MapTile.State.NEW_DATA;
|
||||||
|
import static org.oscim.layers.tile.MapTile.State.NONE;
|
||||||
import static org.oscim.layers.tile.MapTile.State.READY;
|
import static org.oscim.layers.tile.MapTile.State.READY;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -65,7 +66,6 @@ public class TileManager {
|
|||||||
/** cache limit threshold */
|
/** cache limit threshold */
|
||||||
private static final int CACHE_THRESHOLD = 25;
|
private static final int CACHE_THRESHOLD = 25;
|
||||||
private static final int CACHE_CLEAR_THRESHOLD = 10;
|
private static final int CACHE_CLEAR_THRESHOLD = 10;
|
||||||
private static final int QUEUE_CLEAR_THRESHOLD = 10;
|
|
||||||
|
|
||||||
private final Map mMap;
|
private final Map mMap;
|
||||||
private final Viewport mViewport;
|
private final Viewport mViewport;
|
||||||
@ -77,10 +77,10 @@ public class TileManager {
|
|||||||
private int mTilesCount;
|
private int mTilesCount;
|
||||||
|
|
||||||
/** current end position in mTiles */
|
/** current end position in mTiles */
|
||||||
private int mTilesSize;
|
private int mTilesEnd;
|
||||||
|
|
||||||
/** counter for tiles with new data not yet loaded to GL */
|
/** counter for tiles with new data not yet loaded to GL */
|
||||||
private int mTilesForUpload;
|
private int mTilesToUpload;
|
||||||
|
|
||||||
/** new tile jobs for MapWorkers */
|
/** new tile jobs for MapWorkers */
|
||||||
private final ArrayList<MapTile> mJobs;
|
private final ArrayList<MapTile> mJobs;
|
||||||
@ -112,7 +112,7 @@ public class TileManager {
|
|||||||
@Override
|
@Override
|
||||||
public void removeItem(MapTile t) {
|
public void removeItem(MapTile t) {
|
||||||
if (t.node == null)
|
if (t.node == null)
|
||||||
throw new IllegalStateException("already removed: " + t);
|
throw new IllegalStateException("Already removed: " + t);
|
||||||
|
|
||||||
super.remove(t.node);
|
super.remove(t.node);
|
||||||
t.node.item = null;
|
t.node.item = null;
|
||||||
@ -149,8 +149,8 @@ public class TileManager {
|
|||||||
mJobs = new ArrayList<MapTile>();
|
mJobs = new ArrayList<MapTile>();
|
||||||
mTiles = new MapTile[mCacheLimit];
|
mTiles = new MapTile[mCacheLimit];
|
||||||
|
|
||||||
mTilesSize = 0;
|
mTilesEnd = 0;
|
||||||
mTilesForUpload = 0;
|
mTilesToUpload = 0;
|
||||||
mUpdateSerial = 0;
|
mUpdateSerial = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,17 +165,28 @@ public class TileManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void init() {
|
public void init() {
|
||||||
|
if (mCurrentTiles != null)
|
||||||
|
mCurrentTiles.releaseTiles();
|
||||||
/* pass VBOs and VertexItems back to pools */
|
/* pass VBOs and VertexItems back to pools */
|
||||||
for (int i = 0; i < mTilesSize; i++) {
|
for (int i = 0; i < mTilesEnd; i++) {
|
||||||
if (mTiles[i] == null)
|
MapTile t = mTiles[i];
|
||||||
|
if (t == null)
|
||||||
continue;
|
continue;
|
||||||
removeFromCache(mTiles[i]);
|
|
||||||
mTiles[i].state = CANCEL;
|
if (!t.isLocked()) {
|
||||||
|
//log.debug("init clear {} {}", t, t.state());
|
||||||
|
t.clear();
|
||||||
|
}
|
||||||
|
mIndex.removeItem(t);
|
||||||
|
|
||||||
|
/* in case the tile is still loading:
|
||||||
|
* clear when returned from loader */
|
||||||
|
t.setState(DEADBEEF);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* clear references to cached MapTiles */
|
/* clear references to cached MapTiles */
|
||||||
Arrays.fill(mTiles, null);
|
Arrays.fill(mTiles, null);
|
||||||
mTilesSize = 0;
|
mTilesEnd = 0;
|
||||||
mTilesCount = 0;
|
mTilesCount = 0;
|
||||||
|
|
||||||
/* set up TileSet large enough to hold current tiles */
|
/* set up TileSet large enough to hold current tiles */
|
||||||
@ -198,6 +209,7 @@ public class TileManager {
|
|||||||
public boolean update(MapPosition pos) {
|
public boolean update(MapPosition pos) {
|
||||||
|
|
||||||
// FIXME cant expect init to be called otherwise
|
// FIXME cant expect init to be called otherwise
|
||||||
|
// Should use some onLayerAttached callback instead.
|
||||||
if (mNewTiles == null || mNewTiles.tiles.length == 0) {
|
if (mNewTiles == null || mNewTiles.tiles.length == 0) {
|
||||||
mPrevZoomlevel = pos.zoomLevel;
|
mPrevZoomlevel = pos.zoomLevel;
|
||||||
init();
|
init();
|
||||||
@ -319,17 +331,19 @@ public class TileManager {
|
|||||||
mCacheReduce += 10;
|
mCacheReduce += 10;
|
||||||
if (dbg)
|
if (dbg)
|
||||||
log.debug("reduce cache {}", (mCacheLimit - mCacheReduce));
|
log.debug("reduce cache {}", (mCacheLimit - mCacheReduce));
|
||||||
} else
|
} else {
|
||||||
mCacheReduce = 0;
|
mCacheReduce = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* limit cache items */
|
/* limit cache items */
|
||||||
int remove = mTilesCount - (mCacheLimit - mCacheReduce);
|
int remove = mTilesCount - (mCacheLimit - mCacheReduce);
|
||||||
|
|
||||||
if (remove > CACHE_THRESHOLD ||
|
if (remove > CACHE_THRESHOLD || mTilesToUpload > MAX_TILES_IN_QUEUE) {
|
||||||
mTilesForUpload > MAX_TILES_IN_QUEUE)
|
synchronized (mTilelock) {
|
||||||
limitCache(pos, remove);
|
limitCache(pos, remove);
|
||||||
|
}
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -379,9 +393,11 @@ public class TileManager {
|
|||||||
if (tile == null) {
|
if (tile == null) {
|
||||||
TileNode n = mIndex.add(x, y, zoomLevel);
|
TileNode n = mIndex.add(x, y, zoomLevel);
|
||||||
tile = n.item = new MapTile(n, x, y, zoomLevel);
|
tile = n.item = new MapTile(n, x, y, zoomLevel);
|
||||||
|
tile.setState(LOADING);
|
||||||
mJobs.add(tile);
|
mJobs.add(tile);
|
||||||
addToCache(tile);
|
addToCache(tile);
|
||||||
} else if (!tile.isActive()) {
|
} else if (!tile.isActive()) {
|
||||||
|
tile.setState(LOADING);
|
||||||
mJobs.add(tile);
|
mJobs.add(tile);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -393,10 +409,10 @@ public class TileManager {
|
|||||||
p = n.item = new MapTile(n, x >> 1, y >> 1, zoomLevel - 1);
|
p = n.item = new MapTile(n, x >> 1, y >> 1, zoomLevel - 1);
|
||||||
addToCache(p);
|
addToCache(p);
|
||||||
/* this prevents to add tile twice to queue */
|
/* this prevents to add tile twice to queue */
|
||||||
p.state = LOADING;
|
p.setState(LOADING);
|
||||||
mJobs.add(p);
|
mJobs.add(p);
|
||||||
} else if (!p.isActive()) {
|
} else if (!p.isActive()) {
|
||||||
p.state = LOADING;
|
p.setState(LOADING);
|
||||||
mJobs.add(p);
|
mJobs.add(p);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -405,133 +421,139 @@ public class TileManager {
|
|||||||
|
|
||||||
private void addToCache(MapTile tile) {
|
private void addToCache(MapTile tile) {
|
||||||
|
|
||||||
if (mTilesSize == mTiles.length) {
|
if (mTilesEnd == mTiles.length) {
|
||||||
if (mTilesSize > mTilesCount) {
|
if (mTilesEnd > mTilesCount) {
|
||||||
TileDistanceSort.sort(mTiles, 0, mTilesSize);
|
TileDistanceSort.sort(mTiles, 0, mTilesEnd);
|
||||||
/* sorting also repacks the 'sparse' filled array
|
/* sorting also repacks the 'sparse' filled array
|
||||||
* so end of mTiles is at mTilesCount now */
|
* so end of mTiles is at mTilesCount now */
|
||||||
mTilesSize = mTilesCount;
|
mTilesEnd = mTilesCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mTilesSize == mTiles.length) {
|
if (mTilesEnd == mTiles.length) {
|
||||||
log.debug("realloc tiles {}", mTilesSize);
|
log.debug("realloc tiles {}", mTilesEnd);
|
||||||
MapTile[] tmp = new MapTile[mTiles.length + 20];
|
MapTile[] tmp = new MapTile[mTiles.length + 20];
|
||||||
System.arraycopy(mTiles, 0, tmp, 0, mTilesCount);
|
System.arraycopy(mTiles, 0, tmp, 0, mTilesCount);
|
||||||
mTiles = tmp;
|
mTiles = tmp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mTiles[mTilesSize++] = tile;
|
mTiles[mTilesEnd++] = tile;
|
||||||
mTilesCount++;
|
mTilesCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void removeFromCache(MapTile t) {
|
private boolean removeFromCache(MapTile t) {
|
||||||
|
/* TODO check valid states here:When in CANCEL state tile belongs to
|
||||||
|
* TileLoader thread, defer clearing to jobCompleted() */
|
||||||
|
|
||||||
|
if (dbg)
|
||||||
|
log.debug("remove from cache {} {} {}",
|
||||||
|
t, t.state(), t.isLocked());
|
||||||
|
|
||||||
|
if (t.isLocked())
|
||||||
|
return false;
|
||||||
|
|
||||||
if (t.state(NEW_DATA | READY))
|
if (t.state(NEW_DATA | READY))
|
||||||
events.fire(TILE_REMOVED, t);
|
events.fire(TILE_REMOVED, t);
|
||||||
|
|
||||||
if (dbg)
|
t.clear();
|
||||||
log.debug("remove from cache {} {} {}", t, t.state, t.isLocked());
|
|
||||||
|
|
||||||
/* When in CANCEL state tile belongs to TileLoader thread,
|
|
||||||
* defer clearing to jobCompleted() */
|
|
||||||
if (t.state != CANCEL)
|
|
||||||
t.clear();
|
|
||||||
|
|
||||||
mIndex.removeItem(t);
|
mIndex.removeItem(t);
|
||||||
|
|
||||||
mTilesCount--;
|
mTilesCount--;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void limitCache(MapPosition pos, int remove) {
|
private void limitCache(MapPosition pos, int remove) {
|
||||||
MapTile[] tiles = mTiles;
|
MapTile[] tiles = mTiles;
|
||||||
int size = mTilesSize;
|
|
||||||
|
|
||||||
/* count tiles that have new data */
|
/* count tiles that have new data */
|
||||||
mTilesForUpload = 0;
|
|
||||||
int newTileCnt = 0;
|
int newTileCnt = 0;
|
||||||
|
|
||||||
/* remove tiles that were never loaded */
|
/* remove tiles that were never loaded */
|
||||||
for (int i = 0; i < size; i++) {
|
for (int i = 0; i < mTilesEnd; i++) {
|
||||||
MapTile t = tiles[i];
|
MapTile t = tiles[i];
|
||||||
if (t == null)
|
if (t == null)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (t.state == NEW_DATA)
|
if (t.state(NEW_DATA))
|
||||||
newTileCnt++;
|
newTileCnt++;
|
||||||
|
|
||||||
if (t.state == DEADBEEF) {
|
if (t.state(DEADBEEF)) {
|
||||||
//log.debug("found DEADBEEF {}", t);
|
log.debug("found DEADBEEF {}", t);
|
||||||
|
t.clear();
|
||||||
tiles[i] = null;
|
tiles[i] = null;
|
||||||
mTilesCount--;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
/* make sure tile cannot be used by GL or MapWorker Thread */
|
|
||||||
if ((t.state != 0) || t.isLocked()) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* empty tile */
|
/* make sure tile cannot be used by GL or MapWorker Thread */
|
||||||
removeFromCache(t);
|
if (t.state(NONE) && removeFromCache(t)) {
|
||||||
tiles[i] = null;
|
tiles[i] = null;
|
||||||
remove--;
|
remove--;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((remove < CACHE_CLEAR_THRESHOLD) && (newTileCnt < MAX_TILES_IN_QUEUE))
|
if ((remove < CACHE_CLEAR_THRESHOLD) && (newTileCnt < MAX_TILES_IN_QUEUE))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
updateDistances(tiles, size, pos);
|
updateDistances(tiles, mTilesEnd, pos);
|
||||||
TileDistanceSort.sort(tiles, 0, size);
|
TileDistanceSort.sort(tiles, 0, mTilesEnd);
|
||||||
|
|
||||||
/* sorting also repacks the 'sparse' filled array
|
/* sorting also repacks the 'sparse' filled array
|
||||||
* so end of mTiles is at mTilesCount now */
|
* so end of mTiles is at mTilesCount now */
|
||||||
size = mTilesSize = mTilesCount;
|
mTilesEnd = mTilesCount;
|
||||||
|
|
||||||
// log.debug("remove:" + remove + " new:" + newTileCnt);
|
|
||||||
// log.debug("cur: " + mapPosition);
|
|
||||||
|
|
||||||
/* start with farest away tile */
|
/* start with farest away tile */
|
||||||
for (int i = size - 1; i >= 0 && remove > 0; i--) {
|
for (int i = mTilesCount - 1; i >= 0 && remove > 0; i--) {
|
||||||
MapTile t = tiles[i];
|
MapTile t = tiles[i];
|
||||||
|
|
||||||
/* dont remove tile used by TileRenderer, or somewhere else
|
/* dont remove tile used by TileRenderer, or somewhere else
|
||||||
* try again in next run. */
|
* try again in next run. */
|
||||||
if (t.isLocked()) {
|
if (t.isLocked()) {
|
||||||
if (dbg)
|
if (dbg)
|
||||||
log.debug("{} locked (state={}, d={})", t, t.state, t.distance);
|
log.debug("{} locked (state={}, d={})",
|
||||||
|
t, t.state(), t.distance);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (t.state(CANCEL)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* cancel loading of tiles that should not even be cached */
|
/* cancel loading of tiles that should not even be cached */
|
||||||
if (t.state == LOADING) {
|
if (t.state(LOADING)) {
|
||||||
t.state = CANCEL;
|
t.setState(CANCEL);
|
||||||
if (dbg)
|
if (dbg)
|
||||||
log.debug("{} canceled (d={})", t, t.distance);
|
log.debug("{} canceled (d={})", t, t.distance);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* clear new and unused tile */
|
/* clear new and unused tile */
|
||||||
if (t.state == NEW_DATA) {
|
if (t.state(NEW_DATA)) {
|
||||||
newTileCnt--;
|
newTileCnt--;
|
||||||
if (dbg)
|
if (dbg)
|
||||||
log.debug("{} unused (d=({})", t, t.distance);
|
log.debug("{} unused (d=({})", t, t.distance);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeFromCache(t);
|
if (!t.state(READY | NEW_DATA)) {
|
||||||
tiles[i] = null;
|
log.error("stuff that should be here! {} {}", t, t.state());
|
||||||
remove--;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
remove = (newTileCnt - MAX_TILES_IN_QUEUE) + QUEUE_CLEAR_THRESHOLD;
|
if (removeFromCache(t)) {
|
||||||
for (int i = size - 1; i >= 0 && remove > 0; i--) {
|
|
||||||
MapTile t = tiles[i];
|
|
||||||
if ((t != null) && (t.state == NEW_DATA) && !t.isLocked()) {
|
|
||||||
removeFromCache(t);
|
|
||||||
tiles[i] = null;
|
tiles[i] = null;
|
||||||
remove--;
|
remove--;
|
||||||
newTileCnt--;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mTilesForUpload += newTileCnt;
|
for (int i = mTilesCount - 1; i >= 0 && newTileCnt > MAX_TILES_IN_QUEUE; i--) {
|
||||||
|
MapTile t = tiles[i];
|
||||||
|
if ((t != null) && (t.state(NEW_DATA))) {
|
||||||
|
if (removeFromCache(t)) {
|
||||||
|
tiles[i] = null;
|
||||||
|
newTileCnt--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mTilesToUpload = newTileCnt;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -563,21 +585,23 @@ public class TileManager {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
if (success && tile.state != CANCEL) {
|
if (success && tile.state(LOADING)) {
|
||||||
tile.state = NEW_DATA;
|
tile.setState(NEW_DATA);
|
||||||
events.fire(TILE_LOADED, tile);
|
events.fire(TILE_LOADED, tile);
|
||||||
|
mTilesToUpload++;
|
||||||
mTilesForUpload++;
|
|
||||||
//log.debug("loading {}: ready", tile);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug("loading {}: {}", tile,
|
// TODO use mMap.update(true) to retry tile loading?
|
||||||
success ? "canceled"
|
log.debug("Load: {} {} state:{}",
|
||||||
: "failed");
|
tile, success ? "success" : "failed",
|
||||||
|
tile.state());
|
||||||
|
|
||||||
if (tile.state == LOADING)
|
/* got orphaned tile */
|
||||||
mIndex.removeItem(tile);
|
if (tile.state(DEADBEEF)) {
|
||||||
|
tile.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
tile.clear();
|
tile.clear();
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,6 @@ import static org.oscim.layers.tile.MapTile.PROXY_PARENT;
|
|||||||
import static org.oscim.layers.tile.MapTile.State.NEW_DATA;
|
import static org.oscim.layers.tile.MapTile.State.NEW_DATA;
|
||||||
import static org.oscim.layers.tile.MapTile.State.READY;
|
import static org.oscim.layers.tile.MapTile.State.READY;
|
||||||
|
|
||||||
import org.oscim.core.MapPosition;
|
|
||||||
import org.oscim.layers.tile.MapTile.TileNode;
|
import org.oscim.layers.tile.MapTile.TileNode;
|
||||||
import org.oscim.renderer.BufferObject;
|
import org.oscim.renderer.BufferObject;
|
||||||
import org.oscim.renderer.GLViewport;
|
import org.oscim.renderer.GLViewport;
|
||||||
@ -79,50 +78,61 @@ public abstract class TileRenderer extends LayerRenderer {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public synchronized void update(GLViewport v) {
|
public synchronized void update(GLViewport v) {
|
||||||
|
/* count placeholder tiles */
|
||||||
|
|
||||||
if (mAlpha == 0) {
|
if (mAlpha == 0) {
|
||||||
mDrawTiles.releaseTiles();
|
mDrawTiles.releaseTiles();
|
||||||
|
setReady(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* get current tiles to draw */
|
|
||||||
boolean tilesChanged;
|
|
||||||
synchronized (tilelock) {
|
|
||||||
tilesChanged = mTileManager.getActiveTiles(mDrawTiles);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mDrawTiles.cnt == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
/* keep constant while rendering frame */
|
/* keep constant while rendering frame */
|
||||||
mLayerAlpha = mAlpha;
|
mLayerAlpha = mAlpha;
|
||||||
mOverdrawColor = mOverdraw;
|
mOverdrawColor = mOverdraw;
|
||||||
|
|
||||||
int tileCnt = mDrawTiles.cnt;
|
/* get current tiles to draw */
|
||||||
MapTile[] tiles = mDrawTiles.tiles;
|
synchronized (tilelock) {
|
||||||
|
boolean tilesChanged = mTileManager.getActiveTiles(mDrawTiles);
|
||||||
|
|
||||||
if (tilesChanged || v.changed()) {
|
if (mDrawTiles.cnt == 0) {
|
||||||
updateTileVisibility(v.pos, v.plane);
|
setReady(false);
|
||||||
|
mProxyTileCnt = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* update isVisible flag true for tiles that intersect view */
|
||||||
|
if (tilesChanged || v.changed()) {
|
||||||
|
|
||||||
|
/* lock tiles while updating isVisible state */
|
||||||
|
mProxyTileCnt = 0;
|
||||||
|
|
||||||
|
MapTile[] tiles = mDrawTiles.tiles;
|
||||||
|
int tileZoom = tiles[0].zoomLevel;
|
||||||
|
|
||||||
|
for (int i = 0; i < mDrawTiles.cnt; i++)
|
||||||
|
tiles[i].isVisible = false;
|
||||||
|
|
||||||
|
/* no renderable tile can be locked at this point */
|
||||||
|
if (tileZoom > v.pos.zoomLevel + 2 || tileZoom < v.pos.zoomLevel - 4) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* check visibile tiles */
|
||||||
|
mScanBox.scan(v.pos.x, v.pos.y, v.pos.scale, tileZoom, v.plane);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tileCnt += mProxyTileCnt;
|
|
||||||
|
|
||||||
/* prepare tiles for rendering */
|
/* prepare tiles for rendering */
|
||||||
if (compileTileLayers(tiles, tileCnt) > 0) {
|
if (compileTileLayers(mDrawTiles.tiles, mDrawTiles.cnt + mProxyTileCnt) > 0) {
|
||||||
mUploadSerial++;
|
mUploadSerial++;
|
||||||
BufferObject.checkBufferUsage(false);
|
BufferObject.checkBufferUsage(false);
|
||||||
}
|
}
|
||||||
}
|
setReady(true);
|
||||||
|
|
||||||
@Override
|
|
||||||
public void render(GLViewport v) {
|
|
||||||
/* render in update() so that tiles cannot vanish in between. */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void clearTiles() {
|
public void clearTiles() {
|
||||||
/* Clear all references to MapTiles as all current
|
/* Clear all references to MapTiles as all current
|
||||||
* tiles will also be removed from TileManager. */
|
* tiles will also be removed from TileManager. */
|
||||||
//mDrawTiles = new TileSet();
|
mDrawTiles.releaseTiles();
|
||||||
mDrawTiles.tiles = new MapTile[1];
|
mDrawTiles.tiles = new MapTile[1];
|
||||||
mDrawTiles.cnt = 0;
|
mDrawTiles.cnt = 0;
|
||||||
}
|
}
|
||||||
@ -137,17 +147,17 @@ public abstract class TileRenderer extends LayerRenderer {
|
|||||||
if (!tile.isVisible)
|
if (!tile.isVisible)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (tile.state == READY)
|
if (tile.state(READY))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (tile.state == NEW_DATA) {
|
if (tile.state(NEW_DATA)) {
|
||||||
uploadCnt += uploadTileData(tile);
|
uploadCnt += uploadTileData(tile);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* load tile that is referenced by this holder */
|
/* load tile that is referenced by this holder */
|
||||||
MapTile proxy = tile.holder;
|
MapTile proxy = tile.holder;
|
||||||
if (proxy != null && proxy.state == NEW_DATA) {
|
if (proxy != null && proxy.state(NEW_DATA)) {
|
||||||
uploadCnt += uploadTileData(proxy);
|
uploadCnt += uploadTileData(proxy);
|
||||||
tile.state = proxy.state;
|
tile.state = proxy.state;
|
||||||
continue;
|
continue;
|
||||||
@ -174,7 +184,7 @@ public abstract class TileRenderer extends LayerRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static int uploadTileData(MapTile tile) {
|
private static int uploadTileData(MapTile tile) {
|
||||||
tile.state = READY;
|
tile.setState(READY);
|
||||||
RenderBuckets buckets = tile.getBuckets();
|
RenderBuckets buckets = tile.getBuckets();
|
||||||
|
|
||||||
/* tile might only contain label layers */
|
/* tile might only contain label layers */
|
||||||
@ -191,30 +201,6 @@ public abstract class TileRenderer extends LayerRenderer {
|
|||||||
|
|
||||||
private final Object tilelock = new Object();
|
private final Object tilelock = new Object();
|
||||||
|
|
||||||
/** set tile isVisible flag true for tiles that intersect view */
|
|
||||||
private void updateTileVisibility(MapPosition pos, float[] box) {
|
|
||||||
|
|
||||||
/* lock tiles while updating isVisible state */
|
|
||||||
synchronized (tilelock) {
|
|
||||||
MapTile[] tiles = mDrawTiles.tiles;
|
|
||||||
|
|
||||||
int tileZoom = tiles[0].zoomLevel;
|
|
||||||
|
|
||||||
for (int i = 0; i < mDrawTiles.cnt; i++)
|
|
||||||
tiles[i].isVisible = false;
|
|
||||||
|
|
||||||
if (tileZoom > pos.zoomLevel + 2 || tileZoom < pos.zoomLevel - 4) {
|
|
||||||
//log.debug("skip: zoomlevel diff " + (tileZoom - pos.zoomLevel));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
/* count placeholder tiles */
|
|
||||||
mProxyTileCnt = 0;
|
|
||||||
|
|
||||||
/* check visibile tiles */
|
|
||||||
mScanBox.scan(pos.x, pos.y, pos.scale, tileZoom, box);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update tileSet with currently visible tiles get a TileSet of currently
|
* Update tileSet with currently visible tiles get a TileSet of currently
|
||||||
* visible tiles
|
* visible tiles
|
||||||
@ -248,7 +234,7 @@ public abstract class TileRenderer extends LayerRenderer {
|
|||||||
tileSet.cnt = 0;
|
tileSet.cnt = 0;
|
||||||
for (int i = 0; i < cnt; i++) {
|
for (int i = 0; i < cnt; i++) {
|
||||||
MapTile t = newTiles[i];
|
MapTile t = newTiles[i];
|
||||||
if (t.isVisible && t.state == READY) {
|
if (t.isVisible && t.state(READY)) {
|
||||||
t.lock();
|
t.lock();
|
||||||
tileSet.tiles[tileSet.cnt++] = t;
|
tileSet.tiles[tileSet.cnt++] = t;
|
||||||
}
|
}
|
||||||
|
@ -45,8 +45,7 @@ public class VectorTileRenderer extends TileRenderer {
|
|||||||
protected int mDrawSerial;
|
protected int mDrawSerial;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized void update(GLViewport v) {
|
public synchronized void render(GLViewport v) {
|
||||||
super.update(v);
|
|
||||||
|
|
||||||
/* discard depth projection from tilt, depth buffer
|
/* discard depth projection from tilt, depth buffer
|
||||||
* is used for clipping */
|
* is used for clipping */
|
||||||
@ -67,8 +66,8 @@ public class VectorTileRenderer extends TileRenderer {
|
|||||||
|
|
||||||
for (int i = 0; i < tileCnt; i++) {
|
for (int i = 0; i < tileCnt; i++) {
|
||||||
MapTile t = tiles[i];
|
MapTile t = tiles[i];
|
||||||
// TODO check if proxies are actually available
|
|
||||||
if (t.isVisible && t.state != READY) {
|
if (t.isVisible && !t.state(READY)) {
|
||||||
GL.glDepthMask(true);
|
GL.glDepthMask(true);
|
||||||
GL.glClear(GL20.GL_DEPTH_BUFFER_BIT);
|
GL.glClear(GL20.GL_DEPTH_BUFFER_BIT);
|
||||||
|
|
||||||
@ -87,7 +86,7 @@ public class VectorTileRenderer extends TileRenderer {
|
|||||||
/* draw visible tiles */
|
/* draw visible tiles */
|
||||||
for (int i = 0; i < tileCnt; i++) {
|
for (int i = 0; i < tileCnt; i++) {
|
||||||
MapTile t = tiles[i];
|
MapTile t = tiles[i];
|
||||||
if (t.isVisible && t.state == READY)
|
if (t.isVisible && t.state(READY))
|
||||||
drawTile(t, v, 0);
|
drawTile(t, v, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,8 +154,10 @@ public class VectorTileRenderer extends TileRenderer {
|
|||||||
? tile.getBuckets()
|
? tile.getBuckets()
|
||||||
: tile.holder.getBuckets();
|
: tile.holder.getBuckets();
|
||||||
|
|
||||||
if (buckets == null || buckets.vbo == null)
|
if (buckets == null || buckets.vbo == null) {
|
||||||
|
//log.debug("{} no buckets!", tile);
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
MapPosition pos = v.pos;
|
MapPosition pos = v.pos;
|
||||||
/* place tile relative to map position */
|
/* place tile relative to map position */
|
||||||
|
@ -33,7 +33,7 @@ public class BitmapTileLayer extends TileLayer {
|
|||||||
|
|
||||||
protected static final Logger log = LoggerFactory.getLogger(BitmapTileLayer.class);
|
protected static final Logger log = LoggerFactory.getLogger(BitmapTileLayer.class);
|
||||||
|
|
||||||
private final static int CACHE_LIMIT = 20;
|
private final static int CACHE_LIMIT = 40;
|
||||||
|
|
||||||
protected final TileSource mTileSource;
|
protected final TileSource mTileSource;
|
||||||
|
|
||||||
@ -110,7 +110,7 @@ public class BitmapTileLayer extends TileLayer {
|
|||||||
pool.clear();
|
pool.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
final static int POOL_FILL = 40;
|
final static int POOL_FILL = 20;
|
||||||
|
|
||||||
/** pool shared by TextLayers */
|
/** pool shared by TextLayers */
|
||||||
final TexturePool pool = new TexturePool(POOL_FILL) {
|
final TexturePool pool = new TexturePool(POOL_FILL) {
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.oscim.layers.tile.bitmap;
|
package org.oscim.layers.tile.bitmap;
|
||||||
|
|
||||||
import static org.oscim.layers.tile.MapTile.State.CANCEL;
|
import static org.oscim.layers.tile.MapTile.State.LOADING;
|
||||||
|
|
||||||
import org.oscim.backend.canvas.Bitmap;
|
import org.oscim.backend.canvas.Bitmap;
|
||||||
import org.oscim.core.Tile;
|
import org.oscim.core.Tile;
|
||||||
@ -55,23 +55,26 @@ public class BitmapTileLoader extends TileLoader {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setTileImage(Bitmap bitmap) {
|
public void setTileImage(Bitmap bitmap) {
|
||||||
if (isCanceled() || mTile.state(CANCEL))
|
if (isCanceled() || !mTile.state(LOADING)) {
|
||||||
|
bitmap.recycle();
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
BitmapBucket l = new BitmapBucket(false);
|
BitmapBucket l = new BitmapBucket(false);
|
||||||
l.setBitmap(bitmap, Tile.SIZE, Tile.SIZE, mLayer.pool);
|
l.setBitmap(bitmap, Tile.SIZE, Tile.SIZE, mLayer.pool);
|
||||||
|
|
||||||
RenderBuckets b = new RenderBuckets();
|
RenderBuckets buckets = new RenderBuckets();
|
||||||
b.set(l);
|
buckets.set(l);
|
||||||
mTile.data = b;
|
mTile.data = buckets;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void dispose() {
|
public void dispose() {
|
||||||
|
mTileDataSource.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void cancel() {
|
public void cancel() {
|
||||||
|
mTileDataSource.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.oscim.layers.tile.vector;
|
package org.oscim.layers.tile.vector;
|
||||||
|
|
||||||
import static org.oscim.layers.tile.MapTile.State.CANCEL;
|
import static org.oscim.layers.tile.MapTile.State.LOADING;
|
||||||
|
|
||||||
import org.oscim.core.GeometryBuffer.GeometryType;
|
import org.oscim.core.GeometryBuffer.GeometryType;
|
||||||
import org.oscim.core.MapElement;
|
import org.oscim.core.MapElement;
|
||||||
@ -134,14 +134,15 @@ public class VectorTileLoader extends TileLoader implements IRenderTheme.Callbac
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void completed(QueryResult result) {
|
public void completed(QueryResult result) {
|
||||||
mTileLayer.callHooksComplete(mTile, result == QueryResult.SUCCESS);
|
boolean ok = (result == QueryResult.SUCCESS);
|
||||||
|
|
||||||
|
mTileLayer.callHooksComplete(mTile, ok);
|
||||||
|
|
||||||
/* finish buckets- tessellate and cleanup on worker-thread */
|
/* finish buckets- tessellate and cleanup on worker-thread */
|
||||||
mBuckets.prepare();
|
mBuckets.prepare();
|
||||||
|
clearState();
|
||||||
|
|
||||||
super.completed(result);
|
super.completed(result);
|
||||||
|
|
||||||
clearState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static int getValidLayer(int layer) {
|
protected static int getValidLayer(int layer) {
|
||||||
@ -180,7 +181,7 @@ public class VectorTileLoader extends TileLoader implements IRenderTheme.Callbac
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void process(MapElement element) {
|
public void process(MapElement element) {
|
||||||
if (isCanceled() || mTile.state(CANCEL))
|
if (isCanceled() || !mTile.state(LOADING))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (mTileLayer.callProcessHooks(mTile, mBuckets, element))
|
if (mTileLayer.callProcessHooks(mTile, mBuckets, element))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user