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;
+ }
+
+ }
+
+}