From 0f5af24361fc9ea709239c08b7411d79a7f44690 Mon Sep 17 00:00:00 2001 From: Longri Date: Fri, 1 Jun 2018 09:43:39 +0200 Subject: [PATCH] Location texture renderer (#548) - add LocationTextureLayer with accuracy - add Android sample for LocationTextureLayer --- vtm-android-example/AndroidManifest.xml | 3 + vtm-android-example/res/raw/arrow.svg | 19 + .../android/test/LocationTextureActivity.java | 118 ++++++ .../src/org/oscim/android/test/Samples.java | 3 +- .../oscim/layers/LocationTextureLayer.java | 362 ++++++++++++++++++ 5 files changed, 504 insertions(+), 1 deletion(-) create mode 100644 vtm-android-example/res/raw/arrow.svg create mode 100644 vtm-android-example/src/org/oscim/android/test/LocationTextureActivity.java create mode 100644 vtm/src/org/oscim/layers/LocationTextureLayer.java diff --git a/vtm-android-example/AndroidManifest.xml b/vtm-android-example/AndroidManifest.xml index 4d7ed091..cb75dbad 100644 --- a/vtm-android-example/AndroidManifest.xml +++ b/vtm-android-example/AndroidManifest.xml @@ -57,6 +57,9 @@ + diff --git a/vtm-android-example/res/raw/arrow.svg b/vtm-android-example/res/raw/arrow.svg new file mode 100644 index 00000000..2e9e3292 --- /dev/null +++ b/vtm-android-example/res/raw/arrow.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/vtm-android-example/src/org/oscim/android/test/LocationTextureActivity.java b/vtm-android-example/src/org/oscim/android/test/LocationTextureActivity.java new file mode 100644 index 00000000..64784cc4 --- /dev/null +++ b/vtm-android-example/src/org/oscim/android/test/LocationTextureActivity.java @@ -0,0 +1,118 @@ +/* + * Copyright 2016-2018 devemux86 + * Copyright 2018 Longri + * + * 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.android.test; + +import android.content.Context; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.os.Bundle; + +import org.oscim.backend.CanvasAdapter; +import org.oscim.backend.canvas.Bitmap; +import org.oscim.backend.canvas.Color; +import org.oscim.core.MapPosition; +import org.oscim.layers.LocationTextureLayer; +import org.oscim.renderer.atlas.TextureAtlas; +import org.oscim.renderer.atlas.TextureRegion; +import org.oscim.renderer.bucket.TextureItem; + +import java.io.IOException; +import java.io.InputStream; + +public class LocationTextureActivity extends BitmapTileActivity implements LocationListener { + private LocationTextureLayer locationLayer; + private LocationManager locationManager; + private final MapPosition mapPosition = new MapPosition(); + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + //initial Android locationManager + locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE); + + //load a Bitmap/Svg from resources. (North/0° are on top + InputStream is = getResources().openRawResource(R.raw.arrow); + Bitmap bmp = null; + try { + bmp = CanvasAdapter.decodeSvgBitmap(is, 200, 200, 100); + } catch (IOException e) { + e.printStackTrace(); + } + //create TextureRegion from Bitmap + TextureRegion textureRegion = new TextureRegion(new TextureItem(bmp), new TextureAtlas.Rect(0, 0, bmp.getWidth(), bmp.getHeight())); + + //create LocationTexturLayer with created/loaded TextureRegion + locationLayer = new LocationTextureLayer(mMap, textureRegion); + + // you can set the Color of Accuracy circle (Color.BLUE is default) + locationLayer.setAccuracyColor(Color.get(50, 50, 255)); + + // you can set the Color of Indicator circle (Color.RED is default) + locationLayer.setIndicatorColor(Color.MAGENTA); + + locationLayer.setEnabled(false); + mMap.layers().add(locationLayer); + } + + @Override + protected void onResume() { + super.onResume(); + enableAvailableProviders(); + } + + @Override + protected void onPause() { + super.onPause(); + + locationManager.removeUpdates(this); + } + + @Override + public void onLocationChanged(Location location) { + locationLayer.setEnabled(true); + locationLayer.setPosition(location.getLatitude(), location.getLongitude(), location.getBearing(), location.getAccuracy()); + + // Follow location + mMap.getMapPosition(mapPosition); + mapPosition.setPosition(location.getLatitude(), location.getLongitude()); + mMap.setMapPosition(mapPosition); + } + + @Override + public void onProviderDisabled(String provider) { + } + + @Override + public void onProviderEnabled(String provider) { + } + + @Override + public void onStatusChanged(String provider, int status, Bundle extras) { + } + + private void enableAvailableProviders() { + locationManager.removeUpdates(this); + + for (String provider : locationManager.getProviders(true)) { + if (LocationManager.GPS_PROVIDER.equals(provider) + || LocationManager.NETWORK_PROVIDER.equals(provider)) { + locationManager.requestLocationUpdates(provider, 0, 0, this); + } + } + } +} diff --git a/vtm-android-example/src/org/oscim/android/test/Samples.java b/vtm-android-example/src/org/oscim/android/test/Samples.java index 69c5a05d..abdbe55a 100644 --- a/vtm-android-example/src/org/oscim/android/test/Samples.java +++ b/vtm-android-example/src/org/oscim/android/test/Samples.java @@ -2,7 +2,7 @@ * Copyright 2010, 2011, 2012, 2013 mapsforge.org * Copyright 2013 Hannes Janetzek * Copyright 2016-2018 devemux86 - * Copyright 2017 Longri + * Copyright 2017-2018 Longri * Copyright 2017 nebular * * This file is part of the OpenScienceMap project (http://www.opensciencemap.org). @@ -98,6 +98,7 @@ public class Samples extends Activity { } })); linearLayout.addView(createButton(LocationActivity.class)); + linearLayout.addView(createButton(LocationTextureActivity.class)); linearLayout.addView(createButton(PoiSearchActivity.class)); linearLayout.addView(createLabel("Vector Features")); diff --git a/vtm/src/org/oscim/layers/LocationTextureLayer.java b/vtm/src/org/oscim/layers/LocationTextureLayer.java new file mode 100644 index 00000000..d8922157 --- /dev/null +++ b/vtm/src/org/oscim/layers/LocationTextureLayer.java @@ -0,0 +1,362 @@ +/* + * Copyright 2017-2018 Longri + * + * 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.layers; + +import org.oscim.backend.CanvasAdapter; +import org.oscim.backend.GL; +import org.oscim.backend.canvas.Color; +import org.oscim.core.Box; +import org.oscim.core.MercatorProjection; +import org.oscim.core.Point; +import org.oscim.core.PointF; +import org.oscim.core.Tile; +import org.oscim.map.Map; +import org.oscim.renderer.BucketRenderer; +import org.oscim.renderer.GLShader; +import org.oscim.renderer.GLState; +import org.oscim.renderer.GLUtils; +import org.oscim.renderer.GLViewport; +import org.oscim.renderer.MapRenderer; +import org.oscim.renderer.atlas.TextureRegion; +import org.oscim.renderer.bucket.SymbolBucket; +import org.oscim.renderer.bucket.SymbolItem; +import org.oscim.utils.FastMath; +import org.oscim.utils.geom.GeometryUtils; +import org.oscim.utils.math.Interpolation; + +import java.util.Locale; + +import static org.oscim.backend.GLAdapter.gl; + +public class LocationTextureLayer extends Layer { + + private static final PointF CENTER_OFFSET = new PointF(0.5f, 0.5f); + private static final long ANIM_RATE = 50; + private static final long INTERVAL = 2000; + private static final float CIRCLE_SIZE = 30; + private static final int SHOW_ACCURACY_ZOOM = 13; + private static final boolean IS_MAC = System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("mac"); + + private final static String V_SHADER = ("" + + "precision highp float;" + + "uniform mat4 u_mvp;" + + "uniform float u_phase;" + + "uniform float u_scale;" + + "attribute vec2 a_pos;" + + "varying vec2 v_tex;" + + "void main() {" + + " gl_Position = u_mvp * vec4(a_pos * u_scale * u_phase, 0.0, 1.0);" + + " v_tex = a_pos;" + + "}").replace("precision highp float;", IS_MAC ? "" : "precision highp float;"); + + // only circle without direction + private static final String F_SHADER = ("" + + "precision highp float;" + + "varying vec2 v_tex;" + + "uniform float u_scale;" + + "uniform float u_phase;" + + "uniform vec4 u_fill;" + + "void main() {" + + " float len = 1.0 - length(v_tex);" + /// outer ring + + " float a = smoothstep(0.0, 2.0 / u_scale, len);" + /// inner ring + + " float b = 0.8 * smoothstep(3.0 / u_scale, 4.0 / u_scale, len);" + /// center point + + " float c = 0.5 * (1.0 - smoothstep(14.0 / u_scale, 16.0 / u_scale, 1.0 - len));" + + " vec2 dir = normalize(v_tex);" + /// - subtract inner from outer to create the outline + /// - multiply by viewshed + /// - add center point + + " a = (a - (b + c)) + c;" + + " gl_FragColor = u_fill * a;" + + "}").replace("precision highp float;", IS_MAC ? "" : "precision highp float;"); + + + private final LocationTextureRenderer locationRenderer; + + public LocationTextureLayer(Map map, TextureRegion textureRegion) { + super(map); + mRenderer = locationRenderer = new LocationTextureRenderer(map); + setTextureRegion(textureRegion); + } + + + public void setPosition(double latitude, double longitude, float heading, double accuracy) { + locationRenderer.setPosition(latitude, longitude, heading, accuracy); + } + + public void setAccuracyColor(int color) { + locationRenderer.setAccuracyColor(color); + } + + public void setIndicatorColor(int color) { + locationRenderer.setIndicatorColor(color); + } + + public void setTextureRegion(TextureRegion region) { + locationRenderer.setTextureRegion(region); + } + + private static final class LocationTextureRenderer extends BucketRenderer { + private final SymbolBucket symbolBucket; + private final float[] box = new float[8]; + private final Point mapPoint = new Point(); + private final Map map; + private boolean initialized; + private boolean locationIsVisible; + private int shaderProgramNumber; + private int hVertexPosition; + private int hMatrixPosition; + private int hScale; + private int hPhase; + private int uFill; + private double accuracyRadius; + + private final Point indicatorPosition = new Point(); + private final Point screenPoint = new Point(); + private final Box boundingBox = new Box(); + private boolean runAnim; + private long animStart; + private boolean update; + private float heading; + + //properties + private TextureRegion textureRegion; + private int accuracyColor = Color.BLUE; + private int viewShedColor = Color.RED; + + private LocationTextureRenderer(Map map) { + symbolBucket = new SymbolBucket(); + this.map = map; + } + + public void setPosition(double latitude, double longitude, float heading, double accuracy) { + update = true; + this.heading = -heading; + while (this.heading < 0) this.heading += 360; + mapPoint.x = MercatorProjection.longitudeToX(longitude); + mapPoint.y = MercatorProjection.latitudeToY(latitude); + accuracyRadius = accuracy; + } + + private void setAccuracyColor(int color) { + this.accuracyColor = color; + } + + private void setIndicatorColor(int color) { + this.viewShedColor = color; + } + + private void setTextureRegion(TextureRegion region) { + textureRegion = region; + } + + @Override + public synchronized void update(GLViewport v) { + if (!v.changed() && !update) return; + + + //accuracy + if (!initialized) { + init(); + initialized = true; + } + setReady(true); + + int width = map.getWidth(); + int height = map.getHeight(); + v.getBBox(boundingBox, 0); + + double x = mapPoint.x; + double y = mapPoint.y; + + if (!boundingBox.contains(mapPoint)) { + x = FastMath.clamp(x, boundingBox.xmin, boundingBox.xmax); + y = FastMath.clamp(y, boundingBox.ymin, boundingBox.ymax); + } + + // get position of Location in pixel relative to + // screen center + v.toScreenPoint(x, y, screenPoint); + + x = screenPoint.x + width / 2; + y = screenPoint.y + height / 2; + + // clip position to screen boundaries + int visible = 0; + + if (x > width - 5) + x = width; + else if (x < 5) + x = 0; + else + visible++; + + if (y > height - 5) + y = height; + else if (y < 5) + y = 0; + else + visible++; + + locationIsVisible = (visible == 2); + + if (locationIsVisible) { + animate(false); + } else { + animate(true); + } + // set location indicator position + v.fromScreenPoint(x, y, indicatorPosition); + + + //Texture + mMapPosition.copy(v.pos); + + double mx = v.pos.x; + double my = v.pos.y; + double scale = Tile.SIZE * v.pos.scale; + map.viewport().getMapExtents(box, 100); + long flip = (long) (Tile.SIZE * v.pos.scale) >> 1; + + /* check visibility */ + float symbolX = (float) ((mapPoint.x - mx) * scale); + float symbolY = (float) ((mapPoint.y - my) * scale); + + if (symbolX > flip) + symbolX -= (flip << 1); + else if (symbolX < -flip) + symbolX += (flip << 1); + buckets.clear(); + if (!GeometryUtils.pointInPoly(symbolX, symbolY, box, 8, 0)) { + return; + } + + mMapPosition.bearing = -mMapPosition.bearing; + if (textureRegion == null) return; + SymbolItem symbolItem = SymbolItem.pool.get(); + symbolItem.set(symbolX, symbolY, textureRegion, this.heading, true); + symbolItem.offset = CENTER_OFFSET; + symbolBucket.pushSymbol(symbolItem); + + buckets.set(symbolBucket); + buckets.prepare(); + buckets.compile(true); + compile(); + update = false; + } + + @Override + public void render(GLViewport v) { + renderAccuracyCircle(v); + super.render(v); + } + + private void init() { + int shader = GLShader.createProgram(V_SHADER, F_SHADER); + if (shader == 0) + return; + + shaderProgramNumber = shader; + hVertexPosition = gl.getAttribLocation(shader, "a_pos"); + hMatrixPosition = gl.getUniformLocation(shader, "u_mvp"); + hPhase = gl.getUniformLocation(shader, "u_phase"); + hScale = gl.getUniformLocation(shader, "u_scale"); + uFill = gl.getUniformLocation(shader, "u_fill"); + } + + private void renderAccuracyCircle(GLViewport v) { + GLState.useProgram(shaderProgramNumber); + GLState.blend(true); + GLState.test(false, false); + + GLState.enableVertexArrays(hVertexPosition, -1); + MapRenderer.bindQuadVertexVBO(hVertexPosition/*, true*/); + + float radius = 10; + boolean viewShed = false; + if (!locationIsVisible) { + radius = CIRCLE_SIZE * CanvasAdapter.getScale(); + } else { + if (v.pos.zoomLevel >= SHOW_ACCURACY_ZOOM) { + radius = (float) (accuracyRadius / MercatorProjection.groundResolution(v.pos)); + } + radius = Math.max(2, radius); + viewShed = true; + } + gl.uniform1f(hScale, radius); + + double x = indicatorPosition.x - v.pos.x; + double y = indicatorPosition.y - v.pos.y; + double tileScale = Tile.SIZE * v.pos.scale; + + v.mvp.setTransScale((float) (x * tileScale), (float) (y * tileScale), 1); + v.mvp.multiplyMM(v.viewproj, v.mvp); + v.mvp.setAsUniform(hMatrixPosition); + + if (!viewShed) { + float phase = Math.abs(animPhase() - 0.5f) * 2; + //phase = Interpolation.fade.apply(phase); + phase = Interpolation.swing.apply(phase); + gl.uniform1f(hPhase, 0.8f + phase * 0.2f); + } else { + gl.uniform1f(hPhase, 1); + } + + if (viewShed && locationIsVisible) { + GLUtils.setColor(uFill, accuracyColor, 1); + } else { + GLUtils.setColor(uFill, viewShedColor, 1); + } + + gl.drawArrays(GL.TRIANGLE_STRIP, 0, 4); + gl.flush(); + } + + private void animate(boolean enable) { + if (runAnim == enable) + return; + + runAnim = enable; + if (!enable) + return; + + final Runnable action = new Runnable() { + private long lastRun; + + @Override + public void run() { + if (!runAnim) + return; + + long diff = System.currentTimeMillis() - lastRun; + map.postDelayed(this, Math.min(ANIM_RATE, diff)); + map.render(); + lastRun = System.currentTimeMillis(); + } + }; + + animStart = System.currentTimeMillis(); + map.postDelayed(action, ANIM_RATE); + } + + private float animPhase() { + return (float) ((MapRenderer.frametime - animStart) % INTERVAL) / INTERVAL; + } + + } + +}