package org.oscim.layers.tile.vector.labeling; import static org.oscim.layers.tile.MapTile.State.NEW_DATA; import static org.oscim.layers.tile.MapTile.State.READY; import org.oscim.core.MapPosition; import org.oscim.core.Tile; import org.oscim.layers.tile.MapTile; import org.oscim.layers.tile.TileRenderer; import org.oscim.layers.tile.TileSet; import org.oscim.map.Map; import org.oscim.renderer.bucket.SymbolBucket; import org.oscim.renderer.bucket.SymbolItem; import org.oscim.renderer.bucket.TextItem; import org.oscim.theme.styles.TextStyle; import org.oscim.utils.FastMath; import org.oscim.utils.geom.OBB2D; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class LabelPlacement { static final boolean dbg = false; static final Logger log = LoggerFactory.getLogger(LabelPlacement.class); public final static LabelTileData getLabels(MapTile tile) { return (LabelTileData) tile.getData(LabelLayer.LABEL_DATA); } 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) { l.next = mLabels; mLabels = l; } private byte checkOverlap(Label l) { for (Label o = mLabels; o != null;) { //check bounding box if (!Label.bboxOverlaps(l, o, 100)) { 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 = l.x - w; l.y1 = l.y - h; l.x2 = l.x + w; l.y2 = l.y + h; } private Label addWayLabels(MapTile t, Label l, float dx, float dy, double scale) { LabelTileData ld = getLabels(t); if (ld == null) return l; for (TextItem ti : ld.labels) { 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) { LabelTileData ld = getLabels(t); if (ld == null) return l; O: for (TextItem ti : ld.labels) { 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) { return false; } MapPosition pos = work.pos; boolean changedPos = mMap.viewport().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.bearing); float cos = (float) Math.cos(angle); float sin = (float) Math.sin(angle); int maxx = Tile.SIZE << (zoom - 1); // FIXME ??? SymbolBucket 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)); // plus 10 to rather keep label and avoid flickering if (l.width > (l.length + 10) * 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]; if (!t.state(READY | NEW_DATA)) 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]; if (!t.state(READY | NEW_DATA)) 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]; if (!t.state(READY | NEW_DATA)) continue; float dx = (float) (t.tileX * Tile.SIZE - tileX); float dy = (float) (t.tileY * Tile.SIZE - tileY); dx = flipLongitude(dx, maxx); LabelTileData ld = getLabels(t); if (ld == null) continue; for (SymbolItem ti : ld.symbols) { 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 = groupLabels(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(); } /** group labels by string and type */ protected Label groupLabels(Label labels) { for (Label cur = labels; cur != null; cur = (Label) cur.next) { /* keep pointer to previous for removal */ Label p = cur; TextStyle t = cur.text; float w = cur.width; /* iterate through following */ for (Label l = (Label) cur.next; l != null; l = (Label) l.next) { if (w != l.width || t != l.text || !cur.string.equals(l.string)) { p = l; continue; } else if (cur.next == l) { l.string = cur.string; p = l; continue; } l.string = cur.string; /* insert l after cur */ Label tmp = (Label) cur.next; cur.next = l; /* continue outer loop at l */ cur = l; /* remove l from previous place */ p.next = l.next; l.next = tmp; /* continue from previous */ l = p; } } return labels; } }