diff --git a/vtm/src/org/oscim/layers/tile/vector/labeling/Debug.java b/vtm/src/org/oscim/layers/tile/vector/labeling/Debug.java index 8c0932b0..b3fdf70c 100644 --- a/vtm/src/org/oscim/layers/tile/vector/labeling/Debug.java +++ b/vtm/src/org/oscim/layers/tile/vector/labeling/Debug.java @@ -17,6 +17,8 @@ package org.oscim.layers.tile.vector.labeling; 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.LineLayer; import org.oscim.renderer.elements.TextItem; @@ -25,8 +27,10 @@ import org.oscim.theme.styles.Line; class Debug { 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) { LineLayer ll; @@ -75,4 +79,19 @@ class Debug { 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); + // } + // } + // } + } + } diff --git a/vtm/src/org/oscim/layers/tile/vector/labeling/Label.java b/vtm/src/org/oscim/layers/tile/vector/labeling/Label.java index 56bfced5..d95a301b 100644 --- a/vtm/src/org/oscim/layers/tile/vector/labeling/Label.java +++ b/vtm/src/org/oscim/layers/tile/vector/labeling/Label.java @@ -17,10 +17,9 @@ package org.oscim.layers.tile.vector.labeling; import org.oscim.renderer.elements.TextItem; -import org.oscim.tiling.MapTile; import org.oscim.utils.OBB2D; -class Label extends TextItem { +final class Label extends TextItem { TextItem item; //Link blocking; @@ -28,34 +27,75 @@ class Label extends TextItem { // shared list of all label for a tile //Link siblings; - MapTile tile; + int tileX; + int tileY; + int tileZ; - //public byte origin; public int active; public OBB2D bbox; - public TextItem move(TextItem ti, float dx, float dy, float scale) { - this.x = (dx + ti.x) * scale; - this.y = (dy + ti.y) * scale; - return this; - } - - public void clone(TextItem ti) { + public Label clone(TextItem ti) { this.string = ti.string; this.text = ti.text; this.width = ti.width; this.length = ti.length; - } - - 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; + return this; } static int comparePriority(Label l1, Label l2) { 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); + } } diff --git a/vtm/src/org/oscim/layers/tile/vector/labeling/LabelLayer.java b/vtm/src/org/oscim/layers/tile/vector/labeling/LabelLayer.java index dd336e34..230113b6 100644 --- a/vtm/src/org/oscim/layers/tile/vector/labeling/LabelLayer.java +++ b/vtm/src/org/oscim/layers/tile/vector/labeling/LabelLayer.java @@ -21,32 +21,83 @@ import org.oscim.event.MotionEvent; import org.oscim.layers.Layer; import org.oscim.map.Map; import org.oscim.tiling.TileRenderer; +import org.oscim.utils.async.SimpleWorker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class LabelLayer extends Layer implements Map.InputListener, Map.UpdateListener { 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); + mLabelPlacer = new LabelPlacement(map, tileRenderer); + mWorker = new Worker(map); + mRenderer = new TextRenderer(mWorker); + } - //mTextLayer = new org.oscim.renderer.layers.TextRenderLayer(map, tileRenderLayer); - mTextRenderer = new TextRenderer(map, tileRenderLayer); - mRenderer = mTextRenderer; + class Worker extends SimpleWorker { + + 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 public void onDetach() { // TODO stop and clear labeling thread log.debug("DETACH"); - mTextRenderer.clearLabels(); + + // clear labels + mWorker.cancel(true); super.onDetach(); } + @Override + public void onMapUpdate(MapPosition mapPosition, boolean changed, boolean clear) { + if (clear) + mWorker.cancel(true); + + mWorker.submit(MAX_RELABEL_DELAY); + } + @Override public void onMotionEvent(MotionEvent e) { // 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; - // } - } diff --git a/vtm/src/org/oscim/layers/tile/vector/labeling/LabelPlacement.java b/vtm/src/org/oscim/layers/tile/vector/labeling/LabelPlacement.java new file mode 100644 index 00000000..240ee162 --- /dev/null +++ b/vtm/src/org/oscim/layers/tile/vector/labeling/LabelPlacement.java @@ -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(); + } +} diff --git a/vtm/src/org/oscim/layers/tile/vector/labeling/LabelPool.java b/vtm/src/org/oscim/layers/tile/vector/labeling/LabelPool.java new file mode 100644 index 00000000..b81d8505 --- /dev/null +++ b/vtm/src/org/oscim/layers/tile/vector/labeling/LabelPool.java @@ -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 { + 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(); + } +} diff --git a/vtm/src/org/oscim/layers/tile/vector/labeling/LabelTask.java b/vtm/src/org/oscim/layers/tile/vector/labeling/LabelTask.java new file mode 100644 index 00000000..19a5596f --- /dev/null +++ b/vtm/src/org/oscim/layers/tile/vector/labeling/LabelTask.java @@ -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; + } + +} diff --git a/vtm/src/org/oscim/layers/tile/vector/labeling/TextRenderer.java b/vtm/src/org/oscim/layers/tile/vector/labeling/TextRenderer.java index d15a0603..551b48a0 100644 --- a/vtm/src/org/oscim/layers/tile/vector/labeling/TextRenderer.java +++ b/vtm/src/org/oscim/layers/tile/vector/labeling/TextRenderer.java @@ -27,582 +27,27 @@ package org.oscim.layers.tile.vector.labeling; // 2.4 use 4 point labeling // 3 join segments that belong to one feature // 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.Tile; -import org.oscim.map.Map; -import org.oscim.map.Viewport; +import org.oscim.layers.tile.vector.labeling.LabelLayer.Worker; import org.oscim.renderer.ElementRenderer; import org.oscim.renderer.GLState; 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.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.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.LoggerFactory; class TextRenderer extends ElementRenderer { 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 { - 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 mActiveTiles; - - private float mSquareRadius; - private int mRelabelCnt; - private final TileRenderer mTileLayer; - private final Map mMap; private final Worker mWorker; - public TextRenderer(Map map, TileRenderer baseLayer) { - mMap = map; - mViewport = map.getViewport(); - mTileLayer = baseLayer; - mTileSet = new TileSet(); - - layers.textureLayers = new TextLayer(); - layers.textureLayers.next = new SymbolLayer(); - - //mActiveTiles = new HashMap(); - 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; + public TextRenderer(Worker worker) { + mWorker = worker; } long lastDraw = 0; @@ -613,12 +58,13 @@ class TextRenderer extends ElementRenderer { LabelTask t; synchronized (mWorker) { - t = mWorker.poll(); - - if (t == null) + if (t == null) { + if (!mWorker.isRunning()) { + mWorker.submit(50); + } return; - + } layers.clear(); } @@ -626,70 +72,21 @@ class TextRenderer extends ElementRenderer { layers.textureLayers = t.layers; mMapPosition = t.pos; compile(); - - update(); } @Override public synchronized void render(MapPosition pos, Matrices m) { GLState.test(false, false); + //Debug.draw(pos, layers); layers.vbo.bind(); 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); for (RenderElement l = layers.textureLayers; l != null;) l = TextureLayer.Renderer.draw(l, scale, m); } - final class Worker extends SimpleWorker { - - 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); - } } diff --git a/vtm/src/org/oscim/renderer/elements/TextItem.java b/vtm/src/org/oscim/renderer/elements/TextItem.java index c6e5a479..58cd4780 100644 --- a/vtm/src/org/oscim/renderer/elements/TextItem.java +++ b/vtm/src/org/oscim/renderer/elements/TextItem.java @@ -57,22 +57,6 @@ public class TextItem extends Inlist { 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) { this.x = x; this.y = y; @@ -86,35 +70,6 @@ public class TextItem extends Inlist { 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 //public TextItem next; diff --git a/vtm/src/org/oscim/renderer/elements/TextLayer.java b/vtm/src/org/oscim/renderer/elements/TextLayer.java index 13fbb4c6..aa20b99e 100644 --- a/vtm/src/org/oscim/renderer/elements/TextLayer.java +++ b/vtm/src/org/oscim/renderer/elements/TextLayer.java @@ -38,7 +38,6 @@ public final class TextLayer extends TextureLayer { public TextLayer() { super(RenderElement.SYMBOL); - //mCanvas = Graphics.res.getCanvas(); mCanvas = CanvasAdapter.g.getCanvas(); fixed = true; } @@ -58,7 +57,7 @@ public final class TextLayer extends TextureLayer { && !item.string.equals(it.string)) it = it.next; - // unify duplicate string :) + // unify duplicate string // Note: this is required for 'packing test' in prepare to work! if (item.string != it.string && item.string.equals(it.string)) item.string = it.string; @@ -139,117 +138,22 @@ public final class TextLayer extends TextureLayer { if (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) { - - 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) { vi.used = VertexItem.SIZE; vi = VertexItem.pool.getNext(vi); buf = vi.vertices; pos = 0; } - - // 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; + addItem(buf, pos, it, width, height, x, y); + pos += 24; // six indices to draw the four vertices numIndices += TextureLayer.INDICES_PER_SPRITE; 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 = it.next; break; @@ -268,6 +172,86 @@ public final class TextLayer extends TextureLayer { 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 public void clear() { // release textures