/* * Copyright 2012 Hannes Janetzek * * This program is free software: you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License as published by the Free Software * Foundation, either version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License along with * this program. If not, see . */ package org.oscim.renderer.overlays; // TODO // 1. rewrite :) // 2. compare previous to current state // 2.1 test for new labels to be placed // 2.2 handle collisions // 3 join segments that belong to one feature // 4 handle zoom-level changes // 5 3D-Tree might be handy // import java.util.HashMap; import org.oscim.core.MapPosition; import org.oscim.core.Tile; import org.oscim.generator.JobTile; import org.oscim.renderer.BufferObject; import org.oscim.renderer.GLRenderer; import org.oscim.renderer.GLState; import org.oscim.renderer.LineRenderer; import org.oscim.renderer.MapTile; import org.oscim.renderer.PolygonRenderer; import org.oscim.renderer.TextureRenderer; import org.oscim.renderer.TileSet; import org.oscim.renderer.layer.Layer; import org.oscim.renderer.layer.Layers; import org.oscim.renderer.layer.LineLayer; import org.oscim.renderer.layer.TextItem; import org.oscim.renderer.layer.TextLayer; import org.oscim.theme.renderinstruction.Line; import org.oscim.utils.FastMath; import org.oscim.utils.GlUtils; import org.oscim.utils.OBB2D; import org.oscim.utils.PausableThread; import org.oscim.view.MapView; import android.graphics.Color; import android.graphics.Paint.Cap; import android.opengl.GLES20; import android.opengl.Matrix; import android.os.SystemClock; import android.util.Log; public class TextOverlayExp extends BasicOverlay { private final static String TAG = TextOverlayExp.class.getName(); private TileSet mTileSet; private final LabelThread mThread; private MapPosition mTmpPos; // TextLayer that is updating private TextLayer mTmpLayer; // TextLayer that is ready to be added to 'layers' private TextLayer mNextLayer; /* package */boolean mRun; class LabelThread extends PausableThread { @Override protected void doWork() { SystemClock.sleep(300); if (!mRun) return; mRun = false; if (updateLabels()) { mMapView.redrawMap(true); } else { mRun = true; } } @Override protected String getThreadName() { return "Labeling"; } @Override protected boolean hasWork() { return mRun; } } public TextOverlayExp(MapView mapView) { super(mapView); layers.textureLayers = new TextLayer(); mTmpLayer = new TextLayer(); mTmpPos = new MapPosition(); mThread = new LabelThread(); mThread.start(); } private HashMap mItemMap; class PlacementItem extends TextItem { int tileX; int tileY; boolean isTileNeighbour(PlacementItem other) { int dx = other.tileX - tileX; if (dx > 1 || dx < -1) return false; int dy = other.tileY - tileY; if (dy > 1 || dy < -1) return false; return true; } } //private static void setOBB(TextItem ti){ // if (ti.bbox == null) // ti.bbox = new OBB2D(ti.x, ti.y, ti.x1, ti.y1, ti.width + 10, // ti.text.fontHeight + 10); // else // ti.bbox.set(ti.x, ti.y, ti.x1, ti.y1, ti.width + 10, // ti.text.fontHeight + 10); //} // local pool, avoids synchronized TextItem.get()/release() private TextItem mPool; private byte checkOverlap(TextLayer tl, TextItem ti) { for (TextItem lp = tl.labels; lp != null;) { if (lp.text.caption) { lp = lp.next; continue; } // check bounding box if (!TextItem.bboxOverlaps(ti, lp, 100)) { lp = lp.next; continue; } if (lp.text == ti.text && (lp.string == ti.string || lp.string.equals(ti.string))) { // make strings unique ti.string = lp.string; //p.active < ti.active || //Log.d(TAG, "overlap, same label in bbox " + lp.string // + " at " + ti.x + ":" + ti.y + ", " + lp.x + ":" //+ lp.y + " " + ti.length + "/" + lp.length); if (lp.length < ti.length) { //if (lp.length > ti.length) { //Log.d(TAG, "drop " + lp.string); TextItem tmp = lp; lp = lp.next; tl.removeText(tmp); tmp.next = mPool; mPool = tmp; continue; } return 3; } if (ti.bbox == null) { ti.bbox = new OBB2D(ti.x, ti.y, ti.x1, ti.y1, ti.width + 10, ti.text.fontHeight + 10); } if (lp.bbox == null) { lp.bbox = new OBB2D(lp.x, lp.y, lp.x1, lp.y1, lp.width + 10, lp.text.fontHeight + 10); } boolean intersect = ti.bbox.overlaps(lp.bbox); // byte intersect = GeometryUtils.linesIntersect( // ti.x1, ti.y1, ti.x2, ti.y2, // lp.x1, lp.y1, lp.x2, lp.y2); if (intersect) { //Log.d(TAG, "intersection " + lp.string + " <> " + ti.string // + " at " + ti.x + ":" + ti.y); if (lp.text.priority > ti.text.priority || lp.length < ti.length) { //if (lp.length > ti.length) { //Log.d(TAG, "drop " + lp.string); TextItem tmp = lp; lp = lp.next; tl.removeText(tmp); tmp.next = mPool; mPool = tmp; continue; } return 1; // if ((lp.n1 != null && lp.n1 == ti.n2) || // (lp.n2 != null && lp.n2 == ti.n1)) { // Log.d(TAG, "overlap with adjacent label " + lp.string // + " at " + ti.x + ":" + ti.y + ", " + lp.x + ":" + lp.y); // // return intersect; // } // // if ((ti.n1 != null || ti.n2 != null) && (lp.n1 == null && lp.n2 == null)) { // Log.d(TAG, "overlap, other is unique " + lp.string + " " + ti.string // + " at " + ti.x + ":" + ti.y + ", " + lp.x + ":" + lp.y); // return intersect; // } // // return intersect; } lp = lp.next; } return 0; } private Layers mDebugLayer; boolean updateLabels() { if (mTmpLayer == null) return false; mTileSet = GLRenderer.getVisibleTiles(mTileSet); if (mTileSet.cnt == 0) return false; TextLayer tl = mTmpLayer; mTmpLayer = null; Layers dbg = null;//new Layers(); // mTiles might be from another zoomlevel than the current: // this scales MapPosition to the zoomlevel of mTiles... // TODO create a helper function in MapPosition mMapView.getMapViewPosition().getMapPosition(mTmpPos, null); // capture current state MapTile[] tiles = mTileSet.tiles; int diff = tiles[0].zoomLevel - mTmpPos.zoomLevel; float div = FastMath.pow(diff); float scale = mTmpPos.scale * div; double angle = Math.toRadians(mTmpPos.angle); float cos = (float) Math.cos(angle); float sin = (float) Math.sin(angle); int maxx = Tile.TILE_SIZE << (mTmpPos.zoomLevel - 1); TextItem ti2 = null; if (dbg != null) { dbg.clear(); LineLayer ll = (LineLayer) dbg.getLayer(0, Layer.LINE); ll.line = new Line((Color.BLUE & 0xaaffffff), 1, Cap.BUTT); ll.width = 2; ll = (LineLayer) dbg.getLayer(3, Layer.LINE); ll.line = new Line((Color.YELLOW & 0xaaffffff), 1, Cap.BUTT); ll.width = 2; ll = (LineLayer) dbg.getLayer(1, Layer.LINE); ll.line = new Line((Color.RED & 0xaaffffff), 1, Cap.BUTT); ll.width = 2; ll = (LineLayer) dbg.getLayer(2, Layer.LINE); ll.line = new Line((Color.GREEN & 0xaaffffff), 1, Cap.BUTT); ll.width = 2; } for (int i = 0, n = mTileSet.cnt; i < n; i++) { MapTile t = tiles[i]; if (t.state == JobTile.STATE_NONE || t.state == JobTile.STATE_LOADING) continue; //if (t.joined != MapTile.JOINED) // joinTile() float dx = (float) (t.pixelX - mTmpPos.x); float dy = (float) (t.pixelY - mTmpPos.y); // flip around date-line if (dx > maxx) { dx = dx - maxx * 2; } else if (dx < -maxx) { dx = dx + maxx * 2; } dx *= scale; dy *= scale; for (TextItem ti = t.labels; ti != null; ti = ti.next) { // acquire a TextItem to add to TextLayer if (ti2 == null) { if (mPool == null) ti2 = TextItem.get(); else { ti2 = mPool; mPool = mPool.next; ti2.next = null; } } if (ti.text.caption) { ti2.move(ti, dx, dy, scale); ti2.setAxisAlignedBBox(); ti2.bbox = new OBB2D(ti2.x, ti2.y, cos, -sin, ti2.width + 6, ti2.text.fontHeight + 6, true); boolean overlaps = false; for (TextItem lp = tl.labels; lp != null; lp = lp.next) { if (!lp.text.caption) continue; if (ti2.bbox.overlaps(lp.bbox)) { Log.d(TAG, "overlap > " + ti2.string + " " + lp.string); //if (TextItem.bboxOverlaps(ti2, lp, 4)) { overlaps = true; break; } } if (!overlaps) { tl.addText(ti2); ti2 = null; } continue; } /* text is way label */ // check if path at current scale is long enough for text if (dbg == null && ti.width > ti.length * scale) continue; // set line endpoints relative to view to be able to // check intersections with label from other tiles float width = (ti.x2 - ti.x1) / 2f; float height = (ti.y2 - ti.y1) / 2f; ti2.bbox = null; ti2.move(ti, dx, dy, scale); ti2.x2 = (ti2.x + width); ti2.x1 = (ti2.x - width); ti2.y2 = (ti2.y + height); ti2.y1 = (ti2.y - height); byte overlaps = -1; if (dbg == null || ti.width < ti.length * scale) overlaps = checkOverlap(tl, ti2); if (dbg != null) { LineLayer ll; if (ti.width > ti.length * scale) { ll = (LineLayer) dbg.getLayer(1, Layer.LINE); overlaps = 3; } else if (overlaps == 1) ll = (LineLayer) dbg.getLayer(0, Layer.LINE); else if (overlaps == 2) ll = (LineLayer) dbg.getLayer(3, Layer.LINE); else ll = (LineLayer) dbg.getLayer(2, Layer.LINE); //if (ti2.bbox == null) // ti2.bbox = new OBB2D(ti2.x, ti2.y, ti2.x1, ti2.y1, ti.width + 10, // ti.text.fontHeight + 10); { float[] points = new float[4]; short[] indices = { 4 }; points[0] = (ti2.x - width * scale) / scale; points[1] = (ti2.y - height * scale) / scale; points[2] = (ti2.x + width * scale) / scale; points[3] = (ti2.y + height * scale) / scale; ll.addLine(points, indices, false); } if (ti2.bbox != null) { short[] indices = { 8 }; float[] points = new float[8]; for (int p = 0; p < 8; p++) points[p] = ti2.bbox.corner[p] / scale; ll.addLine(points, indices, true); } } if (overlaps == 0) { ti.active++; tl.addText(ti2); ti2.active = ti.active; ti2 = null; } } } for (TextItem ti = tl.labels; ti != null; ti = ti.next) { // scale back to fixed zoom-level. could be done in setMatrix ti.x /= scale; ti.y /= scale; if (ti.text.caption) 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; } } // release temporarily used TextItems if (ti2 != null) { ti2.next = mPool; mPool = ti2; } if (mPool != null) { TextItem.release(mPool); mPool = null; } // draw text to bitmaps and create vertices tl.setScale(scale); tl.prepare(); //TextItem.printPool(); //Log.d(TAG, "new labels: " + count); GLRenderer.releaseTiles(mTileSet); // pass new labels for rendering synchronized (this) { mNextLayer = tl; mDebugLayer = dbg; } return true; } @Override public synchronized void update(MapPosition curPos, boolean positionChanged, boolean tilesChanged) { if (mNextLayer != null) { // keep text layer, not recrating its canvas each time mTmpLayer = (TextLayer) layers.textureLayers; // clear textures and text items from previous layer layers.clear(); if (mDebugLayer != null) { layers.layers = mDebugLayer.layers; mDebugLayer = null; } // set new TextLayer to be uploaded and rendered layers.textureLayers = mNextLayer; // make the 'labeled' MapPosition current MapPosition tmp = mMapPosition; mMapPosition = mTmpPos; mTmpPos = tmp; newData = true; mNextLayer = null; if (!(positionChanged || tilesChanged)) return; } if (mHolding) return; if (!mRun) { mRun = true; synchronized (mThread) { mThread.notify(); } } } @Override public void compile() { int newSize = layers.getSize(); //if (newSize == 0) // Log.d(TAG, "text layer size " + newSize); if (newSize == 0) { //BufferObject.release(vbo); //vbo = null; isReady = false; return; } if (vbo == null) { vbo = BufferObject.get(0); } if (newSize > 0) { if (GLRenderer.uploadLayers(layers, vbo, newSize, true)) isReady = true; } } @Override public synchronized void render(MapPosition pos, float[] mv, float[] proj) { setMatrix(pos, mv); float div = FastMath.pow(mMapPosition.zoomLevel - pos.zoomLevel); Matrix.multiplyMM(mvp, 0, proj, 0, mv, 0); GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vbo.id); GLState.test(false, false); for (Layer l = layers.layers; l != null;) { if (l.type == Layer.POLYGON) { l = PolygonRenderer.draw(pos, l, mvp, true, false); } else { l = LineRenderer.draw(pos, l, mvp, div, 0, layers.lineOffset); } } for (Layer l = layers.textureLayers; l != null;) { l = TextureRenderer.draw(l, (mMapPosition.scale / pos.scale) * div, proj, mv); } } @Override protected void setMatrix(MapPosition curPos, float[] matrix) { MapPosition oPos = mMapPosition; float div = FastMath.pow(oPos.zoomLevel - curPos.zoomLevel); float x = (float) (oPos.x - curPos.x * div); float y = (float) (oPos.y - curPos.y * div); float scale = curPos.scale / div; GlUtils.setMatrix(matrix, x * scale, y * scale, scale / GLRenderer.COORD_MULTIPLIER); Matrix.multiplyMM(matrix, 0, curPos.viewMatrix, 0, matrix, 0); } private boolean mHolding; /** * @param enable layer updates */ public synchronized void hold(boolean enable) { // mHolding = enable; // if (!enable && !mRun) { // mRun = true; // synchronized (mThread) { // mThread.notify(); // } // } // } else { // mRun = false; // } } }