splitup labeling

This commit is contained in:
Hannes Janetzek 2014-01-28 00:55:36 +01:00
parent e620d15964
commit b6d9ed254e
9 changed files with 766 additions and 813 deletions

View File

@ -17,6 +17,8 @@
package org.oscim.layers.tile.vector.labeling; package org.oscim.layers.tile.vector.labeling;
import org.oscim.backend.canvas.Color; import org.oscim.backend.canvas.Color;
import org.oscim.core.MapPosition;
import org.oscim.renderer.MapRenderer.Matrices;
import org.oscim.renderer.elements.ElementLayers; import org.oscim.renderer.elements.ElementLayers;
import org.oscim.renderer.elements.LineLayer; import org.oscim.renderer.elements.LineLayer;
import org.oscim.renderer.elements.TextItem; import org.oscim.renderer.elements.TextItem;
@ -25,8 +27,10 @@ import org.oscim.theme.styles.Line;
class Debug { class Debug {
private final static float[] mDebugPoints = new float[8]; private final static float[] mDebugPoints = new float[8];
// TODO Auto-generated method stub
static ElementLayers dbg;
static void addDebugBox(ElementLayers dbg, Label l, TextItem ti, int overlaps, boolean prev, static void addDebugBox(Label l, TextItem ti, int overlaps, boolean prev,
float scale) { float scale) {
LineLayer ll; LineLayer ll;
@ -75,4 +79,19 @@ class Debug {
dbg.addLineLayer(5, new Line((Color.MAGENTA & alpha), 2)); dbg.addLineLayer(5, new Line((Color.MAGENTA & alpha), 2));
} }
public static void draw(MapPosition pos, Matrices m, ElementLayers layers) {
// if (layers.baseLayers != null) {
// //setMatrix(pos, m, true);
//
// for (RenderElement l = layers.baseLayers; l != null;) {
// if (l.type == RenderElement.POLYGON) {
// l = PolygonLayer.Renderer.draw(pos, l, m, true, 1, false);
// } else {
// //float div = (float) (mMapPosition.scale / (1 << pos.zoomLevel));
// l = LineLayer.Renderer.draw(layers, l, pos, m, div);
// }
// }
// }
}
} }

View File

@ -17,10 +17,9 @@
package org.oscim.layers.tile.vector.labeling; package org.oscim.layers.tile.vector.labeling;
import org.oscim.renderer.elements.TextItem; import org.oscim.renderer.elements.TextItem;
import org.oscim.tiling.MapTile;
import org.oscim.utils.OBB2D; import org.oscim.utils.OBB2D;
class Label extends TextItem { final class Label extends TextItem {
TextItem item; TextItem item;
//Link blocking; //Link blocking;
@ -28,34 +27,75 @@ class Label extends TextItem {
// shared list of all label for a tile // shared list of all label for a tile
//Link siblings; //Link siblings;
MapTile tile; int tileX;
int tileY;
int tileZ;
//public byte origin;
public int active; public int active;
public OBB2D bbox; public OBB2D bbox;
public TextItem move(TextItem ti, float dx, float dy, float scale) { public Label clone(TextItem ti) {
this.x = (dx + ti.x) * scale;
this.y = (dy + ti.y) * scale;
return this;
}
public void clone(TextItem ti) {
this.string = ti.string; this.string = ti.string;
this.text = ti.text; this.text = ti.text;
this.width = ti.width; this.width = ti.width;
this.length = ti.length; this.length = ti.length;
} return this;
public void setAxisAlignedBBox() {
this.x1 = x - width / 2;
this.y1 = y - text.fontHeight / 2;
this.x2 = x + width / 2;
this.y2 = y + text.fontHeight / 2;
} }
static int comparePriority(Label l1, Label l2) { static int comparePriority(Label l1, Label l2) {
return 0; return 0;
} }
public static boolean shareText(Label l, Label ll) {
if (l.text != ll.text)
return false;
if (l.string == ll.string)
return true;
if (l.string.equals(ll.string)) {
// make strings unique, should be done only once..
l.string = ll.string;
return true;
}
return false;
}
public static boolean bboxOverlaps(TextItem it1, TextItem it2, float add) {
if (it1.y1 < it1.y2) {
if (it2.y1 < it2.y2)
return (it1.x1 - add < it2.x2)
&& (it2.x1 < it1.x2 + add)
&& (it1.y1 - add < it2.y2)
&& (it2.y1 < it1.y2 + add);
// flip it2
return (it1.x1 - add < it2.x2)
&& (it2.x1 < it1.x2 + add)
&& (it1.y1 - add < it2.y1)
&& (it2.y2 < it1.y2 + add);
}
// flip it1
if (it2.y1 < it2.y2)
return (it1.x1 - add < it2.x2)
&& (it2.x1 < it1.x2 + add)
&& (it1.y2 - add < it2.y2)
&& (it2.y1 < it1.y1 + add);
// flip both
return (it1.x1 - add < it2.x2)
&& (it2.x1 < it1.x2 + add)
&& (it1.y2 - add < it2.y1)
&& (it2.y2 < it1.y1 + add);
}
public void setAxisAlignedBBox() {
this.x1 = (int) (x - width / 2);
this.y1 = (int) (y - text.fontHeight / 2);
this.x2 = (int) (x + width / 2);
this.y2 = (int) (y + text.fontHeight / 2);
}
} }

View File

@ -21,32 +21,83 @@ import org.oscim.event.MotionEvent;
import org.oscim.layers.Layer; import org.oscim.layers.Layer;
import org.oscim.map.Map; import org.oscim.map.Map;
import org.oscim.tiling.TileRenderer; import org.oscim.tiling.TileRenderer;
import org.oscim.utils.async.SimpleWorker;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
public class LabelLayer extends Layer implements Map.InputListener, Map.UpdateListener { public class LabelLayer extends Layer implements Map.InputListener, Map.UpdateListener {
static final Logger log = LoggerFactory.getLogger(LabelLayer.class); static final Logger log = LoggerFactory.getLogger(LabelLayer.class);
private final TextRenderer mTextRenderer;
//private int multi; private final static long MAX_RELABEL_DELAY = 100;
public LabelLayer(Map map, TileRenderer tileRenderLayer) { private final LabelPlacement mLabelPlacer;
private final Worker mWorker;
public LabelLayer(Map map, TileRenderer tileRenderer) {
super(map); super(map);
mLabelPlacer = new LabelPlacement(map, tileRenderer);
mWorker = new Worker(map);
mRenderer = new TextRenderer(mWorker);
}
//mTextLayer = new org.oscim.renderer.layers.TextRenderLayer(map, tileRenderLayer); class Worker extends SimpleWorker<LabelTask> {
mTextRenderer = new TextRenderer(map, tileRenderLayer);
mRenderer = mTextRenderer; public Worker(Map map) {
super(map, 50, new LabelTask(), new LabelTask());
}
@Override
public boolean doWork(LabelTask t) {
if (mLabelPlacer.updateLabels(t)) {
mMap.render();
return true;
}
return false;
}
@Override
public void cleanup(LabelTask t) {
}
@Override
public void finish() {
mLabelPlacer.cleanup();
}
public synchronized boolean isRunning() {
return mRunning;
}
}
public void clearLabels() {
mWorker.cancel(true);
}
public void update() {
mWorker.submit(MAX_RELABEL_DELAY);
} }
@Override @Override
public void onDetach() { public void onDetach() {
// TODO stop and clear labeling thread // TODO stop and clear labeling thread
log.debug("DETACH"); log.debug("DETACH");
mTextRenderer.clearLabels();
// clear labels
mWorker.cancel(true);
super.onDetach(); super.onDetach();
} }
@Override
public void onMapUpdate(MapPosition mapPosition, boolean changed, boolean clear) {
if (clear)
mWorker.cancel(true);
mWorker.submit(MAX_RELABEL_DELAY);
}
@Override @Override
public void onMotionEvent(MotionEvent e) { public void onMotionEvent(MotionEvent e) {
// int action = e.getAction() & MotionEvent.ACTION_MASK; // int action = e.getAction() & MotionEvent.ACTION_MASK;
@ -64,31 +115,4 @@ public class LabelLayer extends Layer implements Map.InputListener, Map.UpdateLi
// } // }
} }
@Override
public void onMapUpdate(MapPosition mapPosition, boolean changed, boolean clear) {
if (clear)
mTextRenderer.clearLabels();
mTextRenderer.update();
}
// @Override
// public boolean onTouchEvent(MotionEvent e) {
// int action = e.getAction() & MotionEvent.ACTION_MASK;
// if (action == MotionEvent.ACTION_POINTER_DOWN) {
// multi++;
// mTextRenderer.hold(true);
// } else if (action == MotionEvent.ACTION_POINTER_UP) {
// multi--;
// if (multi == 0)
// mTextRenderer.hold(false);
// } else if (action == MotionEvent.ACTION_CANCEL) {
// multi = 0;
// log.debug("cancel " + multi);
// mTextRenderer.hold(false);
// }
//
// return false;
// }
} }

View File

@ -0,0 +1,483 @@
package org.oscim.layers.tile.vector.labeling;
import org.oscim.core.MapPosition;
import org.oscim.core.Tile;
import org.oscim.map.Map;
import org.oscim.renderer.elements.SymbolItem;
import org.oscim.renderer.elements.SymbolLayer;
import org.oscim.renderer.elements.TextItem;
import org.oscim.tiling.MapTile;
import org.oscim.tiling.TileRenderer;
import org.oscim.tiling.TileSet;
import org.oscim.utils.FastMath;
import org.oscim.utils.OBB2D;
public class LabelPlacement {
static final boolean dbg = false;
private final static float MIN_CAPTION_DIST = 5;
private final static float MIN_WAY_DIST = 3;
/** thread local pool of for unused label items */
private final LabelPool mPool = new LabelPool();
private final TileSet mTileSet = new TileSet();
private final TileRenderer mTileRenderer;
private final Map mMap;
/** list of current labels */
private Label mLabels;
private float mSquareRadius;
/**
* incremented each update, to prioritize labels
* that became visible ealier.
*/
private int mRelabelCnt;
public LabelPlacement(Map map, TileRenderer tileRenderer) {
mMap = map;
mTileRenderer = tileRenderer;
}
/** remove Label l from mLabels and return l.next */
private Label removeLabel(Label l) {
Label ret = (Label) l.next;
mLabels = (Label) mPool.release(mLabels, l);
return ret;
}
public void addLabel(Label l) {
for (Label o = mLabels; o != null; o = (Label) o.next) {
/* find other label with same text style */
if (l.text == o.text) {
while (o.next != null
/* break if next item uses different text style */
&& l.text == o.next.text
/* check same string instance */
&& l.string != o.string
/* check same string */
&& !l.string.equals(o.string))
o = (Label) o.next;
/* Note: required for 'packing test' in prepare to work */
Label.shareText(l, o);
/* insert after text of same type or before same string */
l.next = o.next;
o.next = l;
return;
}
}
l.next = mLabels;
mLabels = l;
}
private byte checkOverlap(Label l) {
for (Label o = mLabels; o != null;) {
//check bounding box
if (!Label.bboxOverlaps(l, o, 150)) {
o = (Label) o.next;
continue;
}
if (Label.shareText(l, o)) {
// keep the label that was active earlier
if (o.active <= l.active)
return 1;
// keep the label with longer segment
if (o.length < l.length) {
o = removeLabel(o);
continue;
}
// keep other
return 2;
}
if (l.bbox.overlaps(o.bbox)) {
if (o.active <= l.active)
return 1;
if (!o.text.caption
&& (o.text.priority > l.text.priority
|| o.length < l.length)) {
o = removeLabel(o);
continue;
}
// keep other
return 1;
}
o = (Label) o.next;
}
return 0;
}
private boolean isVisible(float x, float y) {
// rough filter
float dist = x * x + y * y;
if (dist > mSquareRadius)
return false;
return true;
}
private boolean wayIsVisible(Label ti) {
// rough filter
float dist = ti.x * ti.x + ti.y * ti.y;
if (dist < mSquareRadius)
return true;
dist = ti.x1 * ti.x1 + ti.y1 * ti.y1;
if (dist < mSquareRadius)
return true;
dist = ti.x2 * ti.x2 + ti.y2 * ti.y2;
if (dist < mSquareRadius)
return true;
return false;
}
private Label getLabel() {
Label l = (Label) mPool.get();
l.active = Integer.MAX_VALUE;
return l;
}
private static float flipLongitude(float dx, int max) {
// flip around date-line
if (dx > max)
dx = dx - max * 2;
else if (dx < -max)
dx = dx + max * 2;
return dx;
}
private void placeLabelFrom(Label l, TextItem ti) {
// set line endpoints relative to view to be able to
// check intersections with label from other tiles
float w = (ti.x2 - ti.x1) / 2f;
float h = (ti.y2 - ti.y1) / 2f;
l.x1 = (int) (l.x - w);
l.y1 = (int) (l.y - h);
l.x2 = (int) (l.x + w);
l.y2 = (int) (l.y + h);
}
private Label addWayLabels(MapTile t, Label l, float dx, float dy,
double scale) {
for (TextItem ti = t.labels; ti != null; ti = ti.next) {
if (ti.text.caption)
continue;
/* acquire a TextItem to add to TextLayer */
if (l == null)
l = getLabel();
/* check if path at current scale is long enough */
if (!dbg && ti.width > ti.length * scale)
continue;
l.clone(ti);
l.x = (float) ((dx + ti.x) * scale);
l.y = (float) ((dy + ti.y) * scale);
placeLabelFrom(l, ti);
if (!wayIsVisible(l))
continue;
byte overlaps = -1;
if (l.bbox == null)
l.bbox = new OBB2D(l.x, l.y, l.x1, l.y1,
l.width + MIN_WAY_DIST,
l.text.fontHeight + MIN_WAY_DIST);
else
l.bbox.set(l.x, l.y, l.x1, l.y1,
l.width + MIN_WAY_DIST,
l.text.fontHeight + MIN_WAY_DIST);
if (dbg || ti.width < ti.length * scale)
overlaps = checkOverlap(l);
if (dbg)
Debug.addDebugBox(l, ti, overlaps, false, (float) scale);
if (overlaps == 0) {
addLabel(l);
l.item = TextItem.copy(ti);
l.tileX = t.tileX;
l.tileY = t.tileY;
l.tileZ = t.zoomLevel;
l.active = mRelabelCnt;
l = null;
}
}
return l;
}
private Label addNodeLabels(MapTile t, Label l, float dx, float dy,
double scale, float cos, float sin) {
O: for (TextItem ti = t.labels; ti != null; ti = ti.next) {
if (!ti.text.caption)
continue;
// acquire a TextItem to add to TextLayer
if (l == null)
l = getLabel();
l.clone(ti);
l.x = (float) ((dx + ti.x) * scale);
l.y = (float) ((dy + ti.y) * scale);
if (!isVisible(l.x, l.y))
continue;
if (l.bbox == null)
l.bbox = new OBB2D();
l.bbox.setNormalized(l.x, l.y, cos, -sin,
l.width + MIN_CAPTION_DIST,
l.text.fontHeight + MIN_CAPTION_DIST,
l.text.dy);
for (Label o = mLabels; o != null;) {
if (l.bbox.overlaps(o.bbox)) {
if (l.text.priority < o.text.priority) {
o = removeLabel(o);
continue;
}
continue O;
}
o = (Label) o.next;
}
addLabel(l);
l.item = TextItem.copy(ti);
l.tileX = t.tileX;
l.tileY = t.tileY;
l.tileZ = t.zoomLevel;
l.active = mRelabelCnt;
l = null;
}
return l;
}
boolean updateLabels(LabelTask work) {
/* get current tiles */
boolean changedTiles = mTileRenderer.getVisibleTiles(mTileSet);
if (mTileSet.cnt == 0) {
//log.debug("no tiles "+ mTileSet.getSerial());
return false;
}
MapPosition pos = work.pos;
boolean changedPos = mMap.getViewport().getMapPosition(pos);
/* do not loop! */
if (!changedTiles && !changedPos)
return false;
mRelabelCnt++;
MapTile[] tiles = mTileSet.tiles;
int zoom = tiles[0].zoomLevel;
/* estimation for visible area to be labeled */
int mw = (mMap.getWidth() + Tile.SIZE) / 2;
int mh = (mMap.getHeight() + Tile.SIZE) / 2;
mSquareRadius = mw * mw + mh * mh;
/* scale of tiles zoom-level relative to current position */
double scale = pos.scale / (1 << zoom);
double angle = Math.toRadians(pos.angle);
float cos = (float) Math.cos(angle);
float sin = (float) Math.sin(angle);
int maxx = Tile.SIZE << (zoom - 1);
// FIXME ???
SymbolLayer sl = work.symbolLayer;
sl.clearItems();
double tileX = (pos.x * (Tile.SIZE << zoom));
double tileY = (pos.y * (Tile.SIZE << zoom));
/* put current label to previous label */
Label prevLabels = mLabels;
/* new labels */
mLabels = null;
Label l = null;
/* add currently active labels first */
for (l = prevLabels; l != null;) {
if (l.text.caption) {
// TODO!!!
l = mPool.releaseAndGetNext(l);
continue;
}
int diff = l.tileZ - zoom;
if (diff > 1 || diff < -1) {
l = mPool.releaseAndGetNext(l);
continue;
}
float div = FastMath.pow(diff);
float sscale = (float) (pos.scale / (1 << l.tileZ));
if (l.width > l.length * sscale) {
l = mPool.releaseAndGetNext(l);
continue;
}
float dx = (float) (l.tileX * Tile.SIZE - tileX * div);
float dy = (float) (l.tileY * Tile.SIZE - tileY * div);
dx = flipLongitude(dx, maxx);
l.x = (float) ((dx + l.item.x) * sscale);
l.y = (float) ((dy + l.item.y) * sscale);
placeLabelFrom(l, l.item);
if (!wayIsVisible(l)) {
l = mPool.releaseAndGetNext(l);
continue;
}
l.bbox.set(l.x, l.y, l.x1, l.y1,
l.width + MIN_WAY_DIST,
l.text.fontHeight + MIN_WAY_DIST);
byte overlaps = checkOverlap(l);
if (dbg)
Debug.addDebugBox(l, l.item, overlaps, true, sscale);
if (overlaps == 0) {
Label ll = l;
l = (Label) l.next;
ll.next = null;
addLabel(ll);
continue;
}
l = mPool.releaseAndGetNext(l);
}
/* add way labels */
for (int i = 0, n = mTileSet.cnt; i < n; i++) {
MapTile t = tiles[i];
synchronized (t) {
if (!t.state(MapTile.STATE_READY))
continue;
float dx = (float) (t.tileX * Tile.SIZE - tileX);
float dy = (float) (t.tileY * Tile.SIZE - tileY);
dx = flipLongitude(dx, maxx);
l = addWayLabels(t, l, dx, dy, scale);
}
}
/* add caption */
for (int i = 0, n = mTileSet.cnt; i < n; i++) {
MapTile t = tiles[i];
synchronized (t) {
if (!t.state(MapTile.STATE_READY))
continue;
float dx = (float) (t.tileX * Tile.SIZE - tileX);
float dy = (float) (t.tileY * Tile.SIZE - tileY);
dx = flipLongitude(dx, maxx);
l = addNodeLabels(t, l, dx, dy, scale, cos, sin);
}
}
for (Label ti = mLabels; ti != null; ti = (Label) ti.next) {
/* add caption symbols */
if (ti.text.caption) {
if (ti.text.texture != null) {
SymbolItem s = SymbolItem.pool.get();
s.texRegion = ti.text.texture;
s.x = ti.x;
s.y = ti.y;
s.billboard = true;
sl.addSymbol(s);
}
continue;
}
/* flip way label orientation */
if (cos * (ti.x2 - ti.x1) - sin * (ti.y2 - ti.y1) < 0) {
float tmp = ti.x1;
ti.x1 = ti.x2;
ti.x2 = tmp;
tmp = ti.y1;
ti.y1 = ti.y2;
ti.y2 = tmp;
}
}
/* add symbol items */
for (int i = 0, n = mTileSet.cnt; i < n; i++) {
MapTile t = tiles[i];
synchronized (t) {
if (!t.state(MapTile.STATE_READY))
continue;
float dx = (float) (t.tileX * Tile.SIZE - tileX);
float dy = (float) (t.tileY * Tile.SIZE - tileY);
dx = flipLongitude(dx, maxx);
for (SymbolItem ti = t.symbols; ti != null; ti = ti.next) {
if (ti.texRegion == null)
continue;
int x = (int) ((dx + ti.x) * scale);
int y = (int) ((dy + ti.y) * scale);
if (!isVisible(x, y))
continue;
SymbolItem s = SymbolItem.pool.get();
s.texRegion = ti.texRegion;
s.x = x;
s.y = y;
s.billboard = true;
sl.addSymbol(s);
}
}
}
/* temporary used Label */
l = (Label) mPool.release(l);
/* draw text to bitmaps and create vertices */
work.textLayer.labels = mLabels;
work.textLayer.prepare();
work.textLayer.labels = null;
/* remove tile locks */
mTileRenderer.releaseTiles(mTileSet);
return true;
}
public void cleanup() {
mLabels = (Label) mPool.releaseAll(mLabels);
mTileSet.releaseTiles();
}
}

View File

@ -0,0 +1,25 @@
package org.oscim.layers.tile.vector.labeling;
import org.oscim.renderer.elements.TextItem;
import org.oscim.utils.pool.Pool;
final class LabelPool extends Pool<TextItem> {
Label releaseAndGetNext(Label l) {
if (l.item != null)
l.item = TextItem.pool.release(l.item);
// drop references
l.item = null;
l.string = null;
Label ret = (Label) l.next;
// ignore warning
super.release(l);
return ret;
}
@Override
protected Label createItem() {
return new Label();
}
}

View File

@ -0,0 +1,26 @@
package org.oscim.layers.tile.vector.labeling;
import org.oscim.core.MapPosition;
import org.oscim.renderer.elements.SymbolLayer;
import org.oscim.renderer.elements.TextLayer;
import org.oscim.renderer.elements.TextureLayer;
final class LabelTask {
final TextureLayer layers;
final TextLayer textLayer;
final SymbolLayer symbolLayer;
final MapPosition pos;
LabelTask() {
pos = new MapPosition();
symbolLayer = new SymbolLayer();
textLayer = new TextLayer();
layers = symbolLayer;
symbolLayer.next = textLayer;
}
}

View File

@ -27,582 +27,27 @@ package org.oscim.layers.tile.vector.labeling;
// 2.4 use 4 point labeling // 2.4 use 4 point labeling
// 3 join segments that belong to one feature // 3 join segments that belong to one feature
// 4 handle zoom-level changes // 4 handle zoom-level changes
// 5 R-Tree might be handy // 5 QuadTree might be handy
// //
import org.oscim.core.MapPosition; import org.oscim.core.MapPosition;
import org.oscim.core.Tile; import org.oscim.layers.tile.vector.labeling.LabelLayer.Worker;
import org.oscim.map.Map;
import org.oscim.map.Viewport;
import org.oscim.renderer.ElementRenderer; import org.oscim.renderer.ElementRenderer;
import org.oscim.renderer.GLState; import org.oscim.renderer.GLState;
import org.oscim.renderer.MapRenderer.Matrices; import org.oscim.renderer.MapRenderer.Matrices;
import org.oscim.renderer.elements.ElementLayers;
import org.oscim.renderer.elements.LineLayer;
import org.oscim.renderer.elements.PolygonLayer;
import org.oscim.renderer.elements.RenderElement; import org.oscim.renderer.elements.RenderElement;
import org.oscim.renderer.elements.SymbolItem;
import org.oscim.renderer.elements.SymbolLayer;
import org.oscim.renderer.elements.TextItem;
import org.oscim.renderer.elements.TextLayer;
import org.oscim.renderer.elements.TextureLayer; import org.oscim.renderer.elements.TextureLayer;
import org.oscim.tiling.MapTile;
import org.oscim.tiling.TileRenderer;
import org.oscim.tiling.TileSet;
import org.oscim.utils.FastMath;
import org.oscim.utils.OBB2D;
import org.oscim.utils.async.SimpleWorker;
import org.oscim.utils.pool.Pool;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
class TextRenderer extends ElementRenderer { class TextRenderer extends ElementRenderer {
static final Logger log = LoggerFactory.getLogger(TextRenderer.class); static final Logger log = LoggerFactory.getLogger(TextRenderer.class);
static final boolean dbg = false;
private final static float MIN_CAPTION_DIST = 5;
private final static float MIN_WAY_DIST = 3;
private final static long MAX_RELABEL_DELAY = 200;
private final Viewport mViewport;
private final TileSet mTileSet;
//private ElementLayers mDebugLayer;
class LabelTask {
final TextureLayer layers;
final TextLayer textLayer;
final SymbolLayer symbolLayer;
final MapPosition pos;
LabelTask() {
pos = new MapPosition();
symbolLayer = new SymbolLayer();
textLayer = new TextLayer();
layers = symbolLayer;
symbolLayer.next = textLayer;
}
}
// thread local pool (labeling)
class LabelPool extends Pool<TextItem> {
Label releaseAndGetNext(Label l) {
if (l.item != null)
l.item = TextItem.pool.release(l.item);
// drop references
l.item = null;
l.tile = null;
l.string = null;
Label ret = (Label) l.next;
// ignore warning
super.release(l);
return ret;
}
@Override
protected TextItem createItem() {
return new Label();
}
}
private final LabelPool mPool = new LabelPool();
// list of current labels
private Label mLabels;
//private final float[] mTmpCoords = new float[8];
//private final HashMap<MapTile, LabelTile> mActiveTiles;
private float mSquareRadius;
private int mRelabelCnt;
private final TileRenderer mTileLayer;
private final Map mMap;
private final Worker mWorker; private final Worker mWorker;
public TextRenderer(Map map, TileRenderer baseLayer) { public TextRenderer(Worker worker) {
mMap = map; mWorker = worker;
mViewport = map.getViewport();
mTileLayer = baseLayer;
mTileSet = new TileSet();
layers.textureLayers = new TextLayer();
layers.textureLayers.next = new SymbolLayer();
//mActiveTiles = new HashMap<MapTile, LabelTile>();
mRelabelCnt = 0;
mWorker = new Worker(map);
}
// remove Label l from mLabels and return l.next
private Label removeLabel(Label l) {
Label ret = (Label) l.next;
mLabels = (Label) mPool.release(mLabels, l);
return ret;
}
public void addLabel(Label l) {
TextItem ll = mLabels;
for (; ll != null; ll = ll.next) {
// find other label with same text style
if (l.text == ll.text) {
while (ll.next != null
// break if next item uses different text style
&& l.text == ll.next.text
// check same string instance
&& l.string != ll.string
// check same string
&& !l.string.equals(ll.string))
ll = ll.next;
// Note: this is required for 'packing test' in prepare to work!
TextItem.shareText(l, ll);
// insert after text of same type and/or before same string
l.next = ll.next;
ll.next = l;
return;
}
}
l.next = mLabels;
mLabels = l;
}
private byte checkOverlap(Label l) {
for (Label ll = mLabels; ll != null;) {
// check bounding box
if (!TextItem.bboxOverlaps(l, ll, 150)) {
ll = (Label) ll.next;
continue;
}
if (TextItem.shareText(l, ll)) {
// keep the label that was active earlier
if (ll.active <= l.active)
return 1;
// keep the label with longer segment
if (ll.length < l.length) {
ll = removeLabel(ll);
continue;
}
return 2;
}
boolean intersect = l.bbox.overlaps(ll.bbox);
if (intersect) {
if (ll.active <= l.active)
return 1;
//log.debug("intersection " + lp.string + " <> " + ti.string
// + " at " + ti.x + ":" + ti.y);
if (!ll.text.caption
&& (ll.text.priority > l.text.priority || ll.length < l.length)) {
ll = removeLabel(ll);
continue;
}
return 1;
}
ll = (Label) ll.next;
}
return 0;
}
private boolean nodeIsVisible(TextItem ti) {
// rough filter
float dist = ti.x * ti.x + ti.y * ti.y;
if (dist > mSquareRadius)
return false;
return true;
}
private boolean iconIsVisible(int x, int y) {
// rough filter
float dist = x * x + y * y;
if (dist > mSquareRadius)
return false;
return true;
}
private boolean wayIsVisible(TextItem ti) {
// rough filter
float dist = ti.x * ti.x + ti.y * ti.y;
if (dist < mSquareRadius)
return true;
dist = ti.x1 * ti.x1 + ti.y1 * ti.y1;
if (dist < mSquareRadius)
return true;
dist = ti.x2 * ti.x2 + ti.y2 * ti.y2;
if (dist < mSquareRadius)
return true;
return false;
}
private Label getLabel() {
Label l = (Label) mPool.get();
l.active = Integer.MAX_VALUE;
return l;
}
private static float flipLongitude(float dx, int max) {
// flip around date-line
if (dx > max)
dx = dx - max * 2;
else if (dx < -max)
dx = dx + max * 2;
return dx;
}
private Label updateWayLabels(MapTile t, Label l, float dx, float dy, double scale,
ElementLayers dbg) {
for (TextItem ti = t.labels; ti != null; ti = ti.next) {
if (ti.text.caption)
continue;
// acquire a TextItem to add to TextLayer
if (l == null)
l = getLabel();
// check if path at current scale is long enough for text
if (dbg == null && ti.width > ti.length * scale)
continue;
l.clone(ti);
l.move(ti, dx, dy, (float) scale);
// set line endpoints relative to view to be able to
// check intersections with label from other tiles
float w = (ti.x2 - ti.x1) / 2f;
float h = (ti.y2 - ti.y1) / 2f;
l.bbox = null;
l.x1 = l.x - w;
l.y1 = l.y - h;
l.x2 = l.x + w;
l.y2 = l.y + h;
if (!wayIsVisible(l))
continue;
byte overlaps = -1;
if (l.bbox == null)
l.bbox = new OBB2D(l.x, l.y, l.x1, l.y1,
l.width + MIN_WAY_DIST,
l.text.fontHeight + MIN_WAY_DIST);
else
l.bbox.set(l.x, l.y, l.x1, l.y1,
l.width + MIN_WAY_DIST,
l.text.fontHeight + MIN_WAY_DIST);
if (dbg == null || ti.width < ti.length * scale)
overlaps = checkOverlap(l);
if (dbg != null)
Debug.addDebugBox(dbg, l, ti, overlaps, false, (float) scale);
if (overlaps == 0) {
addLabel(l);
l.item = TextItem.copy(ti);
l.tile = t;
l.active = mRelabelCnt;
l = null;
}
}
return l;
}
private Label updateNodeLabels(MapTile t, Label l, float dx, float dy, double scale, float cos,
float sin) {
O: for (TextItem ti = t.labels; ti != null; ti = ti.next) {
if (!ti.text.caption)
continue;
// acquire a TextItem to add to TextLayer
if (l == null)
l = getLabel();
l.clone(ti);
l.move(ti, dx, dy, (float) scale);
if (!nodeIsVisible(l))
continue;
if (l.bbox == null)
l.bbox = new OBB2D();
l.bbox.setNormalized(l.x, l.y, cos, -sin,
l.width + MIN_CAPTION_DIST,
l.text.fontHeight + MIN_CAPTION_DIST,
l.text.dy);
for (Label lp = mLabels; lp != null;) {
if (l.bbox.overlaps(lp.bbox)) {
if (l.text.priority < lp.text.priority) {
lp = removeLabel(lp);
continue;
}
continue O;
}
lp = (Label) lp.next;
}
addLabel(l);
l.item = TextItem.copy(ti);
l.tile = t;
l.active = mRelabelCnt;
l = null;
}
return l;
}
boolean updateLabels(LabelTask work) {
// nextLayer is not loaded yet
//if (mNextLayer.ready)
// return false;
// get current tiles
boolean changedTiles = mTileLayer.getVisibleTiles(mTileSet);
boolean changedPos;
if (mTileSet.cnt == 0) {
//log.debug("no tiles "+ mTileSet.getSerial());
return false;
}
MapPosition pos = work.pos;
synchronized (mViewport) {
changedPos = mViewport.getMapPosition(pos);
//mViewport.getMapViewProjection(coords);
}
if (!changedTiles && !changedPos) {
//log.debug("not changed " + changedTiles + " " + changedPos);
return false;
}
ElementLayers dbg = null;
//if (mMap.getDebugSettings().debugLabels)
// dbg = new ElementLayers();
int mw = (mMap.getWidth() + Tile.SIZE) / 2;
int mh = (mMap.getHeight() + Tile.SIZE) / 2;
mSquareRadius = mw * mw + mh * mh;
MapTile[] tiles = mTileSet.tiles;
int zoom = tiles[0].zoomLevel;
// scale of tiles zoom-level relative to current position
double scale = pos.scale / (1 << zoom);
double angle = Math.toRadians(pos.angle);
float cos = (float) Math.cos(angle);
float sin = (float) Math.sin(angle);
int maxx = Tile.SIZE << (zoom - 1);
//if (dbg != null)
// Debug.addDebugLayers(dbg);
SymbolLayer sl = work.symbolLayer;
sl.clearItems();
mRelabelCnt++;
double tileX = (pos.x * (Tile.SIZE << zoom));
double tileY = (pos.y * (Tile.SIZE << zoom));
Label prevLabels = mLabels;
mLabels = null;
for (Label l = prevLabels; l != null;) {
if (l.text.caption) {
l = mPool.releaseAndGetNext(l);
continue;
}
int diff = l.tile.zoomLevel - zoom;
if (diff > 1 || diff < -1) {
l = mPool.releaseAndGetNext(l);
continue;
}
float div = FastMath.pow(diff);
float sscale = (float) (pos.scale / (1 << l.tile.zoomLevel));
if (l.width > l.length * sscale) {
l = mPool.releaseAndGetNext(l);
continue;
}
float dx = (float) (l.tile.tileX * Tile.SIZE - tileX * div);
float dy = (float) (l.tile.tileY * Tile.SIZE - tileY * div);
dx = flipLongitude(dx, maxx);
l.move(l.item, dx, dy, sscale);
// set line endpoints relative to view to be able to
// check intersections with label from other tiles
float w = (l.item.x2 - l.item.x1) / 2f;
float h = (l.item.y2 - l.item.y1) / 2f;
l.x1 = l.x - w;
l.y1 = l.y - h;
l.x2 = l.x + w;
l.y2 = l.y + h;
if (!wayIsVisible(l)) {
l = mPool.releaseAndGetNext(l);
continue;
}
l.bbox.set(l.x, l.y, l.x1, l.y1,
l.width + MIN_WAY_DIST,
l.text.fontHeight + MIN_WAY_DIST);
byte overlaps = checkOverlap(l);
if (dbg != null)
Debug.addDebugBox(dbg, l, l.item, overlaps, true, sscale);
if (overlaps == 0) {
Label ll = l;
l = (Label) l.next;
ll.next = null;
addLabel(ll);
continue;
}
l = mPool.releaseAndGetNext(l);
}
Label l = null;
/* add way labels */
for (int i = 0, n = mTileSet.cnt; i < n; i++) {
MapTile t = tiles[i];
synchronized (t) {
if (!t.state(MapTile.STATE_READY))
continue;
float dx = (float) (t.tileX * Tile.SIZE - tileX);
float dy = (float) (t.tileY * Tile.SIZE - tileY);
dx = flipLongitude(dx, maxx);
l = updateWayLabels(t, l, dx, dy, scale, dbg);
}
}
/* add caption */
for (int i = 0, n = mTileSet.cnt; i < n; i++) {
MapTile t = tiles[i];
synchronized (t) {
if (!t.state(MapTile.STATE_READY))
continue;
float dx = (float) (t.tileX * Tile.SIZE - tileX);
float dy = (float) (t.tileY * Tile.SIZE - tileY);
dx = flipLongitude(dx, maxx);
l = updateNodeLabels(t, l, dx, dy, scale, cos, sin);
}
}
for (Label ti = mLabels; ti != null; ti = (Label) ti.next) {
if (ti.text.caption) {
if (ti.text.texture != null) {
SymbolItem s = SymbolItem.pool.get();
s.texRegion = ti.text.texture;
s.x = ti.x;
s.y = ti.y;
s.billboard = true;
sl.addSymbol(s);
}
continue;
}
// flip label upside-down
if (cos * (ti.x2 - ti.x1) - sin * (ti.y2 - ti.y1) < 0) {
float tmp = ti.x1;
ti.x1 = ti.x2;
ti.x2 = tmp;
tmp = ti.y1;
ti.y1 = ti.y2;
ti.y2 = tmp;
}
}
for (int i = 0, n = mTileSet.cnt; i < n; i++) {
MapTile t = tiles[i];
synchronized (t) {
if (!t.state(MapTile.STATE_READY))
continue;
float dx = (float) (t.tileX * Tile.SIZE - tileX);
float dy = (float) (t.tileY * Tile.SIZE - tileY);
dx = flipLongitude(dx, maxx);
for (SymbolItem ti = t.symbols; ti != null; ti = ti.next) {
if (ti.texRegion == null)
continue;
int x = (int) ((dx + ti.x) * scale);
int y = (int) ((dy + ti.y) * scale);
if (!iconIsVisible(x, y))
continue;
SymbolItem s = SymbolItem.pool.get();
s.texRegion = ti.texRegion;
s.x = x;
s.y = y;
s.billboard = true;
sl.addSymbol(s);
}
}
}
// temporary used Label
l = (Label) mPool.release(l);
// draw text to bitmaps and create vertices
work.textLayer.labels = mLabels;
work.textLayer.prepare();
work.textLayer.labels = null;
// remove tile locks
mTileLayer.releaseTiles(mTileSet);
//mDebugLayer = dbg;
//mNextLayer.ready = true;
return true;
} }
long lastDraw = 0; long lastDraw = 0;
@ -613,12 +58,13 @@ class TextRenderer extends ElementRenderer {
LabelTask t; LabelTask t;
synchronized (mWorker) { synchronized (mWorker) {
t = mWorker.poll(); t = mWorker.poll();
if (t == null) {
if (t == null) if (!mWorker.isRunning()) {
mWorker.submit(50);
}
return; return;
}
layers.clear(); layers.clear();
} }
@ -626,70 +72,21 @@ class TextRenderer extends ElementRenderer {
layers.textureLayers = t.layers; layers.textureLayers = t.layers;
mMapPosition = t.pos; mMapPosition = t.pos;
compile(); compile();
update();
} }
@Override @Override
public synchronized void render(MapPosition pos, Matrices m) { public synchronized void render(MapPosition pos, Matrices m) {
GLState.test(false, false); GLState.test(false, false);
//Debug.draw(pos, layers);
layers.vbo.bind(); layers.vbo.bind();
float scale = (float) (mMapPosition.scale / pos.scale); float scale = (float) (mMapPosition.scale / pos.scale);
if (layers.baseLayers != null) {
setMatrix(pos, m, true);
for (RenderElement l = layers.baseLayers; l != null;) {
if (l.type == RenderElement.POLYGON) {
l = PolygonLayer.Renderer.draw(pos, l, m, true, 1, false);
} else {
float div = (float) (mMapPosition.scale / (1 << pos.zoomLevel));
l = LineLayer.Renderer.draw(layers, l, pos, m, div);
}
}
}
setMatrix(pos, m, false); setMatrix(pos, m, false);
for (RenderElement l = layers.textureLayers; l != null;) for (RenderElement l = layers.textureLayers; l != null;)
l = TextureLayer.Renderer.draw(l, scale, m); l = TextureLayer.Renderer.draw(l, scale, m);
} }
final class Worker extends SimpleWorker<LabelTask> {
public Worker(Map map) {
super(map, 10, new LabelTask(), new LabelTask());
}
@Override
public boolean doWork(LabelTask t) {
if (updateLabels(t)) {
mMap.render();
return true;
}
return false;
}
@Override
public void cleanup(LabelTask t) {
}
@Override
public void finish() {
mLabels = (Label) mPool.releaseAll(mLabels);
mTileSet.releaseTiles();
}
}
public void clearLabels() {
mWorker.cancel(true);
}
public void update() {
mWorker.submit(MAX_RELABEL_DELAY);
}
} }

View File

@ -57,22 +57,6 @@ public class TextItem extends Inlist<TextItem> {
return ti; return ti;
} }
public static boolean shareText(TextItem ti1, TextItem ti2) {
if (ti1.text != ti2.text)
return false;
if (ti1.string == ti2.string)
return true;
if (ti1.string.equals(ti2.string)) {
// make strings unique, should be done only once..
ti1.string = ti2.string;
return true;
}
return false;
}
public TextItem set(float x, float y, String string, Text text) { public TextItem set(float x, float y, String string, Text text) {
this.x = x; this.x = x;
this.y = y; this.y = y;
@ -86,35 +70,6 @@ public class TextItem extends Inlist<TextItem> {
return this; return this;
} }
public static boolean bboxOverlaps(TextItem it1, TextItem it2, float add) {
if (it1.y1 < it1.y2) {
if (it2.y1 < it2.y2)
return (it1.x1 - add < it2.x2)
&& (it2.x1 < it1.x2 + add)
&& (it1.y1 - add < it2.y2)
&& (it2.y1 < it1.y2 + add);
// flip it2
return (it1.x1 - add < it2.x2)
&& (it2.x1 < it1.x2 + add)
&& (it1.y1 - add < it2.y1)
&& (it2.y2 < it1.y2 + add);
}
// flip it1
if (it2.y1 < it2.y2)
return (it1.x1 - add < it2.x2)
&& (it2.x1 < it1.x2 + add)
&& (it1.y2 - add < it2.y2)
&& (it2.y1 < it1.y1 + add);
// flip both
return (it1.x1 - add < it2.x2)
&& (it2.x1 < it1.x2 + add)
&& (it1.y2 - add < it2.y1)
&& (it2.y2 < it1.y1 + add);
}
// link to next node // link to next node
//public TextItem next; //public TextItem next;

View File

@ -38,7 +38,6 @@ public final class TextLayer extends TextureLayer {
public TextLayer() { public TextLayer() {
super(RenderElement.SYMBOL); super(RenderElement.SYMBOL);
//mCanvas = Graphics.res.getCanvas();
mCanvas = CanvasAdapter.g.getCanvas(); mCanvas = CanvasAdapter.g.getCanvas();
fixed = true; fixed = true;
} }
@ -58,7 +57,7 @@ public final class TextLayer extends TextureLayer {
&& !item.string.equals(it.string)) && !item.string.equals(it.string))
it = it.next; it = it.next;
// unify duplicate string :) // unify duplicate string
// Note: this is required for 'packing test' in prepare to work! // Note: this is required for 'packing test' in prepare to work!
if (item.string != it.string && item.string.equals(it.string)) if (item.string != it.string && item.string.equals(it.string))
item.string = it.string; item.string = it.string;
@ -139,117 +138,22 @@ public final class TextLayer extends TextureLayer {
if (width > TEXTURE_WIDTH) if (width > TEXTURE_WIDTH)
width = TEXTURE_WIDTH; width = TEXTURE_WIDTH;
float hw = width / 2.0f;
float hh = height / 2.0f;
float hh2 = hh;
//if (!it.text.caption) {
// // displace by baseline
// float desc = 0; //(hh - (height - it.text.fontDescent) / 2);
//
// //float desc = it.text.fontDescent / 2;
// hh2 = hh + desc;
// hh = hh - desc;
//}
// texture coordinates
short u1 = (short) (COORD_SCALE * x);
short v1 = (short) (COORD_SCALE * y);
short u2 = (short) (COORD_SCALE * (x + width));
short v2 = (short) (COORD_SCALE * (y + height));
while (it != null) { while (it != null) {
short x1, x2, x3, x4, y1, y3, y2, y4;
if (it.text.caption) {
//if (it.origin == 0) {
x1 = x3 = (short) (COORD_SCALE * -hw);
x2 = x4 = (short) (COORD_SCALE * hw);
y1 = y2 = (short) (COORD_SCALE * (it.text.dy + hh));
y3 = y4 = (short) (COORD_SCALE * (it.text.dy - hh));
//} else {
// x1 = x3 = (short) (SCALE * 0);
// x2 = x4 = (short) (SCALE * width);
// y1 = y2 = (short) (SCALE * 0);
// y3 = y4 = (short) (SCALE * -height);
//}
} else {
float vx = it.x1 - it.x2;
float vy = it.y1 - it.y2;
float a = (float) Math.sqrt(vx * vx + vy * vy);
vx = vx / a;
vy = vy / a;
float ux = -vy * hh;
float uy = vx * hh;
float ux2 = -vy * hh2;
float uy2 = vx * hh2;
vx *= hw;
vy *= hw;
// top-left
x1 = (short) (COORD_SCALE * (vx - ux));
y1 = (short) (COORD_SCALE * (vy - uy));
// top-right
x2 = (short) (COORD_SCALE * (-vx - ux));
y2 = (short) (COORD_SCALE * (-vy - uy));
// bot-right
x4 = (short) (COORD_SCALE * (-vx + ux2));
y4 = (short) (COORD_SCALE * (-vy + uy2));
// bot-left
x3 = (short) (COORD_SCALE * (vx + ux2));
y3 = (short) (COORD_SCALE * (vy + uy2));
}
// add vertices
int tmp = (int) (COORD_SCALE * it.x) & LBIT_MASK;
short tx = (short) (tmp | (it.text.caption ? 1 : 0));
short ty = (short) (COORD_SCALE * it.y);
if (pos == VertexItem.SIZE) { if (pos == VertexItem.SIZE) {
vi.used = VertexItem.SIZE; vi.used = VertexItem.SIZE;
vi = VertexItem.pool.getNext(vi); vi = VertexItem.pool.getNext(vi);
buf = vi.vertices; buf = vi.vertices;
pos = 0; pos = 0;
} }
addItem(buf, pos, it, width, height, x, y);
// top-left pos += 24;
buf[pos++] = tx;
buf[pos++] = ty;
buf[pos++] = x1;
buf[pos++] = y1;
buf[pos++] = u1;
buf[pos++] = v2;
// bot-left
buf[pos++] = tx;
buf[pos++] = ty;
buf[pos++] = x3;
buf[pos++] = y3;
buf[pos++] = u1;
buf[pos++] = v1;
// top-right
buf[pos++] = tx;
buf[pos++] = ty;
buf[pos++] = x2;
buf[pos++] = y2;
buf[pos++] = u2;
buf[pos++] = v2;
// bot-right
buf[pos++] = tx;
buf[pos++] = ty;
buf[pos++] = x4;
buf[pos++] = y4;
buf[pos++] = u2;
buf[pos++] = v1;
// six indices to draw the four vertices // six indices to draw the four vertices
numIndices += TextureLayer.INDICES_PER_SPRITE; numIndices += TextureLayer.INDICES_PER_SPRITE;
numVertices += 4; numVertices += 4;
if (it.next == null || (it.next.text != it.text) if (it.next == null
|| (it.next.text != it.text)
|| (it.next.string != it.string)) { || (it.next.string != it.string)) {
it = it.next; it = it.next;
break; break;
@ -268,6 +172,86 @@ public final class TextLayer extends TextureLayer {
return true; return true;
} }
void addItem(short[] buf, int pos, TextItem it, float width, float height, float x, float y) {
// texture coordinates
short u1 = (short) (COORD_SCALE * x);
short v1 = (short) (COORD_SCALE * y);
short u2 = (short) (COORD_SCALE * (x + width));
short v2 = (short) (COORD_SCALE * (y + height));
short x1, x2, x3, x4, y1, y3, y2, y4;
float hw = width / 2.0f;
float hh = height / 2.0f;
if (it.text.caption) {
x1 = x3 = (short) (COORD_SCALE * -hw);
x2 = x4 = (short) (COORD_SCALE * hw);
y1 = y2 = (short) (COORD_SCALE * (it.text.dy + hh));
y3 = y4 = (short) (COORD_SCALE * (it.text.dy - hh));
} else {
float vx = it.x1 - it.x2;
float vy = it.y1 - it.y2;
float a = (float) Math.sqrt(vx * vx + vy * vy);
vx = vx / a;
vy = vy / a;
float ux = -vy * hh;
float uy = vx * hh;
float ux2 = -vy * hh;
float uy2 = vx * hh;
vx *= hw;
vy *= hw;
// top-left
x1 = (short) (COORD_SCALE * (vx - ux));
y1 = (short) (COORD_SCALE * (vy - uy));
// top-right
x2 = (short) (COORD_SCALE * (-vx - ux));
y2 = (short) (COORD_SCALE * (-vy - uy));
// bot-right
x4 = (short) (COORD_SCALE * (-vx + ux2));
y4 = (short) (COORD_SCALE * (-vy + uy2));
// bot-left
x3 = (short) (COORD_SCALE * (vx + ux2));
y3 = (short) (COORD_SCALE * (vy + uy2));
}
// add vertices
int tmp = (int) (COORD_SCALE * it.x) & LBIT_MASK;
short tx = (short) (tmp | (it.text.caption ? 1 : 0));
short ty = (short) (COORD_SCALE * it.y);
// top-left
buf[pos++] = tx;
buf[pos++] = ty;
buf[pos++] = x1;
buf[pos++] = y1;
buf[pos++] = u1;
buf[pos++] = v2;
// bot-left
buf[pos++] = tx;
buf[pos++] = ty;
buf[pos++] = x3;
buf[pos++] = y3;
buf[pos++] = u1;
buf[pos++] = v1;
// top-right
buf[pos++] = tx;
buf[pos++] = ty;
buf[pos++] = x2;
buf[pos++] = y2;
buf[pos++] = u2;
buf[pos++] = v2;
// bot-right
buf[pos++] = tx;
buf[pos++] = ty;
buf[pos++] = x4;
buf[pos++] = y4;
buf[pos++] = u2;
buf[pos++] = v1;
}
@Override @Override
public void clear() { public void clear() {
// release textures // release textures