From 437db65e6756d09601be36142012fe3d3b42e131 Mon Sep 17 00:00:00 2001 From: AdityaChondke Date: Sun, 18 Aug 2019 09:31:25 +0530 Subject: [PATCH 01/23] Augmented Reality Feature Added to MapMint4ME --- .idea/caches/build_file_checksums.ser | Bin 0 -> 535 bytes .idea/caches/gradle_models.ser | Bin 0 -> 272965 bytes .idea/misc.xml | 2 +- app/build.gradle | 32 +- app/src/main/AndroidManifest.xml | 16 +- app/src/main/assets/cube.obj | 40 + app/src/main/assets/cube_cyan.png | Bin 0 -> 17331 bytes app/src/main/assets/cube_green.png | Bin 0 -> 17261 bytes app/src/main/assets/trigrid.png | Bin 0 -> 37354 bytes .../java/helloar/CameraPermissionHelper.java | 52 + .../java/helloar/DisplayRotationHelper.java | 91 ++ .../helloar/rendering/BackgroundRenderer.java | 178 ++++ .../helloar/rendering/ObjectRenderer.java | 373 +++++++ .../java/helloar/rendering/PlaneRenderer.java | 429 ++++++++ .../helloar/rendering/PointCloudRenderer.java | 144 +++ .../java/helloar/rendering/ShaderUtil.java | 89 ++ .../arcoremeasure/ArMeasureActivity.java | 967 ++++++++++++++++++ .../com/hl3hl3/arcoremeasure/OverlayView.java | 53 + .../arcoremeasure/renderer/LineRenderer.java | 161 +++ .../renderer/RectanglePolygonRenderer.java | 206 ++++ .../renderer/SquareRenderer.java | 175 ++++ app/src/main/java/drawar/AppSettings.java | 47 + app/src/main/java/drawar/BiquadFilter.java | 72 ++ .../java/drawar/DisplayRotationHelper.java | 106 ++ app/src/main/java/drawar/DrawAR.java | 739 +++++++++++++ .../main/java/drawar/PermissionHelper.java | 46 + app/src/main/java/drawar/package-info.java | 17 + .../drawar/rendering/BackgroundRenderer.java | 184 ++++ .../drawar/rendering/LineShaderRenderer.java | 537 ++++++++++ .../main/java/drawar/rendering/LineUtils.java | 130 +++ app/src/main/java/drawar/rendering/Ray.java | 27 + .../java/drawar/rendering/ShaderUtil.java | 97 ++ .../java/drawar/rendering/package-info.java | 19 + .../fr/geolabs/dev/mapmint4me/MapMint4ME.java | 25 + app/src/main/res/drawable/bg_btn.xml | 10 + app/src/main/res/drawable/circle.xml | 6 + app/src/main/res/drawable/draw.png | Bin 0 -> 66984 bytes app/src/main/res/drawable/ic_check_empty.png | Bin 0 -> 286 bytes .../main/res/drawable/ic_check_pressed.png | Bin 0 -> 14925 bytes app/src/main/res/drawable/ic_checked.png | Bin 0 -> 15654 bytes app/src/main/res/drawable/ic_cube.xml | 9 + app/src/main/res/drawable/ic_cube_disable.png | Bin 0 -> 1255 bytes app/src/main/res/drawable/ic_cube_green.png | Bin 0 -> 6922 bytes app/src/main/res/drawable/ic_cube_pink.png | Bin 0 -> 7852 bytes .../res/drawable/ic_delete_black_24dp.xml | 9 + app/src/main/res/drawable/ic_launcher.xml | 5 + .../drawable/ic_my_location_black_24dp.xml | 9 + .../res/drawable/ic_settings_black_24dp.xml | 9 + .../main/res/drawable/ic_undo_black_24dp.xml | 9 + app/src/main/res/drawable/scale.jpg | Bin 0 -> 37749 bytes app/src/main/res/drawable/selector_cb.xml | 9 + app/src/main/res/layout/act_layout.xml | 140 +++ app/src/main/res/layout/activity_main.xml | 170 +++ .../main/res/layout/activity_map_mint4_me.xml | 25 + app/src/main/res/raw/light_vertex.shader | 41 + app/src/main/res/raw/line_frag.glsl | 38 + app/src/main/res/raw/line_vert.glsl | 93 ++ app/src/main/res/raw/object_fragment.shader | 66 ++ app/src/main/res/raw/object_vertex.shader | 32 + .../main/res/raw/passthrough_fragment.shader | 21 + app/src/main/res/raw/plane_fragment.shader | 31 + app/src/main/res/raw/plane_vertex.shader | 28 + .../main/res/raw/point_cloud_vertex.shader | 28 + .../main/res/raw/screenquad_fragment_oes.glsl | 24 + app/src/main/res/raw/screenquad_vertex.glsl | 24 + app/src/main/res/values/colors.xml | 10 +- app/src/main/res/values/dimens.xml | 5 + app/src/main/res/values/strings.xml | 17 + app/src/main/res/values/styles.xml | 28 + build.gradle | 1 + 70 files changed, 5942 insertions(+), 9 deletions(-) create mode 100644 .idea/caches/build_file_checksums.ser create mode 100644 .idea/caches/gradle_models.ser create mode 100644 app/src/main/assets/cube.obj create mode 100644 app/src/main/assets/cube_cyan.png create mode 100644 app/src/main/assets/cube_green.png create mode 100755 app/src/main/assets/trigrid.png create mode 100755 app/src/main/java/com/google/ar/core/examples/java/helloar/CameraPermissionHelper.java create mode 100644 app/src/main/java/com/google/ar/core/examples/java/helloar/DisplayRotationHelper.java create mode 100755 app/src/main/java/com/google/ar/core/examples/java/helloar/rendering/BackgroundRenderer.java create mode 100755 app/src/main/java/com/google/ar/core/examples/java/helloar/rendering/ObjectRenderer.java create mode 100644 app/src/main/java/com/google/ar/core/examples/java/helloar/rendering/PlaneRenderer.java create mode 100644 app/src/main/java/com/google/ar/core/examples/java/helloar/rendering/PointCloudRenderer.java create mode 100755 app/src/main/java/com/google/ar/core/examples/java/helloar/rendering/ShaderUtil.java create mode 100644 app/src/main/java/com/hl3hl3/arcoremeasure/ArMeasureActivity.java create mode 100644 app/src/main/java/com/hl3hl3/arcoremeasure/OverlayView.java create mode 100644 app/src/main/java/com/hl3hl3/arcoremeasure/renderer/LineRenderer.java create mode 100644 app/src/main/java/com/hl3hl3/arcoremeasure/renderer/RectanglePolygonRenderer.java create mode 100644 app/src/main/java/com/hl3hl3/arcoremeasure/renderer/SquareRenderer.java create mode 100644 app/src/main/java/drawar/AppSettings.java create mode 100644 app/src/main/java/drawar/BiquadFilter.java create mode 100755 app/src/main/java/drawar/DisplayRotationHelper.java create mode 100755 app/src/main/java/drawar/DrawAR.java create mode 100755 app/src/main/java/drawar/PermissionHelper.java create mode 100755 app/src/main/java/drawar/package-info.java create mode 100755 app/src/main/java/drawar/rendering/BackgroundRenderer.java create mode 100644 app/src/main/java/drawar/rendering/LineShaderRenderer.java create mode 100644 app/src/main/java/drawar/rendering/LineUtils.java create mode 100644 app/src/main/java/drawar/rendering/Ray.java create mode 100755 app/src/main/java/drawar/rendering/ShaderUtil.java create mode 100755 app/src/main/java/drawar/rendering/package-info.java create mode 100644 app/src/main/res/drawable/bg_btn.xml create mode 100644 app/src/main/res/drawable/circle.xml create mode 100755 app/src/main/res/drawable/draw.png create mode 100644 app/src/main/res/drawable/ic_check_empty.png create mode 100644 app/src/main/res/drawable/ic_check_pressed.png create mode 100644 app/src/main/res/drawable/ic_checked.png create mode 100644 app/src/main/res/drawable/ic_cube.xml create mode 100644 app/src/main/res/drawable/ic_cube_disable.png create mode 100644 app/src/main/res/drawable/ic_cube_green.png create mode 100644 app/src/main/res/drawable/ic_cube_pink.png create mode 100644 app/src/main/res/drawable/ic_delete_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_launcher.xml create mode 100644 app/src/main/res/drawable/ic_my_location_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_settings_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_undo_black_24dp.xml create mode 100644 app/src/main/res/drawable/scale.jpg create mode 100644 app/src/main/res/drawable/selector_cb.xml create mode 100644 app/src/main/res/layout/act_layout.xml create mode 100755 app/src/main/res/layout/activity_main.xml create mode 100755 app/src/main/res/raw/light_vertex.shader create mode 100755 app/src/main/res/raw/line_frag.glsl create mode 100755 app/src/main/res/raw/line_vert.glsl create mode 100755 app/src/main/res/raw/object_fragment.shader create mode 100755 app/src/main/res/raw/object_vertex.shader create mode 100755 app/src/main/res/raw/passthrough_fragment.shader create mode 100755 app/src/main/res/raw/plane_fragment.shader create mode 100755 app/src/main/res/raw/plane_vertex.shader create mode 100755 app/src/main/res/raw/point_cloud_vertex.shader create mode 100755 app/src/main/res/raw/screenquad_fragment_oes.glsl create mode 100755 app/src/main/res/raw/screenquad_vertex.glsl diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser new file mode 100644 index 0000000000000000000000000000000000000000..e1178f6a7909035eba94ab4f4acb4ac418a1411e GIT binary patch literal 535 zcmZ4UmVvdnh`~NNKUXg?FQq6yGexf?KR>5fFEb@IQ7^qHF(oHeub?PDD>b=9F91S2 zm1gFoxMk*~I%lLNXBU^|7Q2L-Ts|(GuF1r}uGBYr_F>vMNC#JY1CYR(Fc`|U8WE7YFbLvN{p#PjoPz-~6wh&fS)DRbIT+}=#DW5S>|SwQpWnES^9AE3mgML8Y#${m E0MJOei2wiq literal 0 HcmV?d00001 diff --git a/.idea/caches/gradle_models.ser b/.idea/caches/gradle_models.ser new file mode 100644 index 0000000000000000000000000000000000000000..93b1d98947f01f38f5983017556feea5b3df0ca0 GIT binary patch literal 272965 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 literal 0 HcmV?d00001 diff --git a/.idea/misc.xml b/.idea/misc.xml index 26a9819..197c30e 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -35,7 +35,7 @@ - + diff --git a/app/build.gradle b/app/build.gradle index 7fdc74c..733d15b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -5,9 +5,9 @@ android { buildToolsVersion '28.0.3' defaultConfig { applicationId "fr.geolabs.dev.mapmint4me" - minSdkVersion 16 + minSdkVersion 24 targetSdkVersion 27 - versionCode 23 + versionCode 22 versionName "1.0" multiDexEnabled true } @@ -25,16 +25,36 @@ android { // but continue the build even when errors are found: abortOnError false } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } } dependencies { //implementation fileTree(include: ['*.jar'], dir: 'libs') - implementation 'com.android.support:appcompat-v7:26.1.0' - implementation 'com.android.support:design:26.1.0' - implementation 'com.android.support:support-compat:26.1.0' + //noinspection GradleCompatible + implementation 'com.android.support:appcompat-v7:27.1.0' + implementation 'com.android.support:design:27.1.0' + implementation 'com.android.support:support-compat:27.1.0' api 'com.google.android.gms:play-services:12.0.1' - api 'com.google.android.gms:play-services-gcm:9.0.2' + api 'com.google.android.gms:play-services-gcm:10.2.1' api 'com.google.android.gms:play-services-location:12.0.1' api 'com.readystatesoftware.sqliteasset:sqliteassethelper:+' //implementation 'com.android.support:leanback-v17:26.1.0' + + implementation 'com.google.ar:core:1.8.0' + implementation 'com.google.ar.sceneform.ux:sceneform-ux:1.8.0' + implementation 'com.google.ar.sceneform:core:1.8.0' + + implementation 'com.google.ar:core:1.0.0' + implementation 'javax.vecmath:vecmath:1.5.2' + implementation 'com.android.support:appcompat-v7:27.1.0' + implementation 'com.android.support:design:27.1.0' + + implementation 'de.javagl:obj:0.2.1' + implementation ('com.crashlytics.sdk.android:crashlytics:2.6.8@aar') { + transitive = true; + } + } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ade7eb8..a7bb893 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,9 +1,12 @@ + + @@ -12,7 +15,8 @@ - + @@ -32,6 +36,8 @@ android:protectionLevel="signature" /> + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/assets/cube.obj b/app/src/main/assets/cube.obj new file mode 100644 index 0000000..198b6a0 --- /dev/null +++ b/app/src/main/assets/cube.obj @@ -0,0 +1,40 @@ +# Blender v2.78 (sub 0) OBJ File: 'cube.blend' +# www.blender.org +mtllib cube.mtl +o Cube +v 0.010000 -0.010000 -0.010000 +v 0.010000 -0.010000 0.010000 +v -0.010000 -0.010000 0.010000 +v -0.010000 -0.010000 -0.010000 +v 0.010000 0.010000 -0.010000 +v 0.010000 0.010000 0.010000 +v -0.010000 0.010000 0.010000 +v -0.010000 0.010000 -0.010000 +vt 0.3756 0.5006 +vt 0.6244 0.5006 +vt 0.6244 0.7494 +vt 0.3756 0.7494 +vt 0.3756 0.2519 +vt 0.3756 0.0031 +vt 0.6244 0.0031 +vt 0.6244 0.2519 +vt 0.8731 0.7494 +vt 0.8731 0.9981 +vt 0.6244 0.9981 +vt 0.3756 0.9981 +vt 0.1269 0.9981 +vt 0.1269 0.7494 +vn 0.0000 -1.0000 0.0000 +vn 0.0000 1.0000 0.0000 +vn 1.0000 0.0000 0.0000 +vn -0.0000 -0.0000 1.0000 +vn -1.0000 -0.0000 -0.0000 +vn 0.0000 0.0000 -1.0000 +usemtl Material +s off +f 1/1/1 2/2/1 3/3/1 4/4/1 +f 5/5/2 8/6/2 7/7/2 6/8/2 +f 1/1/3 5/5/3 6/8/3 2/2/3 +f 2/9/4 6/10/4 7/11/4 3/3/4 +f 3/3/5 7/11/5 8/12/5 4/4/5 +f 5/13/6 1/14/6 4/4/6 8/12/6 diff --git a/app/src/main/assets/cube_cyan.png b/app/src/main/assets/cube_cyan.png new file mode 100644 index 0000000000000000000000000000000000000000..8b4f55fd0bfb815810364a0dadc790a881ef814e GIT binary patch literal 17331 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 literal 0 HcmV?d00001 diff --git a/app/src/main/assets/trigrid.png b/app/src/main/assets/trigrid.png new file mode 100755 index 0000000000000000000000000000000000000000..05cbe6e52f39a5a9aa7e4e9c2b46b9b9b4d5e3b2 GIT binary patch literal 37354 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 C`Ms+E literal 0 HcmV?d00001 diff --git a/app/src/main/java/com/google/ar/core/examples/java/helloar/CameraPermissionHelper.java b/app/src/main/java/com/google/ar/core/examples/java/helloar/CameraPermissionHelper.java new file mode 100755 index 0000000..093550a --- /dev/null +++ b/app/src/main/java/com/google/ar/core/examples/java/helloar/CameraPermissionHelper.java @@ -0,0 +1,52 @@ + +package com.google.ar.core.examples.java.helloar; + +import android.Manifest; +import android.app.Activity; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.provider.Settings; +import android.support.v4.app.ActivityCompat; +import android.support.v4.content.ContextCompat; + +/** + * Helper to ask camera permission. + */ +public class CameraPermissionHelper { + private static final int CAMERA_PERMISSION_CODE = 0; + private static final String CAMERA_PERMISSION = Manifest.permission.CAMERA; + + /** + * Check to see we have the necessary permissions for this app. + */ + public static boolean hasCameraPermission(Activity activity) { + return ContextCompat.checkSelfPermission(activity, CAMERA_PERMISSION) + == PackageManager.PERMISSION_GRANTED; + } + + /** + * Check to see we have the necessary permissions for this app, and ask for them if we don't. + */ + public static void requestCameraPermission(Activity activity) { + ActivityCompat.requestPermissions( + activity, new String[]{CAMERA_PERMISSION}, CAMERA_PERMISSION_CODE); + } + + /** + * Check to see if we need to show the rationale for this permission. + */ + public static boolean shouldShowRequestPermissionRationale(Activity activity) { + return ActivityCompat.shouldShowRequestPermissionRationale(activity, CAMERA_PERMISSION); + } + + /** + * Launch Application Setting to grant permission. + */ + public static void launchPermissionSettings(Activity activity) { + Intent intent = new Intent(); + intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setData(Uri.fromParts("package", activity.getPackageName(), null)); + activity.startActivity(intent); + } +} diff --git a/app/src/main/java/com/google/ar/core/examples/java/helloar/DisplayRotationHelper.java b/app/src/main/java/com/google/ar/core/examples/java/helloar/DisplayRotationHelper.java new file mode 100644 index 0000000..7befd34 --- /dev/null +++ b/app/src/main/java/com/google/ar/core/examples/java/helloar/DisplayRotationHelper.java @@ -0,0 +1,91 @@ + +package com.google.ar.core.examples.java.helloar; + +import android.content.Context; +import android.hardware.display.DisplayManager; +import android.hardware.display.DisplayManager.DisplayListener; +import android.view.Display; +import android.view.WindowManager; + +import com.google.ar.core.Session; + +/** + * Helper to track the display rotations. In particular, the 180 degree rotations are not notified + * by the onSurfaceChanged() callback, and thus they require listening to the android display + * events. + */ +public class DisplayRotationHelper implements DisplayListener { + private boolean viewportChanged; + private int viewportWidth; + private int viewportHeight; + private final Context context; + private final Display display; + + /** + * Constructs the DisplayRotationHelper but does not register the listener yet. + * + * @param context the Android {@link Context}. + */ + public DisplayRotationHelper(Context context) { + this.context = context; + display = context.getSystemService(WindowManager.class).getDefaultDisplay(); + } + + public void onResume() { + context.getSystemService(DisplayManager.class).registerDisplayListener(this, null); + } + + + public void onPause() { + context.getSystemService(DisplayManager.class).unregisterDisplayListener(this); + } + + /** + * Records a change in surface dimensions. This will be later used by {@link + * #updateSessionIfNeeded(Session)}. Should be called from {@link + * android.opengl.GLSurfaceView.Renderer + * #onSurfaceChanged(javax.microedition.khronos.opengles.GL10, int, int)}. + * + * @param width the updated width of the surface. + * @param height the updated height of the surface. + */ + public void onSurfaceChanged(int width, int height) { + viewportWidth = width; + viewportHeight = height; + viewportChanged = true; + } + + /** + * Updates the session display geometry if a change was posted either by {@link + * #onSurfaceChanged(int, int)} call or by {@link #onDisplayChanged(int)} system callback. This + * function should be called explicitly before each call to {@link Session#update()}. This + * function will also clear the 'pending update' (viewportChanged) flag. + * + * @param session the {@link Session} object to update if display geometry changed. + */ + public void updateSessionIfNeeded(Session session) { + if (viewportChanged) { + int displayRotation = display.getRotation(); + session.setDisplayGeometry(displayRotation, viewportWidth, viewportHeight); + viewportChanged = false; + } + } + + /** + * Returns the current rotation state of android display. Same as {@link Display#getRotation()}. + */ + public int getRotation() { + return display.getRotation(); + } + + @Override + public void onDisplayAdded(int displayId) {} + + @Override + public void onDisplayRemoved(int displayId) {} + + @Override + public void onDisplayChanged(int displayId) { + viewportChanged = true; + } +} diff --git a/app/src/main/java/com/google/ar/core/examples/java/helloar/rendering/BackgroundRenderer.java b/app/src/main/java/com/google/ar/core/examples/java/helloar/rendering/BackgroundRenderer.java new file mode 100755 index 0000000..ff67b7f --- /dev/null +++ b/app/src/main/java/com/google/ar/core/examples/java/helloar/rendering/BackgroundRenderer.java @@ -0,0 +1,178 @@ + +package com.google.ar.core.examples.java.helloar.rendering; + +import android.content.Context; +import android.opengl.GLES11Ext; +import android.opengl.GLES20; +import android.opengl.GLSurfaceView; + +import com.google.ar.core.Frame; +import com.google.ar.core.Session; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; + +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; + +import fr.geolabs.dev.mapmint4me.R; + +/** + * This class renders the AR background from camera feed. It creates and hosts the texture given to + * ARCore to be filled with the camera image. + */ +public class BackgroundRenderer { + private static final String TAG = BackgroundRenderer.class.getSimpleName(); + + private static final int COORDS_PER_VERTEX = 3; + private static final int TEXCOORDS_PER_VERTEX = 2; + private static final int FLOAT_SIZE = 4; + + private FloatBuffer quadVertices; + private FloatBuffer quadTexCoord; + private FloatBuffer quadTexCoordTransformed; + + private int quadProgram; + + private int quadPositionParam; + private int quadTexCoordParam; + private int textureId = -1; + + public BackgroundRenderer() { + } + + public int getTextureId() { + return textureId; + } + + /** + * Allocates and initializes OpenGL resources needed by the background renderer. Must be called on + * the OpenGL thread, typically in {@link GLSurfaceView.Renderer#onSurfaceCreated(GL10, + * EGLConfig)}. + * + * @param context Needed to access shader source. + */ + public void createOnGlThread(Context context) { + // Generate the background texture. + int[] textures = new int[1]; + GLES20.glGenTextures(1, textures, 0); + textureId = textures[0]; + int textureTarget = GLES11Ext.GL_TEXTURE_EXTERNAL_OES; + GLES20.glBindTexture(textureTarget, textureId); + GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST); + GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST); + + int numVertices = 4; + if (numVertices != QUAD_COORDS.length / COORDS_PER_VERTEX) { + throw new RuntimeException("Unexpected number of vertices in BackgroundRenderer."); + } + + ByteBuffer bbVertices = ByteBuffer.allocateDirect(QUAD_COORDS.length * FLOAT_SIZE); + bbVertices.order(ByteOrder.nativeOrder()); + quadVertices = bbVertices.asFloatBuffer(); + quadVertices.put(QUAD_COORDS); + quadVertices.position(0); + + ByteBuffer bbTexCoords = + ByteBuffer.allocateDirect(numVertices * TEXCOORDS_PER_VERTEX * FLOAT_SIZE); + bbTexCoords.order(ByteOrder.nativeOrder()); + quadTexCoord = bbTexCoords.asFloatBuffer(); + quadTexCoord.put(QUAD_TEXCOORDS); + quadTexCoord.position(0); + + ByteBuffer bbTexCoordsTransformed = + ByteBuffer.allocateDirect(numVertices * TEXCOORDS_PER_VERTEX * FLOAT_SIZE); + bbTexCoordsTransformed.order(ByteOrder.nativeOrder()); + quadTexCoordTransformed = bbTexCoordsTransformed.asFloatBuffer(); + + int vertexShader = + ShaderUtil.loadGLShader(TAG, context, GLES20.GL_VERTEX_SHADER, R.raw.screenquad_vertex); + int fragmentShader = + ShaderUtil.loadGLShader( + TAG, context, GLES20.GL_FRAGMENT_SHADER, R.raw.screenquad_fragment_oes); + + quadProgram = GLES20.glCreateProgram(); + GLES20.glAttachShader(quadProgram, vertexShader); + GLES20.glAttachShader(quadProgram, fragmentShader); + GLES20.glLinkProgram(quadProgram); + GLES20.glUseProgram(quadProgram); + + ShaderUtil.checkGLError(TAG, "Program creation"); + + quadPositionParam = GLES20.glGetAttribLocation(quadProgram, "a_Position"); + quadTexCoordParam = GLES20.glGetAttribLocation(quadProgram, "a_TexCoord"); + + ShaderUtil.checkGLError(TAG, "Program parameters"); + } + + /** + * Draws the AR background image. The image will be drawn such that virtual content rendered with + * the matrices provided by {@link com.google.ar.core.Camera#getViewMatrix(float[], int)} and + * {@link com.google.ar.core.Camera#getProjectionMatrix(float[], int, float, float)} will + * accurately follow static physical objects. This must be called before drawing virtual + * content. + * + * @param frame The last {@code Frame} returned by {@link Session#update()}. + */ + public void draw(Frame frame) { + // If display rotation changed (also includes view size change), we need to re-query the uv + // coordinates for the screen rect, as they may have changed as well. + if (frame.hasDisplayGeometryChanged()) { + frame.transformDisplayUvCoords(quadTexCoord, quadTexCoordTransformed); + } + + // No need to test or write depth, the screen quad has arbitrary depth, and is expected + // to be drawn first. + GLES20.glDisable(GLES20.GL_DEPTH_TEST); + GLES20.glDepthMask(false); + + GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId); + + GLES20.glUseProgram(quadProgram); + + // Set the vertex positions. + GLES20.glVertexAttribPointer( + quadPositionParam, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, 0, quadVertices); + + // Set the texture coordinates. + GLES20.glVertexAttribPointer( + quadTexCoordParam, + TEXCOORDS_PER_VERTEX, + GLES20.GL_FLOAT, + false, + 0, + quadTexCoordTransformed); + + // Enable vertex arrays + GLES20.glEnableVertexAttribArray(quadPositionParam); + GLES20.glEnableVertexAttribArray(quadTexCoordParam); + + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + + // Disable vertex arrays + GLES20.glDisableVertexAttribArray(quadPositionParam); + GLES20.glDisableVertexAttribArray(quadTexCoordParam); + + // Restore the depth state for further drawing. + GLES20.glDepthMask(true); + GLES20.glEnable(GLES20.GL_DEPTH_TEST); + + ShaderUtil.checkGLError(TAG, "Draw"); + } + + private static final float[] QUAD_COORDS = + new float[]{ + -1.0f, -1.0f, 0.0f, -1.0f, +1.0f, 0.0f, +1.0f, -1.0f, 0.0f, +1.0f, +1.0f, 0.0f, + }; + + private static final float[] QUAD_TEXCOORDS = + new float[]{ + 0.0f, 1.0f, + 0.0f, 0.0f, + 1.0f, 1.0f, + 1.0f, 0.0f, + }; +} diff --git a/app/src/main/java/com/google/ar/core/examples/java/helloar/rendering/ObjectRenderer.java b/app/src/main/java/com/google/ar/core/examples/java/helloar/rendering/ObjectRenderer.java new file mode 100755 index 0000000..1693f65 --- /dev/null +++ b/app/src/main/java/com/google/ar/core/examples/java/helloar/rendering/ObjectRenderer.java @@ -0,0 +1,373 @@ + +package com.google.ar.core.examples.java.helloar.rendering; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.opengl.GLES20; +import android.opengl.GLUtils; +import android.opengl.Matrix; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.nio.ShortBuffer; + +import de.javagl.obj.Obj; +import de.javagl.obj.ObjData; +import de.javagl.obj.ObjReader; +import de.javagl.obj.ObjUtils; +import fr.geolabs.dev.mapmint4me.R; + +/** + * Renders an object loaded from an OBJ file in OpenGL. + */ +public class ObjectRenderer { + private static final String TAG = ObjectRenderer.class.getSimpleName(); + + /** + * Blend mode. + * + * @see #setBlendMode(BlendMode) + */ + public enum BlendMode { + /** + * Multiplies the destination color by the source alpha. + */ + Shadow, + /** + * Normal alpha blending. + */ + Grid + } + + private static final int COORDS_PER_VERTEX = 3; + + // Note: the last component must be zero to avoid applying the translational part of the matrix. + private static final float[] LIGHT_DIRECTION = new float[]{0.250f, 0.866f, 0.433f, 0.0f}; + private final float[] viewLightDirection = new float[4]; + + // Object vertex buffer variables. + private int vertexBufferId; + private int verticesBaseAddress; + private int texCoordsBaseAddress; + private int normalsBaseAddress; + private int indexBufferId; + private int indexCount; + + private int program; + private final int[] textures = new int[1]; + + // Shader location: model view projection matrix. + private int modelViewUniform; + private int modelViewProjectionUniform; + + // Shader location: object attributes. + private int positionAttribute; + private int normalAttribute; + private int texCoordAttribute; + + // Shader location: texture sampler. + private int textureUniform; + + // Shader location: environment properties. + private int lightingParametersUniform; + + // Shader location: material properties. + private int materialParametersUniform; + + private BlendMode blendMode = null; + + // Temporary matrices allocated here to reduce number of allocations for each frame. + private final float[] modelMatrix = new float[16]; + private final float[] modelViewMatrix = new float[16]; + private final float[] modelViewProjectionMatrix = new float[16]; + + // Set some default material properties to use for lighting. + private float ambient = 0.3f; + private float diffuse = 1.0f; + private float specular = 1.0f; + private float specularPower = 6.0f; + + public ObjectRenderer() { + } + + /** + * Creates and initializes OpenGL resources needed for rendering the model. + * + * @param context Context for loading the shader and below-named model and texture assets. + * @param objAssetName Name of the OBJ file containing the model geometry. + * @param diffuseTextureAssetName Name of the PNG file containing the diffuse texture map. + */ + public void createOnGlThread(Context context, String objAssetName, String diffuseTextureAssetName) + throws IOException { + // Read the texture. + Bitmap textureBitmap = + BitmapFactory.decodeStream(context.getAssets().open(diffuseTextureAssetName)); + + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + GLES20.glGenTextures(textures.length, textures, 0); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]); + + GLES20.glTexParameteri( + GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR_MIPMAP_LINEAR); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, textureBitmap, 0); + GLES20.glGenerateMipmap(GLES20.GL_TEXTURE_2D); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); + + textureBitmap.recycle(); + + ShaderUtil.checkGLError(TAG, "Texture loading"); + + // Read the obj file. + InputStream objInputStream = context.getAssets().open(objAssetName); + Obj obj = ObjReader.read(objInputStream); + + // Prepare the Obj so that its structure is suitable for + // rendering with OpenGL: + // 1. Triangulate it + // 2. Make sure that texture coordinates are not ambiguous + // 3. Make sure that normals are not ambiguous + // 4. Convert it to single-indexed data + obj = ObjUtils.convertToRenderable(obj); + + // OpenGL does not use Java arrays. ByteBuffers are used instead to provide data in a format + // that OpenGL understands. + + // Obtain the data from the OBJ, as direct buffers: + IntBuffer wideIndices = ObjData.getFaceVertexIndices(obj, 3); + FloatBuffer vertices = ObjData.getVertices(obj); + FloatBuffer texCoords = ObjData.getTexCoords(obj, 2); + FloatBuffer normals = ObjData.getNormals(obj); + + // Convert int indices to shorts for GL ES 2.0 compatibility + ShortBuffer indices = + ByteBuffer.allocateDirect(2 * wideIndices.limit()) + .order(ByteOrder.nativeOrder()) + .asShortBuffer(); + while (wideIndices.hasRemaining()) { + indices.put((short) wideIndices.get()); + } + indices.rewind(); + + int[] buffers = new int[2]; + GLES20.glGenBuffers(2, buffers, 0); + vertexBufferId = buffers[0]; + indexBufferId = buffers[1]; + + // Load vertex buffer + verticesBaseAddress = 0; + texCoordsBaseAddress = verticesBaseAddress + 4 * vertices.limit(); + normalsBaseAddress = texCoordsBaseAddress + 4 * texCoords.limit(); + final int totalBytes = normalsBaseAddress + 4 * normals.limit(); + + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vertexBufferId); + GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, totalBytes, null, GLES20.GL_STATIC_DRAW); + GLES20.glBufferSubData( + GLES20.GL_ARRAY_BUFFER, verticesBaseAddress, 4 * vertices.limit(), vertices); + GLES20.glBufferSubData( + GLES20.GL_ARRAY_BUFFER, texCoordsBaseAddress, 4 * texCoords.limit(), texCoords); + GLES20.glBufferSubData( + GLES20.GL_ARRAY_BUFFER, normalsBaseAddress, 4 * normals.limit(), normals); + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); + + // Load index buffer + GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, indexBufferId); + indexCount = indices.limit(); + GLES20.glBufferData( + GLES20.GL_ELEMENT_ARRAY_BUFFER, 2 * indexCount, indices, GLES20.GL_STATIC_DRAW); + GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0); + + ShaderUtil.checkGLError(TAG, "OBJ buffer load"); + + final int vertexShader = + ShaderUtil.loadGLShader(TAG, context, GLES20.GL_VERTEX_SHADER, R.raw.object_vertex); + final int fragmentShader = + ShaderUtil.loadGLShader(TAG, context, GLES20.GL_FRAGMENT_SHADER, R.raw.object_fragment); + + program = GLES20.glCreateProgram(); + GLES20.glAttachShader(program, vertexShader); + GLES20.glAttachShader(program, fragmentShader); + GLES20.glLinkProgram(program); + GLES20.glUseProgram(program); + + ShaderUtil.checkGLError(TAG, "Program creation"); + + modelViewUniform = GLES20.glGetUniformLocation(program, "u_ModelView"); + modelViewProjectionUniform = GLES20.glGetUniformLocation(program, "u_ModelViewProjection"); + + positionAttribute = GLES20.glGetAttribLocation(program, "a_Position"); + normalAttribute = GLES20.glGetAttribLocation(program, "a_Normal"); + texCoordAttribute = GLES20.glGetAttribLocation(program, "a_TexCoord"); + + textureUniform = GLES20.glGetUniformLocation(program, "u_Texture"); + + lightingParametersUniform = GLES20.glGetUniformLocation(program, "u_LightingParameters"); + materialParametersUniform = GLES20.glGetUniformLocation(program, "u_MaterialParameters"); + + ShaderUtil.checkGLError(TAG, "Program parameters"); + + Matrix.setIdentityM(modelMatrix, 0); + } + + /** + * Selects the blending mode for rendering. + * + * @param blendMode The blending mode. Null indicates no blending (opaque rendering). + */ + public void setBlendMode(BlendMode blendMode) { + this.blendMode = blendMode; + } + + /** + * Updates the object model matrix and applies scaling. + * + * @param modelMatrix A 4x4 model-to-world transformation matrix, stored in column-major order. + * @param scaleFactor A separate scaling factor to apply before the {@code modelMatrix}. + * @see Matrix + */ + public void updateModelMatrix(float[] modelMatrix, float scaleFactor) { + float[] scaleMatrix = new float[16]; + Matrix.setIdentityM(scaleMatrix, 0); + scaleMatrix[0] = scaleFactor; + scaleMatrix[5] = scaleFactor; + scaleMatrix[10] = scaleFactor; + Matrix.multiplyMM(this.modelMatrix, 0, modelMatrix, 0, scaleMatrix, 0); + } + + /** + * Sets the surface characteristics of the rendered model. + * + * @param ambient Intensity of non-directional surface illumination. + * @param diffuse Diffuse (matte) surface reflectivity. + * @param specular Specular (shiny) surface reflectivity. + * @param specularPower Surface shininess. Larger values result in a smaller, sharper specular + * highlight. + */ + public void setMaterialProperties( + float ambient, float diffuse, float specular, float specularPower) { + this.ambient = ambient; + this.diffuse = diffuse; + this.specular = specular; + this.specularPower = specularPower; + } + + /** + * Draws the model. + * + * @param cameraView A 4x4 view matrix, in column-major order. + * @param cameraPerspective A 4x4 projection matrix, in column-major order. + * @param lightIntensity Illumination intensity. Combined with diffuse and specular material + * properties. + * @see #setBlendMode(BlendMode) + * @see #updateModelMatrix(float[], float) + * @see #setMaterialProperties(float, float, float, float) + * @see Matrix + */ + public void draw(float[] cameraView, float[] cameraPerspective, float lightIntensity) { + + ShaderUtil.checkGLError(TAG, "Before draw"); + + // Build the ModelView and ModelViewProjection matrices + // for calculating object position and light. + Matrix.multiplyMM(modelViewMatrix, 0, cameraView, 0, modelMatrix, 0); + Matrix.multiplyMM(modelViewProjectionMatrix, 0, cameraPerspective, 0, modelViewMatrix, 0); + + GLES20.glUseProgram(program); + + // Set the lighting environment properties. + Matrix.multiplyMV(viewLightDirection, 0, modelViewMatrix, 0, LIGHT_DIRECTION, 0); + normalizeVec3(viewLightDirection); + GLES20.glUniform4f( + lightingParametersUniform, + viewLightDirection[0], + viewLightDirection[1], + viewLightDirection[2], + lightIntensity); + + // Set the object material properties. + GLES20.glUniform4f(materialParametersUniform, ambient, diffuse, specular, specularPower); + + // Attach the object texture. + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]); + GLES20.glUniform1i(textureUniform, 0); + + // Set the vertex attributes. + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vertexBufferId); + + GLES20.glVertexAttribPointer( + positionAttribute, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, 0, verticesBaseAddress); + GLES20.glVertexAttribPointer(normalAttribute, 3, GLES20.GL_FLOAT, false, 0, normalsBaseAddress); + GLES20.glVertexAttribPointer( + texCoordAttribute, 2, GLES20.GL_FLOAT, false, 0, texCoordsBaseAddress); + + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); + + // Set the ModelViewProjection matrix in the shader. + GLES20.glUniformMatrix4fv(modelViewUniform, 1, false, modelViewMatrix, 0); + GLES20.glUniformMatrix4fv(modelViewProjectionUniform, 1, false, modelViewProjectionMatrix, 0); + + // Enable vertex arrays + GLES20.glEnableVertexAttribArray(positionAttribute); + GLES20.glEnableVertexAttribArray(normalAttribute); + GLES20.glEnableVertexAttribArray(texCoordAttribute); + + if (blendMode != null) { + GLES20.glDepthMask(false); + GLES20.glEnable(GLES20.GL_BLEND); + switch (blendMode) { + case Shadow: + // Multiplicative blending function for Shadow. + GLES20.glBlendFunc(GLES20.GL_ZERO, GLES20.GL_ONE_MINUS_SRC_ALPHA); + break; + case Grid: + // Grid, additive blending function. + GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA); + break; + } + } + + GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, indexBufferId); + GLES20.glDrawElements(GLES20.GL_TRIANGLES, indexCount, GLES20.GL_UNSIGNED_SHORT, 0); + GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0); + + if (blendMode != null) { + GLES20.glDisable(GLES20.GL_BLEND); + GLES20.glDepthMask(true); + } + + // Disable vertex arrays + GLES20.glDisableVertexAttribArray(positionAttribute); + GLES20.glDisableVertexAttribArray(normalAttribute); + GLES20.glDisableVertexAttribArray(texCoordAttribute); + + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); + + ShaderUtil.checkGLError(TAG, "After draw"); + } + + private static void normalizeVec3(float[] v) { + float reciprocalLength = 1.0f / (float) Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); + v[0] *= reciprocalLength; + v[1] *= reciprocalLength; + v[2] *= reciprocalLength; + } + + public float[] getModelViewProjectionMatrix(){ + return modelViewProjectionMatrix; + } + + public float[] getModelViewMatrix(){ + return modelViewMatrix; + } + + public float[] getModelMatrix(){ + return modelMatrix; + } + +} diff --git a/app/src/main/java/com/google/ar/core/examples/java/helloar/rendering/PlaneRenderer.java b/app/src/main/java/com/google/ar/core/examples/java/helloar/rendering/PlaneRenderer.java new file mode 100644 index 0000000..6e825c7 --- /dev/null +++ b/app/src/main/java/com/google/ar/core/examples/java/helloar/rendering/PlaneRenderer.java @@ -0,0 +1,429 @@ + +package com.google.ar.core.examples.java.helloar.rendering; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.opengl.GLES20; +import android.opengl.GLSurfaceView; +import android.opengl.GLUtils; +import android.opengl.Matrix; + +import com.google.ar.core.Camera; +import com.google.ar.core.Plane; +import com.google.ar.core.Pose; +import com.google.ar.core.TrackingState; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.ShortBuffer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; + +import fr.geolabs.dev.mapmint4me.R; + +/** + * Renders the detected AR planes. + */ +public class PlaneRenderer { + private static final String TAG = PlaneRenderer.class.getSimpleName(); + + private static final int BYTES_PER_FLOAT = Float.SIZE / 8; + private static final int BYTES_PER_SHORT = Short.SIZE / 8; + private static final int COORDS_PER_VERTEX = 3; // x, z, alpha + + private static final int VERTS_PER_BOUNDARY_VERT = 2; + private static final int INDICES_PER_BOUNDARY_VERT = 3; + private static final int INITIAL_BUFFER_BOUNDARY_VERTS = 64; + + private static final int INITIAL_VERTEX_BUFFER_SIZE_BYTES = + BYTES_PER_FLOAT * COORDS_PER_VERTEX * VERTS_PER_BOUNDARY_VERT * INITIAL_BUFFER_BOUNDARY_VERTS; + + private static final int INITIAL_INDEX_BUFFER_SIZE_BYTES = + BYTES_PER_SHORT + * INDICES_PER_BOUNDARY_VERT + * INDICES_PER_BOUNDARY_VERT + * INITIAL_BUFFER_BOUNDARY_VERTS; + + private static final float FADE_RADIUS_M = 0.25f; + private static final float DOTS_PER_METER = 10.0f; + private static final float EQUILATERAL_TRIANGLE_SCALE = (float) (1 / Math.sqrt(3)); + + // Using the "signed distance field" approach to render sharp lines and circles. + // {dotThreshold, lineThreshold, lineFadeSpeed, occlusionScale} + // dotThreshold/lineThreshold: red/green intensity above which dots/lines are present + // lineFadeShrink: lines will fade in between alpha = 1-(1/lineFadeShrink) and 1.0 + // occlusionShrink: occluded planes will fade out between alpha = 0 and 1/occlusionShrink + private static final float[] GRID_CONTROL = {0.2f, 0.4f, 2.0f, 1.5f}; + + private int planeProgram; + private final int[] textures = new int[1]; + + private int planeXZPositionAlphaAttribute; + + private int planeModelUniform; + private int planeModelViewProjectionUniform; + private int textureUniform; + private int lineColorUniform; + private int dotColorUniform; + private int gridControlUniform; + private int planeUvMatrixUniform; + + private FloatBuffer vertexBuffer = + ByteBuffer.allocateDirect(INITIAL_VERTEX_BUFFER_SIZE_BYTES) + .order(ByteOrder.nativeOrder()) + .asFloatBuffer(); + private ShortBuffer indexBuffer = + ByteBuffer.allocateDirect(INITIAL_INDEX_BUFFER_SIZE_BYTES) + .order(ByteOrder.nativeOrder()) + .asShortBuffer(); + + // Temporary lists/matrices allocated here to reduce number of allocations for each frame. + private final float[] modelMatrix = new float[16]; + private final float[] modelViewMatrix = new float[16]; + private final float[] modelViewProjectionMatrix = new float[16]; + private final float[] planeColor = new float[4]; + private final float[] planeAngleUvMatrix = + new float[4]; // 2x2 rotation matrix applied to uv coords. + + private final Map planeIndexMap = new HashMap<>(); + + public PlaneRenderer() { + } + + /** + * Allocates and initializes OpenGL resources needed by the plane renderer. Must be called on the + * OpenGL thread, typically in {@link GLSurfaceView.Renderer#onSurfaceCreated(GL10, EGLConfig)}. + * + * @param context Needed to access shader source and texture PNG. + * @param gridDistanceTextureName Name of the PNG file containing the grid texture. + */ + public void createOnGlThread(Context context, String gridDistanceTextureName) throws IOException { + int vertexShader = + ShaderUtil.loadGLShader(TAG, context, GLES20.GL_VERTEX_SHADER, R.raw.plane_vertex); + int passthroughShader = + ShaderUtil.loadGLShader(TAG, context, GLES20.GL_FRAGMENT_SHADER, R.raw.plane_fragment); + + planeProgram = GLES20.glCreateProgram(); + GLES20.glAttachShader(planeProgram, vertexShader); + GLES20.glAttachShader(planeProgram, passthroughShader); + GLES20.glLinkProgram(planeProgram); + GLES20.glUseProgram(planeProgram); + + ShaderUtil.checkGLError(TAG, "Program creation"); + + // Read the texture. + Bitmap textureBitmap = + BitmapFactory.decodeStream(context.getAssets().open(gridDistanceTextureName)); + + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + GLES20.glGenTextures(textures.length, textures, 0); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]); + + GLES20.glTexParameteri( + GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR_MIPMAP_LINEAR); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, textureBitmap, 0); + GLES20.glGenerateMipmap(GLES20.GL_TEXTURE_2D); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); + + ShaderUtil.checkGLError(TAG, "Texture loading"); + + planeXZPositionAlphaAttribute = GLES20.glGetAttribLocation(planeProgram, "a_XZPositionAlpha"); + + planeModelUniform = GLES20.glGetUniformLocation(planeProgram, "u_Model"); + planeModelViewProjectionUniform = + GLES20.glGetUniformLocation(planeProgram, "u_ModelViewProjection"); + textureUniform = GLES20.glGetUniformLocation(planeProgram, "u_Texture"); + lineColorUniform = GLES20.glGetUniformLocation(planeProgram, "u_lineColor"); + dotColorUniform = GLES20.glGetUniformLocation(planeProgram, "u_dotColor"); + gridControlUniform = GLES20.glGetUniformLocation(planeProgram, "u_gridControl"); + planeUvMatrixUniform = GLES20.glGetUniformLocation(planeProgram, "u_PlaneUvMatrix"); + + ShaderUtil.checkGLError(TAG, "Program parameters"); + } + + /** + * Updates the plane model transform matrix and extents. + */ + private void updatePlaneParameters( + float[] planeMatrix, float extentX, float extentZ, FloatBuffer boundary) { + System.arraycopy(planeMatrix, 0, modelMatrix, 0, 16); + if (boundary == null) { + vertexBuffer.limit(0); + indexBuffer.limit(0); + return; + } + + // Generate a new set of vertices and a corresponding triangle strip index set so that + // the plane boundary polygon has a fading edge. This is done by making a copy of the + // boundary polygon vertices and scaling it down around center to push it inwards. Then + // the index buffer is setup accordingly. + boundary.rewind(); + int boundaryVertices = boundary.limit() / 2; + int numVertices; + int numIndices; + + numVertices = boundaryVertices * VERTS_PER_BOUNDARY_VERT; + // drawn as GL_TRIANGLE_STRIP with 3n-2 triangles (n-2 for fill, 2n for perimeter). + numIndices = boundaryVertices * INDICES_PER_BOUNDARY_VERT; + + if (vertexBuffer.capacity() < numVertices * COORDS_PER_VERTEX) { + int size = vertexBuffer.capacity(); + while (size < numVertices * COORDS_PER_VERTEX) { + size *= 2; + } + vertexBuffer = + ByteBuffer.allocateDirect(BYTES_PER_FLOAT * size) + .order(ByteOrder.nativeOrder()) + .asFloatBuffer(); + } + vertexBuffer.rewind(); + vertexBuffer.limit(numVertices * COORDS_PER_VERTEX); + + if (indexBuffer.capacity() < numIndices) { + int size = indexBuffer.capacity(); + while (size < numIndices) { + size *= 2; + } + indexBuffer = + ByteBuffer.allocateDirect(BYTES_PER_SHORT * size) + .order(ByteOrder.nativeOrder()) + .asShortBuffer(); + } + indexBuffer.rewind(); + indexBuffer.limit(numIndices); + + // Note: when either dimension of the bounding box is smaller than 2*FADE_RADIUS_M we + // generate a bunch of 0-area triangles. These don't get rendered though so it works + // out ok. + float xScale = Math.max((extentX - 2 * FADE_RADIUS_M) / extentX, 0.0f); + float zScale = Math.max((extentZ - 2 * FADE_RADIUS_M) / extentZ, 0.0f); + + while (boundary.hasRemaining()) { + float x = boundary.get(); + float z = boundary.get(); + vertexBuffer.put(x); + vertexBuffer.put(z); + vertexBuffer.put(0.0f); + vertexBuffer.put(x * xScale); + vertexBuffer.put(z * zScale); + vertexBuffer.put(1.0f); + } + + // step 1, perimeter + indexBuffer.put((short) ((boundaryVertices - 1) * 2)); + for (int i = 0; i < boundaryVertices; ++i) { + indexBuffer.put((short) (i * 2)); + indexBuffer.put((short) (i * 2 + 1)); + } + indexBuffer.put((short) 1); + // This leaves us on the interior edge of the perimeter between the inset vertices + // for boundary verts n-1 and 0. + + // step 2, interior: + for (int i = 1; i < boundaryVertices / 2; ++i) { + indexBuffer.put((short) ((boundaryVertices - 1 - i) * 2 + 1)); + indexBuffer.put((short) (i * 2 + 1)); + } + if (boundaryVertices % 2 != 0) { + indexBuffer.put((short) ((boundaryVertices / 2) * 2 + 1)); + } + } + + private void draw(float[] cameraView, float[] cameraPerspective) { + // Build the ModelView and ModelViewProjection matrices + // for calculating cube position and light. + Matrix.multiplyMM(modelViewMatrix, 0, cameraView, 0, modelMatrix, 0); + Matrix.multiplyMM(modelViewProjectionMatrix, 0, cameraPerspective, 0, modelViewMatrix, 0); + + // Set the position of the plane + vertexBuffer.rewind(); + GLES20.glVertexAttribPointer( + planeXZPositionAlphaAttribute, + COORDS_PER_VERTEX, + GLES20.GL_FLOAT, + false, + BYTES_PER_FLOAT * COORDS_PER_VERTEX, + vertexBuffer); + + // Set the Model and ModelViewProjection matrices in the shader. + GLES20.glUniformMatrix4fv(planeModelUniform, 1, false, modelMatrix, 0); + GLES20.glUniformMatrix4fv( + planeModelViewProjectionUniform, 1, false, modelViewProjectionMatrix, 0); + + indexBuffer.rewind(); + GLES20.glDrawElements( + GLES20.GL_TRIANGLE_STRIP, indexBuffer.limit(), GLES20.GL_UNSIGNED_SHORT, indexBuffer); + ShaderUtil.checkGLError(TAG, "Drawing plane"); + } + + static class SortablePlane { + final float distance; + final Plane plane; + + SortablePlane(float distance, Plane plane) { + this.distance = distance; + this.plane = plane; + } + } + + /** + * Draws the collection of tracked planes, with closer planes hiding more distant ones. + * + * @param allPlanes The collection of planes to draw. + * @param cameraPose The pose of the camera, as returned by {@link Camera#getPose()} + * @param cameraPerspective The projection matrix, as returned by {@link + * Camera#getProjectionMatrix(float[], int, float, float)} + */ + public void drawPlanes(Collection allPlanes, Pose cameraPose, float[] cameraPerspective) { + // Planes must be sorted by distance from camera so that we draw closer planes first, and + // they occlude the farther planes. + List sortedPlanes = new ArrayList<>(); + float[] normal = new float[3]; + float cameraX = cameraPose.tx(); + float cameraY = cameraPose.ty(); + float cameraZ = cameraPose.tz(); + for (Plane plane : allPlanes) { + if (plane.getTrackingState() != TrackingState.TRACKING || plane.getSubsumedBy() != null) { + continue; + } + + Pose center = plane.getCenterPose(); + // Get transformed Y axis of plane's coordinate system. + center.getTransformedAxis(1, 1.0f, normal, 0); + // Compute dot product of plane's normal with vector from camera to plane center. + float distance = + (cameraX - center.tx()) * normal[0] + + (cameraY - center.ty()) * normal[1] + + (cameraZ - center.tz()) * normal[2]; + if (distance < 0) { // Plane is back-facing. + continue; + } + sortedPlanes.add(new SortablePlane(distance, plane)); + } + Collections.sort( + sortedPlanes, + new Comparator() { + @Override + public int compare(SortablePlane a, SortablePlane b) { + return Float.compare(a.distance, b.distance); + } + }); + + float[] cameraView = new float[16]; + cameraPose.inverse().toMatrix(cameraView, 0); + + // Planes are drawn with additive blending, masked by the alpha channel for occlusion. + + // Start by clearing the alpha channel of the color buffer to 1.0. + GLES20.glClearColor(1, 1, 1, 1); + GLES20.glColorMask(false, false, false, true); + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); + GLES20.glColorMask(true, true, true, true); + + // Disable depth write. + GLES20.glDepthMask(false); + + // Additive blending, masked by alpha channel, clearing alpha channel. + GLES20.glEnable(GLES20.GL_BLEND); + GLES20.glBlendFuncSeparate( + GLES20.GL_DST_ALPHA, GLES20.GL_ONE, // RGB (src, dest) + GLES20.GL_ZERO, GLES20.GL_ONE_MINUS_SRC_ALPHA); // ALPHA (src, dest) + + // Set up the shader. + GLES20.glUseProgram(planeProgram); + + // Attach the texture. + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]); + GLES20.glUniform1i(textureUniform, 0); + + // Shared fragment uniforms. + GLES20.glUniform4fv(gridControlUniform, 1, GRID_CONTROL, 0); + + // Enable vertex arrays + GLES20.glEnableVertexAttribArray(planeXZPositionAlphaAttribute); + + ShaderUtil.checkGLError(TAG, "Setting up to draw planes"); + + for (SortablePlane sortedPlane : sortedPlanes) { + Plane plane = sortedPlane.plane; + float[] planeMatrix = new float[16]; + plane.getCenterPose().toMatrix(planeMatrix, 0); + + updatePlaneParameters( + planeMatrix, plane.getExtentX(), plane.getExtentZ(), plane.getPolygon()); + + // Get plane index. Keep a map to assign same indices to same planes. + Integer planeIndex = planeIndexMap.get(plane); + if (planeIndex == null) { + planeIndex = planeIndexMap.size(); + planeIndexMap.put(plane, planeIndex); + } + + // Set plane color. Computed deterministically from the Plane index. + int colorIndex = planeIndex % PLANE_COLORS_RGBA.length; + colorRgbaToFloat(planeColor, PLANE_COLORS_RGBA[colorIndex]); + GLES20.glUniform4fv(lineColorUniform, 1, planeColor, 0); + GLES20.glUniform4fv(dotColorUniform, 1, planeColor, 0); + + // Each plane will have its own angle offset from others, to make them easier to + // distinguish. Compute a 2x2 rotation matrix from the angle. + float angleRadians = planeIndex * 0.144f; + float uScale = DOTS_PER_METER; + float vScale = DOTS_PER_METER * EQUILATERAL_TRIANGLE_SCALE; + planeAngleUvMatrix[0] = +(float) Math.cos(angleRadians) * uScale; + planeAngleUvMatrix[1] = -(float) Math.sin(angleRadians) * vScale; + planeAngleUvMatrix[2] = +(float) Math.sin(angleRadians) * uScale; + planeAngleUvMatrix[3] = +(float) Math.cos(angleRadians) * vScale; + GLES20.glUniformMatrix2fv(planeUvMatrixUniform, 1, false, planeAngleUvMatrix, 0); + + draw(cameraView, cameraPerspective); + } + + // Clean up the state we set + GLES20.glDisableVertexAttribArray(planeXZPositionAlphaAttribute); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); + GLES20.glDisable(GLES20.GL_BLEND); + GLES20.glDepthMask(true); + + ShaderUtil.checkGLError(TAG, "Cleaning up after drawing planes"); + } + + private static void colorRgbaToFloat(float[] planeColor, int colorRgba) { + planeColor[0] = ((float) ((colorRgba >> 24) & 0xff)) / 255.0f; + planeColor[1] = ((float) ((colorRgba >> 16) & 0xff)) / 255.0f; + planeColor[2] = ((float) ((colorRgba >> 8) & 0xff)) / 255.0f; + planeColor[3] = ((float) ((colorRgba >> 0) & 0xff)) / 255.0f; + } + + private static final int[] PLANE_COLORS_RGBA = { + 0xFFFFFFFF, + 0xF44336FF, + 0xE91E63FF, + 0x9C27B0FF, + 0x673AB7FF, + 0x3F51B5FF, + 0x2196F3FF, + 0x03A9F4FF, + 0x00BCD4FF, + 0x009688FF, + 0x4CAF50FF, + 0x8BC34AFF, + 0xCDDC39FF, + 0xFFEB3BFF, + 0xFFC107FF, + 0xFF9800FF, + }; +} diff --git a/app/src/main/java/com/google/ar/core/examples/java/helloar/rendering/PointCloudRenderer.java b/app/src/main/java/com/google/ar/core/examples/java/helloar/rendering/PointCloudRenderer.java new file mode 100644 index 0000000..2b96b15 --- /dev/null +++ b/app/src/main/java/com/google/ar/core/examples/java/helloar/rendering/PointCloudRenderer.java @@ -0,0 +1,144 @@ + +package com.google.ar.core.examples.java.helloar.rendering; + +import android.content.Context; +import android.opengl.GLES20; +import android.opengl.GLSurfaceView; +import android.opengl.Matrix; + +import com.google.ar.core.PointCloud; + +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; + +import fr.geolabs.dev.mapmint4me.R; + +/** + * Renders a point cloud. + */ +public class PointCloudRenderer { + private static final String TAG = PointCloud.class.getSimpleName(); + + private static final int BYTES_PER_FLOAT = Float.SIZE / 8; + private static final int FLOATS_PER_POINT = 4; // X,Y,Z,confidence. + private static final int BYTES_PER_POINT = BYTES_PER_FLOAT * FLOATS_PER_POINT; + private static final int INITIAL_BUFFER_POINTS = 1000; + + private int vbo; + private int vboSize; + + private int programName; + private int positionAttribute; + private int modelViewProjectionUniform; + private int colorUniform; + private int pointSizeUniform; + + private int numPoints = 0; + + // Keep track of the last point cloud rendered to avoid updating the VBO if point cloud + // was not changed. + private PointCloud lastPointCloud = null; + + public PointCloudRenderer() {} + + /** + * Allocates and initializes OpenGL resources needed by the plane renderer. Must be called on the + * OpenGL thread, typically in {@link GLSurfaceView.Renderer#onSurfaceCreated(GL10, EGLConfig)}. + * + * @param context Needed to access shader source. + */ + public void createOnGlThread(Context context) { + ShaderUtil.checkGLError(TAG, "before create"); + + int[] buffers = new int[1]; + GLES20.glGenBuffers(1, buffers, 0); + vbo = buffers[0]; + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vbo); + + vboSize = INITIAL_BUFFER_POINTS * BYTES_PER_POINT; + GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, vboSize, null, GLES20.GL_DYNAMIC_DRAW); + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); + + ShaderUtil.checkGLError(TAG, "buffer alloc"); + + int vertexShader = + ShaderUtil.loadGLShader(TAG, context, GLES20.GL_VERTEX_SHADER, R.raw.point_cloud_vertex); + int passthroughShader = + ShaderUtil.loadGLShader( + TAG, context, GLES20.GL_FRAGMENT_SHADER, R.raw.passthrough_fragment); + + programName = GLES20.glCreateProgram(); + GLES20.glAttachShader(programName, vertexShader); + GLES20.glAttachShader(programName, passthroughShader); + GLES20.glLinkProgram(programName); + GLES20.glUseProgram(programName); + + ShaderUtil.checkGLError(TAG, "program"); + + positionAttribute = GLES20.glGetAttribLocation(programName, "a_Position"); + colorUniform = GLES20.glGetUniformLocation(programName, "u_Color"); + modelViewProjectionUniform = GLES20.glGetUniformLocation(programName, "u_ModelViewProjection"); + pointSizeUniform = GLES20.glGetUniformLocation(programName, "u_PointSize"); + + ShaderUtil.checkGLError(TAG, "program params"); + } + + /** + * Updates the OpenGL buffer contents to the provided point. Repeated calls with the same point + * cloud will be ignored. + */ + public void update(PointCloud cloud) { + if (lastPointCloud == cloud) { + // Redundant call. + return; + } + + ShaderUtil.checkGLError(TAG, "before update"); + + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vbo); + lastPointCloud = cloud; + + // If the VBO is not large enough to fit the new point cloud, resize it. + numPoints = lastPointCloud.getPoints().remaining() / FLOATS_PER_POINT; + if (numPoints * BYTES_PER_POINT > vboSize) { + while (numPoints * BYTES_PER_POINT > vboSize) { + vboSize *= 2; + } + GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, vboSize, null, GLES20.GL_DYNAMIC_DRAW); + } + GLES20.glBufferSubData( + GLES20.GL_ARRAY_BUFFER, 0, numPoints * BYTES_PER_POINT, lastPointCloud.getPoints()); + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); + + ShaderUtil.checkGLError(TAG, "after update"); + } + + /** + * Renders the point cloud. ArCore point cloud is given in world space. + * + * @param cameraView the camera view matrix for this frame, typically from {@link + * com.google.ar.core.Camera#getViewMatrix(float[], int)}. + * @param cameraPerspective the camera projection matrix for this frame, typically from {@link + * com.google.ar.core.Camera#getProjectionMatrix(float[], int, float, float)}. + */ + public void draw(float[] cameraView, float[] cameraPerspective) { + float[] modelViewProjection = new float[16]; + Matrix.multiplyMM(modelViewProjection, 0, cameraPerspective, 0, cameraView, 0); + + ShaderUtil.checkGLError(TAG, "Before draw"); + + GLES20.glUseProgram(programName); + GLES20.glEnableVertexAttribArray(positionAttribute); + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vbo); + GLES20.glVertexAttribPointer(positionAttribute, 4, GLES20.GL_FLOAT, false, BYTES_PER_POINT, 0); + GLES20.glUniform4f(colorUniform, 31.0f / 255.0f, 188.0f / 255.0f, 210.0f / 255.0f, 1.0f); + GLES20.glUniformMatrix4fv(modelViewProjectionUniform, 1, false, modelViewProjection, 0); + GLES20.glUniform1f(pointSizeUniform, 5.0f); + + GLES20.glDrawArrays(GLES20.GL_POINTS, 0, numPoints); + GLES20.glDisableVertexAttribArray(positionAttribute); + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); + + ShaderUtil.checkGLError(TAG, "Draw"); + } +} diff --git a/app/src/main/java/com/google/ar/core/examples/java/helloar/rendering/ShaderUtil.java b/app/src/main/java/com/google/ar/core/examples/java/helloar/rendering/ShaderUtil.java new file mode 100755 index 0000000..93dc727 --- /dev/null +++ b/app/src/main/java/com/google/ar/core/examples/java/helloar/rendering/ShaderUtil.java @@ -0,0 +1,89 @@ + +package com.google.ar.core.examples.java.helloar.rendering; + +import android.content.Context; +import android.opengl.GLES20; +import android.util.Log; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +/** + * Shader helper functions. + */ +public class ShaderUtil { + /** + * Converts a raw text file, saved as a resource, into an OpenGL ES shader. + * + * @param type The type of shader we will be creating. + * @param resId The resource ID of the raw text file about to be turned into a shader. + * @return The shader object handler. + */ + public static int loadGLShader(String tag, Context context, int type, int resId) { + String code = readRawTextFile(context, resId); + int shader = GLES20.glCreateShader(type); + GLES20.glShaderSource(shader, code); + GLES20.glCompileShader(shader); + + // Get the compilation status. + final int[] compileStatus = new int[1]; + GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compileStatus, 0); + + // If the compilation failed, delete the shader. + if (compileStatus[0] == 0) { + Log.e(tag, "Error compiling shader: " + GLES20.glGetShaderInfoLog(shader)); + GLES20.glDeleteShader(shader); + shader = 0; + } + + if (shader == 0) { + throw new RuntimeException("Error creating shader."); + } + + return shader; + } + + /** + * Checks if we've had an error inside of OpenGL ES, and if so what that error is. + * + * @param label Label to report in case of error. + * @throws RuntimeException If an OpenGL error is detected. + */ + public static void checkGLError(String tag, String label) { + int lastError = GLES20.GL_NO_ERROR; + // Drain the queue of all errors. + int error; + while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) { + Log.e(tag, label + ": glError " + error); + lastError = error; + } + if (lastError != GLES20.GL_NO_ERROR) { + throw new RuntimeException(label + ": glError " + lastError); + } + } + + /** + * Converts a raw text file into a string. + * + * @param resId The resource ID of the raw text file about to be turned into a shader. + * @return The context of the text file, or null in case of error. + */ + private static String readRawTextFile(Context context, int resId) { + InputStream inputStream = context.getResources().openRawResource(resId); + try { + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); + } + reader.close(); + return sb.toString(); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } +} diff --git a/app/src/main/java/com/hl3hl3/arcoremeasure/ArMeasureActivity.java b/app/src/main/java/com/hl3hl3/arcoremeasure/ArMeasureActivity.java new file mode 100644 index 0000000..caceaf3 --- /dev/null +++ b/app/src/main/java/com/hl3hl3/arcoremeasure/ArMeasureActivity.java @@ -0,0 +1,967 @@ +package com.hl3hl3.arcoremeasure; + +import android.content.Context; +import android.opengl.GLES20; +import android.opengl.GLSurfaceView; +import android.opengl.Matrix; +import android.os.Build; +import android.os.Bundle; +import android.support.design.widget.BaseTransientBottomBar; +import android.support.design.widget.FloatingActionButton; +import android.support.design.widget.Snackbar; +import android.support.v7.app.AppCompatActivity; +import android.util.Log; +import android.view.GestureDetector; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.WindowManager; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.PopupWindow; +import android.widget.TextView; +import android.widget.Toast; + +import com.crashlytics.android.Crashlytics; +import com.google.ar.core.Anchor; +import com.google.ar.core.ArCoreApk; +import com.google.ar.core.Camera; +import com.google.ar.core.Config; +import com.google.ar.core.Frame; +import com.google.ar.core.HitResult; +import com.google.ar.core.Plane; +import com.google.ar.core.Point; +import com.google.ar.core.PointCloud; +import com.google.ar.core.Pose; +import com.google.ar.core.Session; +import com.google.ar.core.Trackable; +import com.google.ar.core.TrackingState; +import com.google.ar.core.examples.java.helloar.CameraPermissionHelper; +import com.google.ar.core.examples.java.helloar.DisplayRotationHelper; +import com.google.ar.core.examples.java.helloar.rendering.BackgroundRenderer; +import com.google.ar.core.examples.java.helloar.rendering.ObjectRenderer; +import com.google.ar.core.examples.java.helloar.rendering.PlaneRenderer; +import com.google.ar.core.examples.java.helloar.rendering.PointCloudRenderer; +import com.google.ar.core.exceptions.CameraNotAvailableException; +import com.google.ar.core.exceptions.NotTrackingException; +import com.google.ar.core.exceptions.UnavailableApkTooOldException; +import com.google.ar.core.exceptions.UnavailableArcoreNotInstalledException; +import com.google.ar.core.exceptions.UnavailableSdkTooOldException; +import com.google.ar.core.exceptions.UnavailableUserDeclinedInstallationException; +import com.hl3hl3.arcoremeasure.renderer.RectanglePolygonRenderer; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.concurrent.ArrayBlockingQueue; + +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; + +import fr.geolabs.dev.mapmint4me.BuildConfig; +import fr.geolabs.dev.mapmint4me.R; +import io.fabric.sdk.android.Fabric; + + +public class ArMeasureActivity extends AppCompatActivity { + private static final String TAG = ArMeasureActivity.class.getSimpleName(); + private static final String ASSET_NAME_CUBE_OBJ = "cube.obj"; + private static final String ASSET_NAME_CUBE = "cube_green.png"; + private static final String ASSET_NAME_CUBE_SELECTED = "cube_cyan.png"; + + private static final int MAX_CUBE_COUNT = 16; + + // Rendering. The Renderers are created here, and initialized when the GL surface is created. + private GLSurfaceView surfaceView = null; + + private boolean installRequested; + + private Session session = null; + private GestureDetector gestureDetector; + private Snackbar messageSnackbar = null; + private DisplayRotationHelper displayRotationHelper; + + private final BackgroundRenderer backgroundRenderer = new BackgroundRenderer(); + private final PlaneRenderer planeRenderer = new PlaneRenderer(); + private final PointCloudRenderer pointCloud = new PointCloudRenderer(); + + private final ObjectRenderer cube = new ObjectRenderer(); + private final ObjectRenderer cubeSelected = new ObjectRenderer(); + private RectanglePolygonRenderer rectRenderer = null; + + // Temporary matrix allocated here to reduce number of allocations for each frame. + private final float[] anchorMatrix = new float[MAX_CUBE_COUNT]; + private final ImageView[] ivCubeIconList = new ImageView[MAX_CUBE_COUNT]; + private final int[] cubeIconIdArray = { + R.id.iv_cube1, + R.id.iv_cube2, + R.id.iv_cube3, + R.id.iv_cube4, + R.id.iv_cube5, + R.id.iv_cube6, + R.id.iv_cube7, + R.id.iv_cube8, + R.id.iv_cube9, + R.id.iv_cube10, + R.id.iv_cube11, + R.id.iv_cube12, + R.id.iv_cube13, + R.id.iv_cube14, + R.id.iv_cube15, + R.id.iv_cube16 + }; + + // Tap handling and UI. + private ArrayBlockingQueue queuedSingleTaps = new ArrayBlockingQueue<>(MAX_CUBE_COUNT); + private ArrayBlockingQueue queuedLongPress = new ArrayBlockingQueue<>(MAX_CUBE_COUNT); + private final ArrayList anchors = new ArrayList<>(); + private ArrayList showingTapPointX = new ArrayList<>(); + private ArrayList showingTapPointY = new ArrayList<>(); + + private ArrayBlockingQueue queuedScrollDx = new ArrayBlockingQueue<>(MAX_CUBE_COUNT); + private ArrayBlockingQueue queuedScrollDy = new ArrayBlockingQueue<>(MAX_CUBE_COUNT); + + private void log(String tag, String log){ + if(BuildConfig.DEBUG) { + Log.d(tag, log); + } + } + + private void log(Exception e){ + try { + Crashlytics.logException(e); + if (BuildConfig.DEBUG) { + e.printStackTrace(); + } + }catch (Exception ex){ + if (BuildConfig.DEBUG) { + ex.printStackTrace(); + } + } + } + + private void logStatus(String msg){ + try { + Crashlytics.log(msg); + }catch (Exception e){ + log(e); + } + } + + // OverlayView overlayViewForTest; + private TextView tv_result; + private FloatingActionButton fab; + + private GLSurfaceRenderer glSerfaceRenderer = null; + private GestureDetector.SimpleOnGestureListener gestureDetectorListener = new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onSingleTapUp(MotionEvent e) { + // Queue tap if there is space. Tap is lost if queue is full. + queuedSingleTaps.offer(e); +// log(TAG, "onSingleTapUp, e=" + e.getRawX() + ", " + e.getRawY()); + return true; + } + + @Override + public void onLongPress(MotionEvent e) { + queuedLongPress.offer(e); + } + + @Override + public boolean onDown(MotionEvent e) { + return true; + } + + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, + float distanceX, float distanceY) { +// log(TAG, "onScroll, dx=" + distanceX + " dy=" + distanceY); + queuedScrollDx.offer(distanceX); + queuedScrollDy.offer(distanceY); + return true; + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Fabric.with(this, new Crashlytics()); + setContentView(R.layout.activity_main); + +// overlayViewForTest = (OverlayView)findViewById(R.id.overlay_for_test); + tv_result = findViewById(R.id.tv_result); + fab = findViewById(R.id.fab); + + for(int i=0; i= Build.VERSION_CODES.N) { + float screenWidth = getResources().getDisplayMetrics().widthPixels; + float screenHeight = getResources().getDisplayMetrics().heightPixels; + popUp.showAtLocation(v, Gravity.NO_GRAVITY, (int)screenWidth/2, (int)screenHeight/2); + } else { + popUp.showAsDropDown(v); + } + } + }); + fab.hide(); + + + displayRotationHelper = new DisplayRotationHelper(/*context=*/ this); + + if(CameraPermissionHelper.hasCameraPermission(this)){ + setupRenderer(); + } + + installRequested = false; + } + + private void setupRenderer(){ + if(surfaceView != null){ + return; + } + surfaceView = findViewById(R.id.surfaceview); + + // Set up tap listener. + gestureDetector = new GestureDetector(this, gestureDetectorListener); + surfaceView.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + return gestureDetector.onTouchEvent(event); + } + }); + glSerfaceRenderer = new GLSurfaceRenderer(this); + surfaceView.setPreserveEGLContextOnPause(true); + surfaceView.setEGLContextClientVersion(2); + surfaceView.setEGLConfigChooser(8, 8, 8, 8, 16, 0); // Alpha used for plane blending. + surfaceView.setRenderer(glSerfaceRenderer); + surfaceView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY); + } + + @Override + protected void onResume() { + super.onResume(); + logStatus("onResume()"); + if (session == null) { + Exception exception = null; + String message = null; + try { + switch (ArCoreApk.getInstance().requestInstall(this, !installRequested)) { + case INSTALL_REQUESTED: + installRequested = true; + return; + case INSTALLED: + break; + } + + // ARCore requires camera permissions to operate. If we did not yet obtain runtime + // permission on Android M and above, now is a good time to ask the user for it. + if (!CameraPermissionHelper.hasCameraPermission(this)) { + CameraPermissionHelper.requestCameraPermission(this); + return; + } + + session = new Session(/* context= */ this); + } catch (UnavailableArcoreNotInstalledException + | UnavailableUserDeclinedInstallationException e) { + message = "Please install ARCore"; + exception = e; + } catch (UnavailableApkTooOldException e) { + message = "Please update ARCore"; + exception = e; + } catch (UnavailableSdkTooOldException e) { + message = "Please update this app"; + exception = e; + } catch (Exception e) { + message = "This device does not support AR"; + exception = e; + } + + if (message != null) { + showSnackbarMessage(message, true); + Log.e(TAG, "Exception creating session", exception); + return; + } + + // Create default config and check if supported. + Config config = new Config(session); + if (!session.isSupported(config)) { + showSnackbarMessage("This device does not support AR", true); + } + session.configure(config); + + setupRenderer(); + } + + showLoadingMessage(); + // Note that order matters - see the note in onPause(), the reverse applies here. + try { + session.resume(); + } catch (CameraNotAvailableException e) { + e.printStackTrace(); + } + surfaceView.onResume(); + displayRotationHelper.onResume(); + } + + @Override + public void onPause() { + super.onPause(); + logStatus("onPause()"); + if (session != null) { + // Note that the order matters - GLSurfaceView is paused first so that it does not try + // to query the session. If Session is paused before GLSurfaceView, GLSurfaceView may + // still call session.update() and get a SessionPausedException. + displayRotationHelper.onPause(); + surfaceView.onPause(); + session.pause(); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] results) { + logStatus("onRequestPermissionsResult()"); + Toast.makeText(this, R.string.need_permission, Toast.LENGTH_LONG) + .show(); + if (!CameraPermissionHelper.shouldShowRequestPermissionRationale(this)) { + // Permission denied with checking "Do not ask again". + CameraPermissionHelper.launchPermissionSettings(this); + } + finish(); + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + logStatus("onWindowFocusChanged()"); + if (hasFocus) { + // Standard Android full-screen functionality. + getWindow().getDecorView().setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + } + + private void showLoadingMessage() { + runOnUiThread(new Runnable() { + @Override + public void run() { + messageSnackbar = Snackbar.make( + ArMeasureActivity.this.findViewById(android.R.id.content), + "Searching for surfaces...", Snackbar.LENGTH_INDEFINITE); + messageSnackbar.getView().setBackgroundColor(0xbf323232); + messageSnackbar.show(); + } + }); + } + + private void hideLoadingMessage() { + runOnUiThread(new Runnable() { + @Override + public void run() { + if(messageSnackbar != null) { + messageSnackbar.dismiss(); + } + messageSnackbar = null; + } + }); + } + + private void showSnackbarMessage(String message, boolean finishOnDismiss) { + messageSnackbar = + Snackbar.make( + ArMeasureActivity.this.findViewById(android.R.id.content), + message, + Snackbar.LENGTH_INDEFINITE); + messageSnackbar.getView().setBackgroundColor(0xbf323232); + if (finishOnDismiss) { + messageSnackbar.setAction( + "Dismiss", + new View.OnClickListener() { + @Override + public void onClick(View v) { + messageSnackbar.dismiss(); + } + }); + messageSnackbar.addCallback( + new BaseTransientBottomBar.BaseCallback() { + @Override + public void onDismissed(Snackbar transientBottomBar, int event) { + super.onDismissed(transientBottomBar, event); + finish(); + } + }); + } + messageSnackbar.show(); + } + + private void toast(int stringResId){ + Toast.makeText(this, stringResId, Toast.LENGTH_SHORT).show(); + } + private boolean isVerticalMode = false; + private PopupWindow popupWindow; + private PopupWindow getPopupWindow() { + + // initialize a pop up window type + popupWindow = new PopupWindow(this); + + ArrayList sortList = new ArrayList<>(); + sortList.add(getString(R.string.action_1)); + sortList.add(getString(R.string.action_2)); + sortList.add(getString(R.string.action_3)); + sortList.add(getString(R.string.action_4)); + + ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_dropdown_item_1line, + sortList); + // the drop down list is a list view + ListView listViewSort = new ListView(this); + // set our adapter and pass our pop up window contents + listViewSort.setAdapter(adapter); + listViewSort.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { + @Override + public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { + switch (position){ + case 3:// move vertical axis + toast(R.string.action_4_toast); + break; + case 0:// delete + toast(R.string.action_1_toast); + break; + case 1:// set as first + toast(R.string.action_2_toast); + break; + case 2:// move horizontal axis + default: + toast(R.string.action_3_toast); + break; + } + return true; + } + }); + // set on item selected + listViewSort.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + switch (position){ + case 3:// move vertical axis + isVerticalMode = true; + popupWindow.dismiss(); + break; + case 0:// delete + glSerfaceRenderer.deleteNowSelection(); + popupWindow.dismiss(); + fab.hide(); + break; + case 1:// set as first + glSerfaceRenderer.setNowSelectionAsFirst(); + popupWindow.dismiss(); + fab.hide(); + break; + case 2:// move horizontal axis + default: + isVerticalMode = false; + popupWindow.dismiss(); + break; + } + + } + }); + // some other visual settings for popup window + popupWindow.setFocusable(true); + popupWindow.setWidth((int)(getResources().getDisplayMetrics().widthPixels * 0.4f)); + // popupWindow.setBackgroundDrawable(getResources().getDrawable(R.drawable.white)); + popupWindow.setHeight(WindowManager.LayoutParams.WRAP_CONTENT); + // set the listview as popup content + popupWindow.setContentView(listViewSort); + return popupWindow; + } + + private class GLSurfaceRenderer implements GLSurfaceView.Renderer{ + private static final String TAG = "GLSurfaceRenderer"; + private Context context; + private final int DEFAULT_VALUE = -1; + private int nowTouchingPointIndex = DEFAULT_VALUE; + private int viewWidth = 0; + private int viewHeight = 0; + // according to cube.obj, cube diameter = 0.02f + private final float cubeHitAreaRadius = 0.08f; + private final float[] centerVertexOfCube = {0f, 0f, 0f, 1}; + private final float[] vertexResult = new float[4]; + + private float[] tempTranslation = new float[3]; + private float[] tempRotation = new float[4]; + private float[] projmtx = new float[16]; + private float[] viewmtx = new float[16]; + + public GLSurfaceRenderer(Context context){ + this.context = context; + } + + @Override + public void onSurfaceCreated(GL10 gl, EGLConfig config) { + logStatus("onSurfaceCreated()"); + GLES20.glClearColor(0.1f, 0.1f, 0.1f, 1.0f); + + // Create the texture and pass it to ARCore session to be filled during update(). + backgroundRenderer.createOnGlThread(context); + if (session != null) { + session.setCameraTextureName(backgroundRenderer.getTextureId()); + } + + // Prepare the other rendering objects. + try { + rectRenderer = new RectanglePolygonRenderer(); + cube.createOnGlThread(context, ASSET_NAME_CUBE_OBJ, ASSET_NAME_CUBE); + cube.setMaterialProperties(0.0f, 3.5f, 1.0f, 6.0f); + cubeSelected.createOnGlThread(context, ASSET_NAME_CUBE_OBJ, ASSET_NAME_CUBE_SELECTED); + cubeSelected.setMaterialProperties(0.0f, 3.5f, 1.0f, 6.0f); + } catch (IOException e) { + log(TAG, "Failed to read obj file"); + } + try { + planeRenderer.createOnGlThread(context, "trigrid.png"); + } catch (IOException e) { + log(TAG, "Failed to read plane texture"); + } + pointCloud.createOnGlThread(context); + } + + @Override + public void onSurfaceChanged(GL10 gl, int width, int height) { + if(width <= 0 || height <= 0){ + logStatus("onSurfaceChanged(), <= 0"); + return; + } + logStatus("onSurfaceChanged()"); + + displayRotationHelper.onSurfaceChanged(width, height); + GLES20.glViewport(0, 0, width, height); + viewWidth = width; + viewHeight = height; + setNowTouchingPointIndex(DEFAULT_VALUE); + } + + public void deleteNowSelection(){ + logStatus("deleteNowSelection()"); + int index = nowTouchingPointIndex; + if (index > -1){ + if(index < anchors.size()) { + anchors.remove(index).detach(); + } + if(index < showingTapPointX.size()) { + showingTapPointX.remove(index); + } + if(index < showingTapPointY.size()) { + showingTapPointY.remove(index); + } + } + setNowTouchingPointIndex(DEFAULT_VALUE); + } + + public void setNowSelectionAsFirst(){ + logStatus("setNowSelectionAsFirst()"); + int index = nowTouchingPointIndex; + if (index > -1 && index < anchors.size()) { + if(index < anchors.size()){ + for(int i=0; i= 16) { + anchors.get(0).detach(); + anchors.remove(0); + + showingTapPointX.remove(0); + showingTapPointY.remove(0); + } + + // Adding an Anchor tells ARCore that it should track this position in + // space. This anchor will be used in PlaneAttachment to place the 3d model + // in the correct position relative both to the world and to the plane. + anchors.add(hit.createAnchor()); + + showingTapPointX.add(tap.getX()); + showingTapPointY.add(tap.getY()); + nowTouchingPointIndex = anchors.size() - 1; + + showMoreAction(); + showCubeStatus(); + break; + } + } + }else{ + handleMoveEvent(nowTouchingPointIndex); + } + } catch (Throwable t) { + // Avoid crashing the application due to unhandled exceptions. + Log.e(TAG, "Exception on the OpenGL thread", t); + } + } + + private void handleMoveEvent(int nowSelectedIndex){ + try { + if (showingTapPointX.size() < 1 || queuedScrollDx.size() < 2) { + // no action, don't move + return; + } + if (nowTouchingPointIndex == DEFAULT_VALUE) { + // no selected cube, don't move + return; + } + if (nowSelectedIndex >= showingTapPointX.size()) { + // wrong index, don't move. + return; + } + float scrollDx = 0; + float scrollDy = 0; + int scrollQueueSize = queuedScrollDx.size(); + for (int i = 0; i < scrollQueueSize; i++) { + scrollDx += queuedScrollDx.poll(); + scrollDy += queuedScrollDy.poll(); + } + + if (isVerticalMode) { + Anchor anchor = anchors.remove(nowSelectedIndex); + anchor.detach(); + setPoseDataToTempArray(getPose(anchor)); +// log(TAG, "point[" + nowSelectedIndex + "] move vertical "+ (scrollDy / viewHeight) + ", tY=" + tempTranslation[1] +// + ", new tY=" + (tempTranslation[1] += (scrollDy / viewHeight))); + tempTranslation[1] += (scrollDy / viewHeight); + anchors.add(nowSelectedIndex, + session.createAnchor(new Pose(tempTranslation, tempRotation))); + } else { + float toX = showingTapPointX.get(nowSelectedIndex) - scrollDx; + showingTapPointX.remove(nowSelectedIndex); + showingTapPointX.add(nowSelectedIndex, toX); + + float toY = showingTapPointY.get(nowSelectedIndex) - scrollDy; + showingTapPointY.remove(nowSelectedIndex); + showingTapPointY.add(nowSelectedIndex, toY); + + if (anchors.size() > nowSelectedIndex) { + Anchor anchor = anchors.remove(nowSelectedIndex); + anchor.detach(); + // remove duplicated anchor + setPoseDataToTempArray(getPose(anchor)); + tempTranslation[0] -= (scrollDx / viewWidth); + tempTranslation[2] -= (scrollDy / viewHeight); + anchors.add(nowSelectedIndex, + session.createAnchor(new Pose(tempTranslation, tempRotation))); + } + } + } catch (NotTrackingException e) { + e.printStackTrace(); + } + } + + private final float[] mPoseTranslation = new float[3]; + private final float[] mPoseRotation = new float[4]; + private Pose getPose(Anchor anchor){ + Pose pose = anchor.getPose(); + pose.getTranslation(mPoseTranslation, 0); + pose.getRotationQuaternion(mPoseRotation, 0); + return new Pose(mPoseTranslation, mPoseRotation); + } + + private void setPoseDataToTempArray(Pose pose){ + pose.getTranslation(tempTranslation, 0); + pose.getRotationQuaternion(tempRotation, 0); + } + + private void drawLine(Pose pose0, Pose pose1, float[] viewmtx, float[] projmtx){ + float lineWidth = 0.002f; + float lineWidthH = lineWidth / viewHeight * viewWidth; + rectRenderer.setVerts( + pose0.tx() - lineWidth, pose0.ty() + lineWidthH, pose0.tz() - lineWidth, + pose0.tx() + lineWidth, pose0.ty() + lineWidthH, pose0.tz() + lineWidth, + pose1.tx() + lineWidth, pose1.ty() + lineWidthH, pose1.tz() + lineWidth, + pose1.tx() - lineWidth, pose1.ty() + lineWidthH, pose1.tz() - lineWidth + , + pose0.tx() - lineWidth, pose0.ty() - lineWidthH, pose0.tz() - lineWidth, + pose0.tx() + lineWidth, pose0.ty() - lineWidthH, pose0.tz() + lineWidth, + pose1.tx() + lineWidth, pose1.ty() - lineWidthH, pose1.tz() + lineWidth, + pose1.tx() - lineWidth, pose1.ty() - lineWidthH, pose1.tz() - lineWidth + ); + + rectRenderer.draw(viewmtx, projmtx); + } + + private void drawObj(Pose pose, ObjectRenderer renderer, float[] cameraView, float[] cameraPerspective, float lightIntensity){ + pose.toMatrix(anchorMatrix, 0); + renderer.updateModelMatrix(anchorMatrix, 1); + renderer.draw(cameraView, cameraPerspective, lightIntensity); + } + + private void checkIfHit(ObjectRenderer renderer, int cubeIndex){ + if(isMVPMatrixHitMotionEvent(renderer.getModelViewProjectionMatrix(), queuedLongPress.peek())){ + // long press hit a cube, show context menu for the cube + nowTouchingPointIndex = cubeIndex; + queuedLongPress.poll(); + showMoreAction(); + showCubeStatus(); + runOnUiThread(new Runnable() { + @Override + public void run() { + fab.performClick(); + } + }); + }else if(isMVPMatrixHitMotionEvent(renderer.getModelViewProjectionMatrix(), queuedSingleTaps.peek())){ + nowTouchingPointIndex = cubeIndex; + queuedSingleTaps.poll(); + showMoreAction(); + showCubeStatus(); + } + } + + private boolean isMVPMatrixHitMotionEvent(float[] ModelViewProjectionMatrix, MotionEvent event){ + if(event == null){ + return false; + } + Matrix.multiplyMV(vertexResult, 0, ModelViewProjectionMatrix, 0, centerVertexOfCube, 0); + /** + * vertexResult = [x, y, z, w] + * + * coordinates in View + * ┌─────────────────────────────────────────┐╮ + * │[0, 0] [viewWidth, 0]│ + * │ [viewWidth/2, viewHeight/2] │view height + * │[0, viewHeight] [viewWidth, viewHeight]│ + * └─────────────────────────────────────────┘╯ + * ╰ view width ╯ + * + * coordinates in GLSurfaceView frame + * ┌─────────────────────────────────────────┐╮ + * │[-1.0, 1.0] [1.0, 1.0]│ + * │ [0, 0] │view height + * │[-1.0, -1.0] [1.0, -1.0]│ + * └─────────────────────────────────────────┘╯ + * ╰ view width ╯ + */ + // circle hit test + float radius = (viewWidth / 2) * (cubeHitAreaRadius/vertexResult[3]); + float dx = event.getX() - (viewWidth / 2) * (1 + vertexResult[0]/vertexResult[3]); + float dy = event.getY() - (viewHeight / 2) * (1 - vertexResult[1]/vertexResult[3]); + double distance = Math.sqrt(dx * dx + dy * dy); +// // for debug +// overlayViewForTest.setPoint("cubeCenter", screenX, screenY); +// overlayViewForTest.postInvalidate(); + return distance < radius; + } + + private double getDistance(Pose pose0, Pose pose1){ + float dx = pose0.tx() - pose1.tx(); + float dy = pose0.ty() - pose1.ty(); + float dz = pose0.tz() - pose1.tz(); + return Math.sqrt(dx * dx + dz * dz + dy * dy); + } + + private void showResult(final String result){ + runOnUiThread(new Runnable() { + @Override + public void run() { + tv_result.setText(result); + } + }); + } + + private void showMoreAction(){ + runOnUiThread(new Runnable() { + @Override + public void run() { + fab.show(); + } + }); + } + + private void hideMoreAction(){ + runOnUiThread(new Runnable() { + @Override + public void run() { + fab.hide(); + } + }); + } + + private void showCubeStatus(){ + runOnUiThread(new Runnable() { + @Override + public void run() { + int nowSelectIndex = glSerfaceRenderer.getNowTouchingPointIndex(); + for(int i = 0; i pointMap = new HashMap<>(); + + public void setPoint(String tag, float x, float y){ + pointMap.put(tag, new Point((int)x, (int)y)); + } + + @Override + protected void onDraw(Canvas canvas) { + if(pointMap == null){ + return; + } + + for(String key : pointMap.keySet()){ + Point point = pointMap.get(key); + canvas.drawCircle(point.x, point.y, 20, paint); + Log.d("OverlayView", "drawCircle"); + } + + } + +} diff --git a/app/src/main/java/com/hl3hl3/arcoremeasure/renderer/LineRenderer.java b/app/src/main/java/com/hl3hl3/arcoremeasure/renderer/LineRenderer.java new file mode 100644 index 0000000..a82f43f --- /dev/null +++ b/app/src/main/java/com/hl3hl3/arcoremeasure/renderer/LineRenderer.java @@ -0,0 +1,161 @@ +package com.hl3hl3.arcoremeasure.renderer; + +import android.opengl.GLES20; +import android.opengl.Matrix; + +import com.google.ar.core.examples.java.helloar.rendering.ShaderUtil; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; + + +public class LineRenderer { + + private final int mProgram; + + private final String vertexShaderCode = + // This matrix member variable provides a hook to manipulate + // the coordinates of the objects that use this vertex shader + "uniform mat4 uMVPMatrix;" + + "attribute vec4 vPosition;" + + "void main() {" + + // the matrix must be included as a modifier of gl_Position + // Note that the uMVPMatrix factor *must be first* in order + // for the matrix multiplication product to be correct. + " gl_Position = uMVPMatrix * vPosition;" + + "}"; + + // Use to access and set the view transformation + private int mMVPMatrixHandle; + + private final String fragmentShaderCode = + "precision mediump float;" + + "uniform vec4 vColor;" + + "void main() {" + + " gl_FragColor = vColor;" + + "}"; + + + private FloatBuffer vertexBuffer; + + static final int COORDS_PER_VERTEX = 3; + static float coordinates[] = { // in counterclockwise order: + 0.0f, 0.0f, 0.0f, // point 1 + 1.0f, 0.0f, 0.0f, // point 2 + }; + + float color[] = {0.63671875f, 0.76953125f, 0.22265625f, 1.0f}; + + private int mPositionHandle; + private int mColorHandle; + + private final int vertexCount = coordinates.length / COORDS_PER_VERTEX; + private final int vertexStride = COORDS_PER_VERTEX * 4; + + private int loadShader(int type, String shaderCode){ + int shader = GLES20.glCreateShader(type); + GLES20.glShaderSource(shader, shaderCode); + GLES20.glCompileShader(shader); + return shader; + } + + public LineRenderer() { + // initialize vertex byte buffer for shape coordinates + // number ofr coordinate values * 4 bytes per float + ByteBuffer bb = ByteBuffer.allocateDirect(coordinates.length * 4); + bb.order(ByteOrder.nativeOrder()); // use the device hardware's native byte order + vertexBuffer = bb.asFloatBuffer(); // create a floating point buffer from the ByteBuffer + vertexBuffer.put(coordinates); // add the coordinate to the FloatBuffer + vertexBuffer.position(0); // set the buffer to read the first coordinate + + int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode); + int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode); + + mProgram = GLES20.glCreateProgram(); // create empty OpenGL ES Program + GLES20.glAttachShader(mProgram, vertexShader); // add the shader to program + GLES20.glAttachShader(mProgram, fragmentShader); + GLES20.glLinkProgram(mProgram); // create OpenGL ES program executables + Matrix.setIdentityM(mModelMatrix, 0); + } + + public void setVerts(float v0, float v1, float v2, float v3, float v4, float v5) { + coordinates[0] = v0; + coordinates[1] = v1; + coordinates[2] = v2; + coordinates[3] = v3; + coordinates[4] = v4; + coordinates[5] = v5; + + vertexBuffer.put(coordinates); + // set the buffer to read the first coordinate + vertexBuffer.position(0); + } + + public void setColor(float red, float green, float blue, float alpha) { + color[0] = red; + color[1] = green; + color[2] = blue; + color[3] = alpha; + } + + // Temporary matrices allocated here to reduce number of allocations for each frame. + private float[] mModelMatrix = new float[16]; + private float[] mModelViewMatrix = new float[16]; + private float[] mModelViewProjectionMatrix = new float[16]; + final String TAG = "Line"; + + public void draw(float[] cameraView, float[] cameraPerspective) { + ShaderUtil.checkGLError(TAG, "Before draw"); + + // Build the ModelView and ModelViewProjection matrices + // for calculating object position and light. + Matrix.multiplyMM(mModelViewMatrix, 0, cameraView, 0, mModelMatrix, 0); + Matrix.multiplyMM(mModelViewProjectionMatrix, 0, cameraPerspective, 0, mModelViewMatrix, 0); + + // add program to OpenGL ES environment + GLES20.glUseProgram(mProgram); + ShaderUtil.checkGLError(TAG, "After glBindBuffer"); + + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); + ShaderUtil.checkGLError(TAG, "After glBindBuffer"); + + // get handle to vertex shader's vPosition member + mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition"); + ShaderUtil.checkGLError(TAG, "After glGetAttribLocation"); + + // enable a handle to the vertices + GLES20.glEnableVertexAttribArray(mPositionHandle); + ShaderUtil.checkGLError(TAG, "After glEnableVertexAttribArray"); + + // prepare the coordinate data + GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX, + GLES20.GL_FLOAT, false, + vertexStride, vertexBuffer); + ShaderUtil.checkGLError(TAG, "After glVertexAttribPointer"); + + // get handle to fragment shader's vColor member + mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor"); + + // set color for drawing the triangle + GLES20.glUniform4fv(mColorHandle, 1, color, 0); + ShaderUtil.checkGLError(TAG, "After glUniform4fv"); + + // get handle to shape's transformation matrix + mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix"); + + // Pass the projection and view transformation to the shader + GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mModelViewProjectionMatrix, 0); + ShaderUtil.checkGLError(TAG, "After glUniformMatrix4fv"); + + // Draw the line + GLES20.glDrawArrays(GLES20.GL_LINES, 0, vertexCount); + ShaderUtil.checkGLError(TAG, "After glDrawArrays"); + + // Disable vertex array + GLES20.glDisableVertexAttribArray(mPositionHandle); + ShaderUtil.checkGLError(TAG, "After draw"); + } + + +} diff --git a/app/src/main/java/com/hl3hl3/arcoremeasure/renderer/RectanglePolygonRenderer.java b/app/src/main/java/com/hl3hl3/arcoremeasure/renderer/RectanglePolygonRenderer.java new file mode 100644 index 0000000..1abfbf6 --- /dev/null +++ b/app/src/main/java/com/hl3hl3/arcoremeasure/renderer/RectanglePolygonRenderer.java @@ -0,0 +1,206 @@ +package com.hl3hl3.arcoremeasure.renderer; + +import android.opengl.GLES20; +import android.opengl.Matrix; + +import com.google.ar.core.examples.java.helloar.rendering.ShaderUtil; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.ShortBuffer; + + +public class RectanglePolygonRenderer { + + private FloatBuffer vertexBuffer; + private ShortBuffer drawListBuffer; + + // number of coordinates pervertex in this array + static final int COORDS_PER_VERTEX = 3; + static float coords[] = { + -0.6f, 0.5f, 0.2f, // top left + -0.4f, -0.5f, 0.2f, // bottom left + 0.5f, -0.5f, 0.2f, // bottom right + 0.5f, 0.5f, 0.2f, // top right + + -0.5f, 0.6f, 0.0f, // top left + -0.5f, -0.8f, 0.0f, // bottom left + 0.5f, -0.5f, 0.0f, // bottom right + 0.5f, 0.5f, 0.0f // top right + }; + private short drawOrder[] = { + 0, 1, 2, 0, 2, 3, + 3, 2, 6, 3, 6, 7, + 4, 5, 6, 4, 6, 7, + 0, 1, 5, 0, 5, 4, + 4, 0, 3, 4, 3, 7, + 5, 1, 2, 5, 2, 6 + }; // order to draw vertex + + float color[] = {0.63671875f, 0.76953125f, 0.22265625f, 1.0f}; + + public void setVerts(float v0, float v1, float v2, + float v3, float v4, float v5, + float v6, float v7, float v8, + float v9, float v10, float v11, + + float v12, float v13, float v14, + float v15, float v16, float v17, + float v18, float v19, float v20, + float v21, float v22, float v23 + ) { + coords[0] = v0; + coords[1] = v1; + coords[2] = v2; + + coords[3] = v3; + coords[4] = v4; + coords[5] = v5; + + coords[6] = v6; + coords[7] = v7; + coords[8] = v8; + + coords[9] = v9; + coords[10] = v10; + coords[11] = v11; + + coords[12] = v12; + coords[13] = v13; + coords[14] = v14; + + coords[15] = v15; + coords[16] = v16; + coords[17] = v17; + + coords[18] = v18; + coords[19] = v19; + coords[20] = v20; + + coords[21] = v21; + coords[22] = v22; + coords[23] = v23; + + vertexBuffer.put(coords); + // set the buffer to read the first coordinate + vertexBuffer.position(0); + } + + public void setColor(float red, float green, float blue, float alpha) { + color[0] = red; + color[1] = green; + color[2] = blue; + color[3] = alpha; + } + + + private final int mProgram; + + private final String vertexShaderCode = + "uniform mat4 uMVPMatrix;" + + "attribute vec4 vPosition;" + + "void main() {" + + " gl_Position = uMVPMatrix * vPosition;" + + "}"; + + // Use to access and set the view transformation + private int mMVPMatrixHandle; + + private final String fragmentShaderCode = + "precision mediump float;" + + "uniform vec4 vColor;" + + "void main() {" + + " gl_FragColor = vColor;" + + "}"; + + private int mPositionHandle; + private int mColorHandle; + + private final int vertexCount = coords.length / COORDS_PER_VERTEX; + private final int vertexStride = COORDS_PER_VERTEX * 4; + + private int loadShader(int type, String shaderCode){ + int shader = GLES20.glCreateShader(type); + GLES20.glShaderSource(shader, shaderCode); + GLES20.glCompileShader(shader); + return shader; + } + + public RectanglePolygonRenderer(){ + // initialize vertex byte buffer for shape coordinates + ByteBuffer bb = ByteBuffer.allocateDirect(coords.length * 4); + bb.order(ByteOrder.nativeOrder()); + vertexBuffer = bb.asFloatBuffer(); + vertexBuffer.put(coords); + vertexBuffer.position(0); + + // initialize byte buffer for the draw list + ByteBuffer dlb = ByteBuffer.allocateDirect(drawOrder.length * 2); // 2 bytes per short + dlb.order(ByteOrder.nativeOrder()); + drawListBuffer = dlb.asShortBuffer(); + drawListBuffer.put(drawOrder); + drawListBuffer.position(0); + + int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode); + int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode); + + // create empty OpenGL ES Program + mProgram = GLES20.glCreateProgram(); + + // add the shader to program + GLES20.glAttachShader(mProgram, vertexShader); + GLES20.glAttachShader(mProgram, fragmentShader); + + // create OpenGL ES program executables + GLES20.glLinkProgram(mProgram); + Matrix.setIdentityM(mModelMatrix, 0); + } + + // Temporary matrices allocated here to reduce number of allocations for each frame. + private float[] mModelMatrix = new float[16]; + private float[] mModelViewMatrix = new float[16]; + private float[] mModelViewProjectionMatrix = new float[16]; + final String TAG = "RectanglePolygon"; + + public void draw(float[] cameraView, float[] cameraPerspective) { + ShaderUtil.checkGLError(TAG, "Before draw"); + + // Build the ModelView and ModelViewProjection matrices + // for calculating object position and light. + Matrix.multiplyMM(mModelViewMatrix, 0, cameraView, 0, mModelMatrix, 0); + Matrix.multiplyMM(mModelViewProjectionMatrix, 0, cameraPerspective, 0, mModelViewMatrix, 0); + + // add program to OpenGL ES environment + GLES20.glUseProgram(mProgram); + + // get handle to vertex shader's vPosition member + mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition"); + + // enable a handle to the triangle vertices + GLES20.glEnableVertexAttribArray(mPositionHandle); + + // prepare the triangle coordinate data + GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX, + GLES20.GL_FLOAT, false, + vertexStride, vertexBuffer); + + // get handle to fragment shader's vColor member + mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor"); + + // set color for drawing the triangle + GLES20.glUniform4fv(mColorHandle, 1, color, 0); + + // get handle to shape's transformation matrix + mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix"); + + // Pass the projection and view transformation to the shader + GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mModelViewProjectionMatrix, 0); + + GLES20.glDrawElements(GLES20.GL_TRIANGLES, drawOrder.length, + GLES20.GL_UNSIGNED_SHORT, drawListBuffer); + + // Disable vertex array + GLES20.glDisableVertexAttribArray(mPositionHandle); + } +} diff --git a/app/src/main/java/com/hl3hl3/arcoremeasure/renderer/SquareRenderer.java b/app/src/main/java/com/hl3hl3/arcoremeasure/renderer/SquareRenderer.java new file mode 100644 index 0000000..393711e --- /dev/null +++ b/app/src/main/java/com/hl3hl3/arcoremeasure/renderer/SquareRenderer.java @@ -0,0 +1,175 @@ +package com.hl3hl3.arcoremeasure.renderer; + +import android.opengl.GLES20; +import android.opengl.Matrix; + +import com.google.ar.core.examples.java.helloar.rendering.ShaderUtil; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.ShortBuffer; + + +public class SquareRenderer { + + private FloatBuffer vertexBuffer; + private ShortBuffer drawListBuffer; + + // number of coordinates pervertex in this array + static final int COORDS_PER_VERTEX = 3; + static float coords[] = { + -0.05f, 0.05f, 0.0f, // top left + -0.05f, -0.05f, 0.0f, // bottom left + 0.05f, -0.05f, 0.0f, // bottom right + 0.05f, 0.05f, 0.0f }; // top right + private short drawOrder[] = {0, 1, 2, 0, 2, 3}; // order to draw vertex + + float color[] = {0.63671875f, 0.76953125f, 0.22265625f, 1.0f}; + + public void setVerts(float v0, float v1, float v2, + float v3, float v4, float v5, + float v6, float v7, float v8, + float v9, float v10, float v11) { + coords[0] = v0; + coords[1] = v1; + coords[2] = v2; + + coords[3] = v3; + coords[4] = v4; + coords[5] = v5; + + coords[6] = v6; + coords[7] = v7; + coords[8] = v8; + + coords[9] = v9; + coords[10] = v10; + coords[11] = v11; + + vertexBuffer.put(coords); + // set the buffer to read the first coordinate + vertexBuffer.position(0); + } + + public void setColor(float red, float green, float blue, float alpha) { + color[0] = red; + color[1] = green; + color[2] = blue; + color[3] = alpha; + } + + + + private final int mProgram; + + private final String vertexShaderCode = + "uniform mat4 uMVPMatrix;" + + "attribute vec4 vPosition;" + + "void main() {" + + " gl_Position = uMVPMatrix * vPosition;" + + "}"; + + // Use to access and set the view transformation + private int mMVPMatrixHandle; + + private final String fragmentShaderCode = + "precision mediump float;" + + "uniform vec4 vColor;" + + "void main() {" + + " gl_FragColor = vColor;" + + "}"; + + private int mPositionHandle; + private int mColorHandle; + + private final int vertexCount = coords.length / COORDS_PER_VERTEX; + private final int vertexStride = COORDS_PER_VERTEX * 4; + + private int loadShader(int type, String shaderCode){ + int shader = GLES20.glCreateShader(type); + GLES20.glShaderSource(shader, shaderCode); + GLES20.glCompileShader(shader); + return shader; + } + + public SquareRenderer(){ + // initialize vertex byte buffer for shape coordinates + ByteBuffer bb = ByteBuffer.allocateDirect(coords.length * 4); + bb.order(ByteOrder.nativeOrder()); + vertexBuffer = bb.asFloatBuffer(); + vertexBuffer.put(coords); + vertexBuffer.position(0); + + // initialize byte buffer for the draw list + ByteBuffer dlb = ByteBuffer.allocateDirect(drawOrder.length * 2); // 2 bytes per short + dlb.order(ByteOrder.nativeOrder()); + drawListBuffer = dlb.asShortBuffer(); + drawListBuffer.put(drawOrder); + drawListBuffer.position(0); + + + + int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode); + int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode); + + // create empty OpenGL ES Program + mProgram = GLES20.glCreateProgram(); + + // add the shader to program + GLES20.glAttachShader(mProgram, vertexShader); + GLES20.glAttachShader(mProgram, fragmentShader); + + // create OpenGL ES program executables + GLES20.glLinkProgram(mProgram); + Matrix.setIdentityM(mModelMatrix, 0); + } + + // Temporary matrices allocated here to reduce number of allocations for each frame. + private float[] mModelMatrix = new float[16]; + private float[] mModelViewMatrix = new float[16]; + private float[] mModelViewProjectionMatrix = new float[16]; + final String TAG = "Rectangle"; + + public void draw(float[] cameraView, float[] cameraPerspective) { // pass in the calculated transformation matrix + + ShaderUtil.checkGLError(TAG, "Before draw"); + + // Build the ModelView and ModelViewProjection matrices + // for calculating object position and light. + Matrix.multiplyMM(mModelViewMatrix, 0, cameraView, 0, mModelMatrix, 0); + Matrix.multiplyMM(mModelViewProjectionMatrix, 0, cameraPerspective, 0, mModelViewMatrix, 0); + + // add program to OpenGL ES environment + GLES20.glUseProgram(mProgram); + + // get handle to vertex shader's vPosition member + mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition"); + + // enable a handle to the triangle vertices + GLES20.glEnableVertexAttribArray(mPositionHandle); + + // prepare the triangle coordinate data + GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX, + GLES20.GL_FLOAT, false, + vertexStride, vertexBuffer); + + // get handle to fragment shader's vColor member + mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor"); + + // set color for drawing the triangle + GLES20.glUniform4fv(mColorHandle, 1, color, 0); + + // get handle to shape's transformation matrix + mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix"); + + // Pass the projection and view transformation to the shader + GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mModelViewProjectionMatrix, 0); + + GLES20.glDrawElements(GLES20.GL_TRIANGLES, drawOrder.length, + GLES20.GL_UNSIGNED_SHORT, drawListBuffer); + + // Disable vertex array + GLES20.glDisableVertexAttribArray(mPositionHandle); + } +} diff --git a/app/src/main/java/drawar/AppSettings.java b/app/src/main/java/drawar/AppSettings.java new file mode 100644 index 0000000..9f91f82 --- /dev/null +++ b/app/src/main/java/drawar/AppSettings.java @@ -0,0 +1,47 @@ +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + package drawar; + +import javax.vecmath.Vector3f; + +public class AppSettings { + private static final Vector3f color = new Vector3f(1f, 1f, 1f); + private static final float strokeDrawDistance = 0.125f; + private static final float minDistance = 0.000625f; + private static final float nearClip = 0.001f; + private static final float farClip = 100.0f; + + + public static float getStrokeDrawDistance() { + return strokeDrawDistance; + } + + public static Vector3f getColor() { + return color; + } + + public static float getMinDistance() { + return minDistance; + } + + public static float getNearClip(){ + return nearClip; + } + public static float getFarClip(){ + return farClip; + } + +} diff --git a/app/src/main/java/drawar/BiquadFilter.java b/app/src/main/java/drawar/BiquadFilter.java new file mode 100644 index 0000000..ec39de2 --- /dev/null +++ b/app/src/main/java/drawar/BiquadFilter.java @@ -0,0 +1,72 @@ +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package drawar; + +import javax.vecmath.Vector3f; + + +/** + * BiquadFilter is a object for easily lowpass filtering incomming values. + */ +public class BiquadFilter { + private Vector3f val = new Vector3f(); + + private BiquadFilterInstance[] inst = new BiquadFilterInstance[3]; + + BiquadFilter(double Fc){ + for(int i=0;i<3;i++){ + inst[i] = new BiquadFilterInstance(Fc); + } + } + + Vector3f update(Vector3f in){ + val.x = (float) inst[0].process(in.x); + val.y = (float) inst[1].process(in.y); + val.z = (float) inst[2].process(in.z); + return val; + } + + private class BiquadFilterInstance { + double a0, a1, a2, b1, b2; + double Fc=0.5,Q=0.707, peakGain=0.0; + double z1=0.0, z2=0.0; + + BiquadFilterInstance(double fc){ + Fc = fc; + calcBiquad(); + } + + double process(double in){ + double out = in * a0 + z1; + z1 = in * a1 + z2 - b1 * out; + z2 = in * a2 - b2 * out; + return out; + } + + void calcBiquad() { + double norm; + double K = Math.tan(Math.PI * Fc); + norm = 1 / (1 + K / Q + K * K); + a0 = K * K * norm; + a1 = 2 * a0; + a2 = a0; + b1 = 2 * (K * K - 1) * norm; + b2 = (1 - K / Q + K * K) * norm; + } + } +} + + diff --git a/app/src/main/java/drawar/DisplayRotationHelper.java b/app/src/main/java/drawar/DisplayRotationHelper.java new file mode 100755 index 0000000..de5a3b4 --- /dev/null +++ b/app/src/main/java/drawar/DisplayRotationHelper.java @@ -0,0 +1,106 @@ +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package drawar; + +import android.app.Activity; +import android.content.Context; +import android.hardware.display.DisplayManager; +import android.hardware.display.DisplayManager.DisplayListener; +import android.view.Display; +import android.view.WindowManager; + +import com.google.ar.core.Session; + +/** + * Helper to track the display rotations. In particular, the 180 degree rotations are not notified + * by the onSurfaceChanged() callback, and thus they require listening to the android display + * events. + */ +public class DisplayRotationHelper implements DisplayListener { + private boolean viewportChanged; + private int viewportWidth; + private int viewportHeight; + private final Context context; + private final Display display; + + /** + * Constructs the DisplayRotationHelper but does not register the listener yet. + * + * @param context the Android {@link Context}. + */ + public DisplayRotationHelper(Context context) { + this.context = context; + display = context.getSystemService(WindowManager.class).getDefaultDisplay(); + } + + + public void onResume() { + context.getSystemService(DisplayManager.class).registerDisplayListener(this, null); + } + + + public void onPause() { + context.getSystemService(DisplayManager.class).unregisterDisplayListener(this); + } + + /** + * Records a change in surface dimensions. This will be later used by {@link + * #updateSessionIfNeeded(Session)}. Should be called from {@link + * android.opengl.GLSurfaceView.Renderer + * #onSurfaceChanged(javax.microedition.khronos.opengles.GL10, int, int)}. + * + * @param width the updated width of the surface. + * @param height the updated height of the surface. + */ + public void onSurfaceChanged(int width, int height) { + viewportWidth = width; + viewportHeight = height; + viewportChanged = true; + } + + /** + * Updates the session display geometry if a change was posted either by {@link + * #onSurfaceChanged(int, int)} call or by {@link #onDisplayChanged(int)} system callback. This + * function should be called explicitly before each call to {@link Session#update()}. This + * function will also clear the 'pending update' (viewportChanged) flag. + * + * @param session the {@link Session} object to update if display geometry changed. + */ + public void updateSessionIfNeeded(Session session) { + if (viewportChanged) { + int displayRotation = display.getRotation(); + session.setDisplayGeometry(displayRotation, viewportWidth, viewportHeight); + viewportChanged = false; + } + } + + /** + * Returns the current rotation state of android display. Same as {@link Display#getRotation()}. + */ + public int getRotation() { + return display.getRotation(); + } + + @Override + public void onDisplayAdded(int displayId) {} + + @Override + public void onDisplayRemoved(int displayId) {} + + @Override + public void onDisplayChanged(int displayId) { + viewportChanged = true; + } +} diff --git a/app/src/main/java/drawar/DrawAR.java b/app/src/main/java/drawar/DrawAR.java new file mode 100755 index 0000000..22ec4a3 --- /dev/null +++ b/app/src/main/java/drawar/DrawAR.java @@ -0,0 +1,739 @@ +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package drawar; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.opengl.GLES20; +import android.opengl.GLSurfaceView; +import android.opengl.Matrix; +import android.os.Bundle; +import android.support.design.widget.Snackbar; +import android.support.v4.view.GestureDetectorCompat; +import android.support.v7.app.AppCompatActivity; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View; +import android.view.WindowManager; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.SeekBar; +import android.widget.Toast; + +import com.google.ar.core.ArCoreApk; +import com.google.ar.core.Camera; +import com.google.ar.core.Config; +import com.google.ar.core.Frame; +import com.google.ar.core.Session; +import com.google.ar.core.TrackingState; +import com.google.ar.core.exceptions.CameraNotAvailableException; +import com.google.ar.core.exceptions.UnavailableApkTooOldException; +import com.google.ar.core.exceptions.UnavailableArcoreNotInstalledException; +import com.google.ar.core.exceptions.UnavailableSdkTooOldException; +import com.google.ar.core.exceptions.UnavailableUserDeclinedInstallationException; + +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; +import javax.vecmath.Vector2f; +import javax.vecmath.Vector3f; + +import drawar.rendering.BackgroundRenderer; +import drawar.rendering.LineShaderRenderer; +import drawar.rendering.LineUtils; +import fr.geolabs.dev.mapmint4me.R; + + +/** + * This is a complex example that shows how to create an augmented reality (AR) application using + * the ARCore API. + */ + +public class DrawAR extends AppCompatActivity implements GLSurfaceView.Renderer, GestureDetector.OnGestureListener, + GestureDetector.OnDoubleTapListener{ + private static final String TAG = DrawAR.class.getSimpleName(); + + private GLSurfaceView mSurfaceView; + + private Config mDefaultConfig; + private Session mSession; + private BackgroundRenderer mBackgroundRenderer = new BackgroundRenderer(); + private LineShaderRenderer mLineShaderRenderer = new LineShaderRenderer(); + private Frame mFrame; + + private float[] projmtx = new float[16]; + private float[] viewmtx = new float[16]; + private float[] mZeroMatrix = new float[16]; + + private boolean mPaused = false; + + private float mScreenWidth = 0; + private float mScreenHeight = 0; + + private BiquadFilter biquadFilter; + private Vector3f mLastPoint; + private AtomicReference lastTouch = new AtomicReference<>(); + + private GestureDetectorCompat mDetector; + + private LinearLayout mSettingsUI; + private LinearLayout mButtonBar; + + private SeekBar mLineWidthBar; + private SeekBar mLineDistanceScaleBar; + private SeekBar mSmoothingBar; + + + private float mLineWidthMax = 0.33f; + private float mDistanceScale = 0.0f; + private float mLineSmoothing = 0.1f; + + private float[] mLastFramePosition; + + private AtomicBoolean bIsTracking = new AtomicBoolean(true); + private AtomicBoolean bReCenterView = new AtomicBoolean(false); + private AtomicBoolean bTouchDown = new AtomicBoolean(false); + private AtomicBoolean bClearDrawing = new AtomicBoolean(false); + private AtomicBoolean bLineParameters = new AtomicBoolean(false); + private AtomicBoolean bUndo = new AtomicBoolean(false); + private AtomicBoolean bNewStroke = new AtomicBoolean(false); + + private ArrayList> mStrokes; + + private DisplayRotationHelper mDisplayRotationHelper; + private Snackbar mMessageSnackbar; + + private boolean bInstallRequested; + + private TrackingState mState; + /** + * Setup the app when main activity is created + */ + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + final SharedPreferences sharedPref = this.getPreferences(Context.MODE_PRIVATE); + + setContentView(R.layout.act_layout); + + mSurfaceView = findViewById(R.id.surfaceview); + mSettingsUI = findViewById(R.id.strokeUI); + mButtonBar = findViewById(R.id.button_bar); + + // Settings seek bars + mLineDistanceScaleBar = findViewById(R.id.distanceScale); + mLineWidthBar = findViewById(R.id.lineWidth); + mSmoothingBar = findViewById(R.id.smoothingSeekBar); + + mLineDistanceScaleBar.setProgress(sharedPref.getInt("mLineDistanceScale", 1)); + mLineWidthBar.setProgress(sharedPref.getInt("mLineWidth", 10)); + mSmoothingBar.setProgress(sharedPref.getInt("mSmoothing", 50)); + + mDistanceScale = LineUtils.map((float) mLineDistanceScaleBar.getProgress(), 0, 100, 1, 200, true); + mLineWidthMax = LineUtils.map((float) mLineWidthBar.getProgress(), 0f, 100f, 0.1f, 5f, true); + mLineSmoothing = LineUtils.map((float) mSmoothingBar.getProgress(), 0, 100, 0.01f, 0.2f, true); + + SeekBar.OnSeekBarChangeListener seekBarChangeListener = new SeekBar.OnSeekBarChangeListener() { + /** + * Listen for seekbar changes, and update the settings + */ + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + SharedPreferences.Editor editor = sharedPref.edit(); + + if (seekBar == mLineDistanceScaleBar) { + editor.putInt("mLineDistanceScale", progress); + mDistanceScale = LineUtils.map((float) progress, 0f, 100f, 1f, 200f, true); + } else if (seekBar == mLineWidthBar) { + editor.putInt("mLineWidth", progress); + mLineWidthMax = LineUtils.map((float) progress, 0f, 100f, 0.1f, 5f, true); + } else if (seekBar == mSmoothingBar) { + editor.putInt("mSmoothing", progress); + mLineSmoothing = LineUtils.map((float) progress, 0, 100, 0.01f, 0.2f, true); + } + mLineShaderRenderer.bNeedsUpdate.set(true); + + editor.apply(); + + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + }; + + mLineDistanceScaleBar.setOnSeekBarChangeListener(seekBarChangeListener); + mLineWidthBar.setOnSeekBarChangeListener(seekBarChangeListener); + mSmoothingBar.setOnSeekBarChangeListener(seekBarChangeListener); + + // Hide the settings ui + mSettingsUI.setVisibility(View.GONE); + + mDisplayRotationHelper = new DisplayRotationHelper(/*context=*/ this); + // Reset the zero matrix + Matrix.setIdentityM(mZeroMatrix, 0); + + mLastPoint = new Vector3f(0, 0, 0); + + bInstallRequested = false; + + // Set up renderer. + mSurfaceView.setPreserveEGLContextOnPause(true); + mSurfaceView.setEGLContextClientVersion(2); + mSurfaceView.setEGLConfigChooser(8, 8, 8, 8, 16, 0); // Alpha used for plane blending. + mSurfaceView.setRenderer(this); + mSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY); + + // Setup touch detector + mDetector = new GestureDetectorCompat(this, this); + mDetector.setOnDoubleTapListener(this); + mStrokes = new ArrayList<>(); + + + } + + + /** + * addStroke adds a new stroke to the scene + * + * @param touchPoint a 2D point in screen space and is projected into 3D world space + */ + private void addStroke(Vector2f touchPoint) { + Vector3f newPoint = LineUtils.GetWorldCoords(touchPoint, mScreenWidth, mScreenHeight, projmtx, viewmtx); + addStroke(newPoint); + } + + + /** + * addPoint adds a point to the current stroke + * + * @param touchPoint a 2D point in screen space and is projected into 3D world space + */ + private void addPoint(Vector2f touchPoint) { + Vector3f newPoint = LineUtils.GetWorldCoords(touchPoint, mScreenWidth, mScreenHeight, projmtx, viewmtx); + addPoint(newPoint); + } + + + /** + * addStroke creates a new stroke + * + * @param newPoint a 3D point in world space + */ + private void addStroke(Vector3f newPoint) { + biquadFilter = new BiquadFilter(mLineSmoothing); + for (int i = 0; i < 1500; i++) { + biquadFilter.update(newPoint); + } + Vector3f p = biquadFilter.update(newPoint); + mLastPoint = new Vector3f(p); + mStrokes.add(new ArrayList()); + mStrokes.get(mStrokes.size() - 1).add(mLastPoint); + } + + /** + * addPoint adds a point to the current stroke + * + * @param newPoint a 3D point in world space + */ + private void addPoint(Vector3f newPoint) { + if (LineUtils.distanceCheck(newPoint, mLastPoint)) { + Vector3f p = biquadFilter.update(newPoint); + mLastPoint = new Vector3f(p); + mStrokes.get(mStrokes.size() - 1).add(mLastPoint); + } + } + + + /** + * onResume part of the Android Activity Lifecycle + */ + @Override + protected void onResume() { + super.onResume(); + + if (mSession == null) { + Exception exception = null; + String message = null; + try { + switch (ArCoreApk.getInstance().requestInstall(this, !bInstallRequested)) { + case INSTALL_REQUESTED: + bInstallRequested = true; + return; + case INSTALLED: + break; + } + + // ARCore requires camera permissions to operate. If we did not yet obtain runtime + // permission on Android M and above, now is a good time to ask the user for it. + if (!PermissionHelper.hasCameraPermission(this)) { + PermissionHelper.requestCameraPermission(this); + return; + } + + mSession = new Session(/* context= */ this); + } catch (UnavailableArcoreNotInstalledException + | UnavailableUserDeclinedInstallationException e) { + message = "Please install ARCore"; + exception = e; + } catch (UnavailableApkTooOldException e) { + message = "Please update ARCore"; + exception = e; + } catch (UnavailableSdkTooOldException e) { + message = "Please update this app"; + exception = e; + } catch (Exception e) { + message = "This device does not support AR"; + exception = e; + } + + if (message != null) { + Log.e(TAG, "Exception creating session", exception); + return; + } + + // Create default config and check if supported. + Config config = new Config(mSession); + if (!mSession.isSupported(config)) { + Log.e(TAG, "Exception creating session Device Does Not Support ARCore", exception); + } + mSession.configure(config); + } + // Note that order matters - see the note in onPause(), the reverse applies here. + try { + mSession.resume(); + } catch (CameraNotAvailableException e) { + e.printStackTrace(); + } + mSurfaceView.onResume(); + mDisplayRotationHelper.onResume(); + mPaused = false; + } + + /** + * onPause part of the Android Activity Lifecycle + */ + @Override + public void onPause() { + super.onPause(); + // Note that the order matters - GLSurfaceView is paused first so that it does not try + // to query the session. If Session is paused before GLSurfaceView, GLSurfaceView may + // still call mSession.update() and get a SessionPausedException. + + if (mSession != null) { + mDisplayRotationHelper.onPause(); + mSurfaceView.onPause(); + mSession.pause(); + } + + mPaused = false; + + + DisplayMetrics displayMetrics = new DisplayMetrics(); + getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); + mScreenHeight = displayMetrics.heightPixels; + mScreenWidth = displayMetrics.widthPixels; + } + + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] results) { + if (!PermissionHelper.hasCameraPermission(this)) { + Toast.makeText(this, + "Camera permission is needed to run this application", Toast.LENGTH_LONG).show(); + finish(); + } + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + if (hasFocus) { + // Standard Android full-screen functionality. + getWindow().getDecorView().setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + } + + + /** + * Create renderers after the Surface is Created and on the GL Thread + */ + @Override + public void onSurfaceCreated(GL10 gl, EGLConfig config) { + + if (mSession == null) { + return; + } + + GLES20.glClearColor(0.1f, 0.1f, 0.1f, 1.0f); + + // Create the texture and pass it to ARCore session to be filled during update(). + mBackgroundRenderer.createOnGlThread(/*context=*/this); + + try { + + mSession.setCameraTextureName(mBackgroundRenderer.getTextureId()); + mLineShaderRenderer.createOnGlThread(this); + + } catch (Exception e) { + e.printStackTrace(); + } + } + + + @Override + public void onSurfaceChanged(GL10 gl, int width, int height) { + GLES20.glViewport(0, 0, width, height); + // Notify ARCore session that the view size changed so that the perspective matrix and + // the video background can be properly adjusted. + mDisplayRotationHelper.onSurfaceChanged(width, height); + mScreenWidth = width; + mScreenHeight = height; + } + + + /** + * update() is executed on the GL Thread. + * The method handles all operations that need to take place before drawing to the screen. + * The method : + * extracts the current projection matrix and view matrix from the AR Pose + * handles adding stroke and points to the data collections + * updates the ZeroMatrix and performs the matrix multiplication needed to re-center the drawing + * updates the Line Renderer with the current strokes, color, distance scale, line width etc + */ + private void update() { + + if (mSession == null) { + return; + } + + mDisplayRotationHelper.updateSessionIfNeeded(mSession); + + try { + + mSession.setCameraTextureName(mBackgroundRenderer.getTextureId()); + + mFrame = mSession.update(); + Camera camera = mFrame.getCamera(); + + mState = camera.getTrackingState(); + + // Update tracking states + if (mState == TrackingState.TRACKING && !bIsTracking.get()) { + bIsTracking.set(true); + } else if (mState== TrackingState.STOPPED && bIsTracking.get()) { + bIsTracking.set(false); + bTouchDown.set(false); + } + + // Get projection matrix. + camera.getProjectionMatrix(projmtx, 0, AppSettings.getNearClip(), AppSettings.getFarClip()); + camera.getViewMatrix(viewmtx, 0); + + float[] position = new float[3]; + camera.getPose().getTranslation(position, 0); + + // Check if camera has moved much, if thats the case, stop touchDown events + // (stop drawing lines abruptly through the air) + if (mLastFramePosition != null) { + Vector3f distance = new Vector3f(position[0], position[1], position[2]); + distance.sub(new Vector3f(mLastFramePosition[0], mLastFramePosition[1], mLastFramePosition[2])); + + if (distance.length() > 0.15) { + bTouchDown.set(false); + } + } + mLastFramePosition = position; + + // Multiply the zero matrix + Matrix.multiplyMM(viewmtx, 0, viewmtx, 0, mZeroMatrix, 0); + + + if (bNewStroke.get()) { + bNewStroke.set(false); + addStroke(lastTouch.get()); + mLineShaderRenderer.bNeedsUpdate.set(true); + } else if (bTouchDown.get()) { + addPoint(lastTouch.get()); + mLineShaderRenderer.bNeedsUpdate.set(true); + } + + if (bReCenterView.get()) { + bReCenterView.set(false); + mZeroMatrix = getCalibrationMatrix(); + } + + if (bClearDrawing.get()) { + bClearDrawing.set(false); + clearDrawing(); + mLineShaderRenderer.bNeedsUpdate.set(true); + } + + if (bUndo.get()) { + bUndo.set(false); + if (mStrokes.size() > 0) { + mStrokes.remove(mStrokes.size() - 1); + mLineShaderRenderer.bNeedsUpdate.set(true); + } + } + mLineShaderRenderer.setDrawDebug(bLineParameters.get()); + if (mLineShaderRenderer.bNeedsUpdate.get()) { + mLineShaderRenderer.setColor(AppSettings.getColor()); + mLineShaderRenderer.mDrawDistance = AppSettings.getStrokeDrawDistance(); + mLineShaderRenderer.setDistanceScale(mDistanceScale); + mLineShaderRenderer.setLineWidth(mLineWidthMax); + mLineShaderRenderer.clear(); + mLineShaderRenderer.updateStrokes(mStrokes); + mLineShaderRenderer.upload(); + } + + } catch (Exception e) { + e.printStackTrace(); + } + } + + + + + /** + * GL Thread Loop + * clears the Color Buffer and Depth Buffer, draws the current texture from the camera + * and draws the Line Renderer if ARCore is tracking the world around it + */ + @Override + public void onDrawFrame(GL10 gl) { + if (mPaused) return; + + update(); + + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT); + + if (mFrame == null) { + return; + } + + // Draw background. + mBackgroundRenderer.draw(mFrame); + + // Draw Lines + if (mFrame.getCamera().getTrackingState() == TrackingState.TRACKING) { + mLineShaderRenderer.draw(viewmtx, projmtx, mScreenWidth, mScreenHeight, AppSettings.getNearClip(), AppSettings.getFarClip()); + } + } + + + /** + * Get a matrix usable for zero calibration (only position and compass direction) + */ + public float[] getCalibrationMatrix() { + float[] t = new float[3]; + float[] m = new float[16]; + + mFrame.getCamera().getPose().getTranslation(t, 0); + float[] z = mFrame.getCamera().getPose().getZAxis(); + Vector3f zAxis = new Vector3f(z[0], z[1], z[2]); + zAxis.y = 0; + zAxis.normalize(); + + double rotate = Math.atan2(zAxis.x, zAxis.z); + + Matrix.setIdentityM(m, 0); + Matrix.translateM(m, 0, t[0], t[1], t[2]); + Matrix.rotateM(m, 0, (float) Math.toDegrees(rotate), 0, 1, 0); + return m; + } + + + /** + * Clears the Datacollection of Strokes and sets the Line Renderer to clear and update itself + * Designed to be executed on the GL Thread + */ + public void clearDrawing() { + mStrokes.clear(); + mLineShaderRenderer.clear(); + } + + + /** + * onClickUndo handles the touch input on the GUI and sets the AtomicBoolean bUndo to be true + * the actual undo functionality is executed in the GL Thread + */ + public void onClickUndo(View button) { + bUndo.set(true); + } + + /** + * onClickLineDebug toggles the Line Renderer's Debug View on and off. The line renderer will + * highlight the lines on the same depth plane to allow users to draw things more coherently + */ + public void onClickLineDebug(View button) { + bLineParameters.set(!bLineParameters.get()); + } + + + /** + * onClickSettings toggles showing and hiding the Line Width, Smoothing, and Debug View toggle + */ + public void onClickSettings(View button) { + ImageButton settingsButton = findViewById(R.id.settingsButton); + + if (mSettingsUI.getVisibility() == View.GONE) { + mSettingsUI.setVisibility(View.VISIBLE); + mLineDistanceScaleBar = findViewById(R.id.distanceScale); + mLineWidthBar = findViewById(R.id.lineWidth); + + settingsButton.setColorFilter(getResources().getColor(R.color.active)); + } else { + mSettingsUI.setVisibility(View.GONE); + settingsButton.setColorFilter(getResources().getColor(R.color.gray)); + } + } + + /** + * onClickClear handle showing an AlertDialog to clear the drawing + */ + public void onClickClear(View button) { + + AlertDialog.Builder builder = new AlertDialog.Builder(this) + .setMessage("Sure you want to clear?"); + + // Set up the buttons + builder.setPositiveButton("Clear ", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + bClearDrawing.set(true); + } + }); + builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + } + }); + + builder.show(); + } + + + /** + * onClickRecenter handles the touch input on the GUI and sets the AtomicBoolean bReCEnterView to be true + * the actual recenter functionality is executed on the GL Thread + */ + public void onClickRecenter(View button) { + bReCenterView.set(true); + } + + // ------- Touch events + + /** + * onTouchEvent handles saving the lastTouch screen position and setting bTouchDown and bNewStroke + * AtomicBooleans to trigger addPoint and addStroke on the GL Thread to be called + */ + @Override + public boolean onTouchEvent(MotionEvent tap) { + this.mDetector.onTouchEvent(tap); + + if (tap.getAction() == MotionEvent.ACTION_DOWN ) { + lastTouch.set(new Vector2f(tap.getX(), tap.getY())); + bTouchDown.set(true); + bNewStroke.set(true); + return true; + } else if (tap.getAction() == MotionEvent.ACTION_MOVE || tap.getAction() == MotionEvent.ACTION_POINTER_DOWN) { + lastTouch.set(new Vector2f(tap.getX(), tap.getY())); + bTouchDown.set(true); + return true; + } else if (tap.getAction() == MotionEvent.ACTION_UP || tap.getAction() == MotionEvent.ACTION_CANCEL) { + bTouchDown.set(false); + lastTouch.set(new Vector2f(tap.getX(), tap.getY())); + return true; + } + + return super.onTouchEvent(tap); + } + + + @Override + public boolean onSingleTapConfirmed(MotionEvent e) { + return false; + } + + /** + * onDoubleTap shows and hides the Button Bar at the Top of the View + */ + @Override + public boolean onDoubleTap(MotionEvent e) { + if (mButtonBar.getVisibility() == View.GONE) { + mButtonBar.setVisibility(View.VISIBLE); + } else { + mButtonBar.setVisibility(View.GONE); + } + return false; + } + + @Override + public boolean onDoubleTapEvent(MotionEvent e) { + return false; + } + + @Override + public boolean onDown(MotionEvent e) { + return false; + } + + @Override + public void onShowPress(MotionEvent tap) { + } + + @Override + public boolean onSingleTapUp(MotionEvent e) { + return false; + } + + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + return false; + } + + @Override + public void onLongPress(MotionEvent e) { + + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + return false; + } + +} diff --git a/app/src/main/java/drawar/PermissionHelper.java b/app/src/main/java/drawar/PermissionHelper.java new file mode 100755 index 0000000..f234556 --- /dev/null +++ b/app/src/main/java/drawar/PermissionHelper.java @@ -0,0 +1,46 @@ +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package drawar; + +import android.Manifest; +import android.app.Activity; +import android.content.pm.PackageManager; +import android.support.v4.app.ActivityCompat; +import android.support.v4.content.ContextCompat; + +/** + * Helper to ask camera permission. + */ +public class PermissionHelper { + private static final String CAMERA_PERMISSION = Manifest.permission.CAMERA; + private static final int CAMERA_PERMISSION_CODE = 0; + + /** + * Check to see we have the necessary permissions for this app. + */ + public static boolean hasCameraPermission(Activity activity) { + return ContextCompat.checkSelfPermission(activity, CAMERA_PERMISSION) == + PackageManager.PERMISSION_GRANTED; + + } + + /** + * Check to see we have the necessary permissions for this app, and ask for them if we don't. + */ + public static void requestCameraPermission(Activity activity) { + ActivityCompat.requestPermissions(activity, new String[]{CAMERA_PERMISSION}, + CAMERA_PERMISSION_CODE); + } +} diff --git a/app/src/main/java/drawar/package-info.java b/app/src/main/java/drawar/package-info.java new file mode 100755 index 0000000..7fee246 --- /dev/null +++ b/app/src/main/java/drawar/package-info.java @@ -0,0 +1,17 @@ +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package drawar; diff --git a/app/src/main/java/drawar/rendering/BackgroundRenderer.java b/app/src/main/java/drawar/rendering/BackgroundRenderer.java new file mode 100755 index 0000000..b8e07ba --- /dev/null +++ b/app/src/main/java/drawar/rendering/BackgroundRenderer.java @@ -0,0 +1,184 @@ +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package drawar.rendering; + +import android.content.Context; +import android.opengl.GLES11Ext; +import android.opengl.GLES20; +import android.opengl.GLSurfaceView; + +import com.google.ar.core.Frame; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; + +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; + +import fr.geolabs.dev.mapmint4me.R; + +/** + * This class renders the AR background from camera feed. It creates and hosts the texture + * given to ARCore to be filled with the camera image. + */ +public class BackgroundRenderer { + private static final String TAG = BackgroundRenderer.class.getSimpleName(); + + private static final int COORDS_PER_VERTEX = 3; + private static final int TEXCOORDS_PER_VERTEX = 2; + private static final int FLOAT_SIZE = 4; + + private FloatBuffer mQuadVertices; + private FloatBuffer mQuadTexCoord; + private FloatBuffer mQuadTexCoordTransformed; + + private int mQuadProgram; + + private int mQuadPositionParam; + private int mQuadTexCoordParam; + private int mTextureId = -1; + private int mTextureTarget = GLES11Ext.GL_TEXTURE_EXTERNAL_OES; + public BackgroundRenderer() { + } + + /** + * @return + */ + public int getTextureId() { + return mTextureId; + } + + /** + * Allocates and initializes OpenGL resources needed by the background renderer. Must be + * called on the OpenGL thread, typically in + * {@link GLSurfaceView.Renderer#onSurfaceCreated(GL10, EGLConfig)}. + * + * @param context Needed to access shader source. + */ + public void createOnGlThread(Context context) { + // Generate the background texture. + int textures[] = new int[1]; + GLES20.glGenTextures(1, textures, 0); + mTextureId = textures[0]; + GLES20.glBindTexture(mTextureTarget, mTextureId); + GLES20.glTexParameteri(mTextureTarget, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameteri(mTextureTarget, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameteri(mTextureTarget, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST); + GLES20.glTexParameteri(mTextureTarget, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST); + + int numVertices = 4; + if (numVertices != QUAD_COORDS.length / COORDS_PER_VERTEX) { + throw new RuntimeException("Unexpected number of vertices in BackgroundRenderer."); + } + + ByteBuffer bbVertices = ByteBuffer.allocateDirect(QUAD_COORDS.length * FLOAT_SIZE); + bbVertices.order(ByteOrder.nativeOrder()); + mQuadVertices = bbVertices.asFloatBuffer(); + mQuadVertices.put(QUAD_COORDS); + mQuadVertices.position(0); + + ByteBuffer bbTexCoords = ByteBuffer.allocateDirect( + numVertices * TEXCOORDS_PER_VERTEX * FLOAT_SIZE); + bbTexCoords.order(ByteOrder.nativeOrder()); + mQuadTexCoord = bbTexCoords.asFloatBuffer(); + mQuadTexCoord.put(QUAD_TEXCOORDS); + mQuadTexCoord.position(0); + + ByteBuffer bbTexCoordsTransformed = ByteBuffer.allocateDirect( + numVertices * TEXCOORDS_PER_VERTEX * FLOAT_SIZE); + bbTexCoordsTransformed.order(ByteOrder.nativeOrder()); + mQuadTexCoordTransformed = bbTexCoordsTransformed.asFloatBuffer(); + + int vertexShader = ShaderUtil.loadGLShader(TAG, context, + GLES20.GL_VERTEX_SHADER, R.raw.screenquad_vertex); + int fragmentShader = ShaderUtil.loadGLShader(TAG, context, + GLES20.GL_FRAGMENT_SHADER, R.raw.screenquad_fragment_oes); + + mQuadProgram = GLES20.glCreateProgram(); + GLES20.glAttachShader(mQuadProgram, vertexShader); + GLES20.glAttachShader(mQuadProgram, fragmentShader); + GLES20.glLinkProgram(mQuadProgram); + GLES20.glUseProgram(mQuadProgram); + + ShaderUtil.checkGLError(TAG, "Program creation"); + + mQuadPositionParam = GLES20.glGetAttribLocation(mQuadProgram, "a_Position"); + mQuadTexCoordParam = GLES20.glGetAttribLocation(mQuadProgram, "a_TexCoord"); + + ShaderUtil.checkGLError(TAG, "Program parameters"); + } + + + public void draw(Frame frame) { + + if (frame == null) { + return; + } + + // If display rotation changed (also includes view size change), we need to re-query the uv + // coordinates for the screen rect, as they may have changed as well. + if (frame.hasDisplayGeometryChanged()) { + frame.transformDisplayUvCoords(mQuadTexCoord, mQuadTexCoordTransformed); + } + + // No need to test or write depth, the screen quad has arbitrary depth, and is expected + // to be drawn first. + GLES20.glDisable(GLES20.GL_DEPTH_TEST); + GLES20.glDepthMask(false); + + GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureId); + + GLES20.glUseProgram(mQuadProgram); + + // Set the vertex positions. + GLES20.glVertexAttribPointer( + mQuadPositionParam, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, 0, mQuadVertices); + + // Set the texture coordinates. + GLES20.glVertexAttribPointer(mQuadTexCoordParam, TEXCOORDS_PER_VERTEX, + GLES20.GL_FLOAT, false, 0, mQuadTexCoordTransformed); + + // Enable vertex arrays + GLES20.glEnableVertexAttribArray(mQuadPositionParam); + GLES20.glEnableVertexAttribArray(mQuadTexCoordParam); + + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + + // Disable vertex arrays + GLES20.glDisableVertexAttribArray(mQuadPositionParam); + GLES20.glDisableVertexAttribArray(mQuadTexCoordParam); + + // Restore the depth state for further drawing. + GLES20.glDepthMask(true); + GLES20.glEnable(GLES20.GL_DEPTH_TEST); + + ShaderUtil.checkGLError(TAG, "Draw"); + } + + public static final float[] QUAD_COORDS = new float[]{ + -1.0f, -1.0f, 0.0f, + -1.0f, +1.0f, 0.0f, + +1.0f, -1.0f, 0.0f, + +1.0f, +1.0f, 0.0f, + }; + + public static final float[] QUAD_TEXCOORDS = new float[]{ + 0.0f, 1.0f, + 0.0f, 0.0f, + 1.0f, 1.0f, + 1.0f, 0.0f, + }; +} diff --git a/app/src/main/java/drawar/rendering/LineShaderRenderer.java b/app/src/main/java/drawar/rendering/LineShaderRenderer.java new file mode 100644 index 0000000..f4dddfe --- /dev/null +++ b/app/src/main/java/drawar/rendering/LineShaderRenderer.java @@ -0,0 +1,537 @@ +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package drawar.rendering; + + +import android.content.Context; +import android.opengl.GLES20; +import android.opengl.GLSurfaceView; +import android.opengl.Matrix; +import android.util.Log; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; +import javax.vecmath.Vector3f; + +import fr.geolabs.dev.mapmint4me.R; + + +/** + * Renders a point cloud. + */ +public class LineShaderRenderer { + + + private static final String TAG = LineShaderRenderer.class.getSimpleName(); + private static final int FLOATS_PER_POINT = 3; // X,Y,Z. + private static final int BYTES_PER_FLOAT = 4; + private static final int BYTES_PER_POINT = BYTES_PER_FLOAT * FLOATS_PER_POINT; + + private float[] mModelMatrix = new float[16]; + private float[] mModelViewMatrix = new float[16]; + private float[] mModelViewProjectionMatrix = new float[16]; + + + private int mPositionAttribute = 0; + private int mPreviousAttribute = 0; + private int mNextAttribute = 0; + private int mSideAttribute = 0; + private int mWidthAttribte = 0; + + private int mCountersAttribute = 0; + + private int mProjectionUniform = 0; + private int mModelViewUniform = 0; + private int mResolutionUniform = 0; + private int mLineWidthUniform = 0; + private int mColorUniform = 0; + private int mOpacityUniform = 0; + private int mNearUniform = 0; + private int mFarUniform = 0; + private int mSizeAttenuationUniform = 0; + private int mDrawModeUniform = 0; + private int mNearCutoffUniform = 0; + private int mFarCutoffUniform = 0; + + private boolean mDrawMode = false; + + private int mVisibility = 0; + private int mAlphaTest = 0; + + + private float[] mPositions; + private float[] mCounters; + private float[] mNext; + private float[] mSide; + private float[] mWidth; + private float[] mPrevious; + + private int mPositionAddress; + private int mPreviousAddress; + private int mNextAddress; + private int mSideAddress; + private int mWidthAddress; + private int mCounterAddress; + + + private int mNumPoints = 0; + private int mNumBytes = 0; + + private int mVbo = 0; + private int mVboSize = 0; + + private int mProgramName = 0; + private float lineWidth = 0; + + + private Vector3f mColor; + + + public AtomicBoolean bNeedsUpdate = new AtomicBoolean(); + + private int mLineDepthScaleUniform; + private float mLineDepthScale = 10.0f; + + public float mDrawDistance; + + public LineShaderRenderer() { + + } + + /** + * Allocates and initializes OpenGL resources needed by the Line renderer. Must be + * called on the OpenGL thread, typically in + * {@link GLSurfaceView.Renderer#onSurfaceCreated(GL10, EGLConfig)}. + * + * @param context Needed to access shader source. + */ + public void createOnGlThread(Context context) { + ShaderUtil.checkGLError(TAG, "before create"); + + int buffers[] = new int[1]; + GLES20.glGenBuffers(1, buffers, 0); + mVbo = buffers[0]; + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mVbo); + mVboSize = 0; + GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, mVboSize, null, GLES20.GL_DYNAMIC_DRAW); + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); + + + ShaderUtil.checkGLError(TAG, "buffer alloc"); + + + /** + * + * The LineShaderRenderer uses an ES2 pipeline. It uses the line_vert.glsl and + * line_frag.glsl shader to render a volumetric line. It uses several techniques detailed in + * the following resources: + * + * Drawing Lines is Hard by Matt DesLauriers + * https://mattdesl.svbtle.com/drawing-lines-is-hard + * + * InkSpace an Android Experiment by Zach Lieberman + * https://experiments.withgoogle.com/android/ink-space + * https://github.com/ofZach/inkSpace + * + * THREEJS.MeshLine by Jaume Sanchez + * https://github.com/spite/THREE.MeshLine/blob/master/src/THREE.MeshLine.js + * + * + * The Renderer batches all of the geometry into a single VBO. This allows us to have a single + * draw call to render the geometry. We also optimize the application to only re-upload the + * geometry data when a new stroke or new points are added to the drawing. The renderer uses + * a technique detailed in the following link to create degenerate faces between the strokes + * to disconnect them from one another. + * https://developer.apple.com/library/content/documentation/3DDrawing/Conceptual/OpenGLES_ProgrammingGuide/TechniquesforWorkingwithVertexData/TechniquesforWorkingwithVertexData.html + * + */ + + int vertexShader = ShaderUtil.loadGLShader(TAG, context, + GLES20.GL_VERTEX_SHADER, R.raw.line_vert); + int fragmentShader = ShaderUtil.loadGLShader(TAG, context, + GLES20.GL_FRAGMENT_SHADER, R.raw.line_frag); + + + mProgramName = GLES20.glCreateProgram(); + GLES20.glAttachShader(mProgramName, vertexShader); + GLES20.glAttachShader(mProgramName, fragmentShader); + GLES20.glLinkProgram(mProgramName); + GLES20.glUseProgram(mProgramName); + + ShaderUtil.checkGLError(TAG, "program"); + + mPositionAttribute = GLES20.glGetAttribLocation(mProgramName, "position"); + mPreviousAttribute = GLES20.glGetAttribLocation(mProgramName, "previous"); + mNextAttribute = GLES20.glGetAttribLocation(mProgramName, "next"); + mSideAttribute = GLES20.glGetAttribLocation(mProgramName, "side"); + mWidthAttribte = GLES20.glGetAttribLocation(mProgramName, "width"); + mCountersAttribute = GLES20.glGetAttribLocation(mProgramName, "counters"); + mProjectionUniform = GLES20.glGetUniformLocation(mProgramName, "projectionMatrix"); + mModelViewUniform = GLES20.glGetUniformLocation(mProgramName, "modelViewMatrix"); + mResolutionUniform = GLES20.glGetUniformLocation(mProgramName, "resolution"); + mLineWidthUniform = GLES20.glGetUniformLocation(mProgramName, "lineWidth"); + mColorUniform = GLES20.glGetUniformLocation(mProgramName, "color"); + mOpacityUniform = GLES20.glGetUniformLocation(mProgramName, "opacity"); + mNearUniform = GLES20.glGetUniformLocation(mProgramName, "near"); + mFarUniform = GLES20.glGetUniformLocation(mProgramName, "far"); + mSizeAttenuationUniform = GLES20.glGetUniformLocation(mProgramName, "sizeAttenuation"); + mVisibility = GLES20.glGetUniformLocation(mProgramName, "visibility"); + mAlphaTest = GLES20.glGetUniformLocation(mProgramName, "alphaTest"); + mDrawModeUniform = GLES20.glGetUniformLocation(mProgramName, "drawMode"); + mNearCutoffUniform = GLES20.glGetUniformLocation(mProgramName, "nearCutOff"); + mFarCutoffUniform = GLES20.glGetUniformLocation(mProgramName, "farCutOff"); + mLineDepthScaleUniform = GLES20.glGetUniformLocation(mProgramName, "lineDepthScale"); + + ShaderUtil.checkGLError(TAG, "program params"); + + Matrix.setIdentityM(mModelMatrix, 0); + + mColor = new Vector3f(1f, 1f, 1f); + lineWidth = 0.5f; + } + + /** + * Sets the LineWidth of the Line. + * Requires bNeedsUpdate.set(true) to take effect + * @param width + */ + public void setLineWidth(float width){ + lineWidth = width; + } + + /** + * Enables or Disables the Debug View in the Fragment Shader. Debug View highlights the strokes + * at the same depth as the user. It allows the user to position new drawings to intersect or + * bypass the existing strokes + * @param drawDebugMode + */ + public void setDrawDebug(boolean drawDebugMode){ + mDrawMode = drawDebugMode; + } + + + /** + * This sets the color of the line by setting the color uniform in the shader. + * @param color a Vector3f representing R, G, B for the X, Y, Z values + */ + public void setColor(Vector3f color) { + mColor = new Vector3f(color); + } + + + /** + * This sets a feature in the vertex shader to scale the line width based on the distance away + * from the current view. + * @param distanceScale + */ + public void setDistanceScale(float distanceScale) { + this.mLineDepthScale = distanceScale; + } + + /** + * This updates the geometry data to be rendered. It ensures the capacity of the float arrays + * and then calls addLine to generate the geometry. + * @param strokes a ArrayList of ArrayLists of Vector3fs in world space. The outer ArrayList + * contains the strokes, while the inner ArrayList contains the Vertex of each Line + */ + public void updateStrokes(ArrayList> strokes) { + mNumPoints = 0; + for (ArrayList l : strokes) { + mNumPoints += l.size()*2 + 2; + } + + ensureCapacity(mNumPoints); + + int offset = 0; + for (ArrayList l : strokes) { + offset = addLine(l, offset); + } + mNumBytes = offset; + + } + + /** + * This ensures the capacity of the float arrays that hold the information bound to the Vertex + * Attributes needed to render the line with the Vertex and Fragment shader. + * @param numPoints + */ + private void ensureCapacity(int numPoints){ + int count = 1024; + if(mSide != null){ + count = mSide.length; + } + + while(count < numPoints){ + count += 1024; + } + + if(mSide == null || mSide.length < count) { + Log.i(TAG, "alloc "+count); + mPositions = new float[count*3]; + mNext = new float[count*3]; + mPrevious = new float[count*3]; + + mCounters = new float[count]; + mSide = new float[count]; + mWidth = new float[count]; + } + } + + /** + * AddLine takes in the 3D positions adds to the buffers to create the stroke and the degenerate + * faces needed so the lines render properly. + * @param line + * @param offset + * @return + */ + private int addLine(List line, int offset) { + if (line == null || line.size() < 2) + return offset; + + + + + int lineSize = line.size(); + + + int ii = offset; + for (int i = 0; i < lineSize; i++) { + + int iGood = i; + if (iGood < 0) iGood = 0; + if (iGood >= lineSize) iGood = lineSize - 1; + + int i_m_1 = (iGood - 1) < 0 ? iGood : iGood - 1; + int i_p_1 = (iGood + 1) > (lineSize - 1) ? iGood : iGood + 1; + float c = ((float) i / lineSize); + + + Vector3f current = line.get(iGood); + Vector3f previous = line.get(i_m_1); + Vector3f next = line.get(i_p_1); + + + + if (i == 0) { + setMemory(ii++, current, previous, next, c, lineWidth, 1f); + } + + setMemory(ii++, current, previous, next, c, lineWidth, 1f); + setMemory(ii++, current, previous, next, c, lineWidth, -1f); + + if (i == lineSize - 1) { + setMemory(ii++, current, previous, next, c, lineWidth, -1f); + } + } + return ii; + } + + /** + * + * setMemory is a helper method used to add the stroke data to the float[] buffers + * @param index + * @param pos + * @param prev + * @param next + * @param counter + * @param width + * @param side + */ + private void setMemory(int index, Vector3f pos, Vector3f prev, Vector3f next, float counter, float width, float side){ + mPositions[index*3] = pos.x; + mPositions[index*3+1] = pos.y; + mPositions[index*3+2] = pos.z; + + mNext[index*3] = next.x; + mNext[index*3+1] = next.y; + mNext[index*3+2] = next.z; + + mPrevious[index*3] = prev.x; + mPrevious[index*3+1] = prev.y; + mPrevious[index*3+2] = prev.z; + + mCounters[index] = counter; + mSide[index] = side; + mWidth[index] = width; + + } + + /** + * Sets the bNeedsUpdate to true. + */ + public void clear() { + bNeedsUpdate.set(true); + } + + + /** + * This takes the float[] and creates FloatBuffers, Binds the VBO, and upload the Attributes to + * correct locations with the correct offsets so the Vertex and Fragment shader can render the lines + */ + public void upload() { + bNeedsUpdate.set(false); + + FloatBuffer current = toFloatBuffer(mPositions); + FloatBuffer next = toFloatBuffer(mNext); + FloatBuffer previous = toFloatBuffer(mPrevious); + + FloatBuffer side = toFloatBuffer(mSide); + FloatBuffer width = toFloatBuffer(mWidth); + FloatBuffer counter = toFloatBuffer(mCounters); + + +// mNumPoints = mPositions.length; + + mPositionAddress = 0; + mNextAddress = mPositionAddress + mNumBytes * 3 * BYTES_PER_FLOAT; + mPreviousAddress = mNextAddress + mNumBytes * 3 * BYTES_PER_FLOAT; + mSideAddress = mPreviousAddress + mNumBytes *3 * BYTES_PER_FLOAT; + + mWidthAddress = mSideAddress + mNumBytes * BYTES_PER_FLOAT; + mCounterAddress = mWidthAddress + mNumBytes * BYTES_PER_FLOAT; + mVboSize = mCounterAddress + mNumBytes * BYTES_PER_FLOAT; + + ShaderUtil.checkGLError(TAG, "before update"); + + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mVbo); + + GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, mVboSize, null, GLES20.GL_DYNAMIC_DRAW); + + GLES20.glBufferSubData(GLES20.GL_ARRAY_BUFFER, mPositionAddress, mNumBytes * 3 * BYTES_PER_FLOAT, + current); + GLES20.glBufferSubData(GLES20.GL_ARRAY_BUFFER, mNextAddress, mNumBytes * 3 * BYTES_PER_FLOAT, + next); + GLES20.glBufferSubData(GLES20.GL_ARRAY_BUFFER, mPreviousAddress, mNumBytes * 3 * BYTES_PER_FLOAT, + previous); + GLES20.glBufferSubData(GLES20.GL_ARRAY_BUFFER, mSideAddress, mNumBytes * BYTES_PER_FLOAT, + side); + GLES20.glBufferSubData(GLES20.GL_ARRAY_BUFFER, mWidthAddress, mNumBytes * BYTES_PER_FLOAT, + width); + GLES20.glBufferSubData(GLES20.GL_ARRAY_BUFFER, mCounterAddress, mNumBytes * BYTES_PER_FLOAT, + counter); + + + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); + + ShaderUtil.checkGLError(TAG, "after update"); + } + + + /** + * + * This method takes in the current CameraView Matrix and the Camera's Projection Matrix, the + * current position and pose of the device, uses those to calculate the ModelViewMatrix and + * ModelViewProjectionMatrix. It binds the VBO, enables the custom attribute locations, + * binds and uploads the shader uniforms, calls our single DrawArray call, and finally disables + * and unbinds the shader attributes and VBO. + * + * @param cameraView + * @param cameraPerspective + * @param screenWidth + * @param screenHeight + * @param nearClip + * @param farClip + */ + public void draw(float[] cameraView, float[] cameraPerspective, float screenWidth, float screenHeight, float nearClip, float farClip) { + + + Matrix.multiplyMM(mModelViewMatrix, 0, cameraView, 0, mModelMatrix, 0); + Matrix.multiplyMM(mModelViewProjectionMatrix, 0, cameraPerspective, 0, mModelViewMatrix, 0); + + ShaderUtil.checkGLError(TAG, "Before draw"); + + GLES20.glUseProgram(mProgramName); + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mVbo); + GLES20.glVertexAttribPointer( + mPositionAttribute, FLOATS_PER_POINT, GLES20.GL_FLOAT, false, BYTES_PER_POINT, mPositionAddress); + GLES20.glVertexAttribPointer( + mPreviousAttribute, FLOATS_PER_POINT, GLES20.GL_FLOAT, false, BYTES_PER_POINT, mPreviousAddress); + GLES20.glVertexAttribPointer( + mNextAttribute, FLOATS_PER_POINT, GLES20.GL_FLOAT, false, BYTES_PER_POINT, mNextAddress); + GLES20.glVertexAttribPointer( + mSideAttribute, 1, GLES20.GL_FLOAT, false, BYTES_PER_FLOAT, mSideAddress); + GLES20.glVertexAttribPointer( + mWidthAttribte, 1, GLES20.GL_FLOAT, false, BYTES_PER_FLOAT, mWidthAddress); + GLES20.glVertexAttribPointer( + mCountersAttribute, 1, GLES20.GL_FLOAT, false, BYTES_PER_FLOAT, mCounterAddress); + GLES20.glUniformMatrix4fv( + mModelViewUniform, 1, false, mModelViewMatrix, 0); + GLES20.glUniformMatrix4fv( + mProjectionUniform, 1, false, cameraPerspective, 0); + + + GLES20.glUniform2f(mResolutionUniform, screenWidth, screenHeight); + GLES20.glUniform1f(mLineWidthUniform, 0.01f); + GLES20.glUniform3f(mColorUniform, mColor.x, mColor.y, mColor.z); + GLES20.glUniform1f(mOpacityUniform, 1.0f); + GLES20.glUniform1f(mNearUniform, nearClip); + GLES20.glUniform1f(mFarUniform, farClip); + GLES20.glUniform1f(mSizeAttenuationUniform, 1.0f); + GLES20.glUniform1f(mVisibility, 1.0f); + GLES20.glUniform1f(mAlphaTest, 1.0f); + GLES20.glUniform1f(mDrawModeUniform, mDrawMode?1.0f:0.0f); + GLES20.glUniform1f(mNearCutoffUniform, mDrawDistance - 0.0075f); + GLES20.glUniform1f(mFarCutoffUniform, mDrawDistance + 0.0075f); + GLES20.glUniform1f(mLineDepthScaleUniform, mLineDepthScale); + + GLES20.glEnableVertexAttribArray(mPositionAttribute); + GLES20.glEnableVertexAttribArray(mPreviousAttribute); + GLES20.glEnableVertexAttribArray(mNextAttribute); + GLES20.glEnableVertexAttribArray(mSideAttribute); + GLES20.glEnableVertexAttribArray(mWidthAttribte); + GLES20.glEnableVertexAttribArray(mCountersAttribute); + + + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, mNumBytes); + + + GLES20.glDisableVertexAttribArray(mCountersAttribute); + GLES20.glDisableVertexAttribArray(mWidthAttribte); + GLES20.glDisableVertexAttribArray(mSideAttribute); + GLES20.glDisableVertexAttribArray(mNextAttribute); + GLES20.glDisableVertexAttribArray(mPreviousAttribute); + GLES20.glDisableVertexAttribArray(mPositionAttribute); + + + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); + + ShaderUtil.checkGLError(TAG, "Draw"); + } + + + /** + * A helper function to allocate a FloatBuffer the size of our float[] and copy the float[] into + * the newly created FloatBuffer. + * @param data + * @return + */ + private FloatBuffer toFloatBuffer(float[] data) { + FloatBuffer buff; + ByteBuffer bb = ByteBuffer.allocateDirect(data.length * BYTES_PER_FLOAT); + bb.order(ByteOrder.nativeOrder()); + buff = bb.asFloatBuffer(); + buff.put(data); + buff.position(0); + return buff; + } + +} diff --git a/app/src/main/java/drawar/rendering/LineUtils.java b/app/src/main/java/drawar/rendering/LineUtils.java new file mode 100644 index 0000000..614c763 --- /dev/null +++ b/app/src/main/java/drawar/rendering/LineUtils.java @@ -0,0 +1,130 @@ +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + package drawar.rendering; + +import android.opengl.Matrix; + +import javax.vecmath.Vector2f; +import javax.vecmath.Vector3f; + +import drawar.AppSettings; + +public class LineUtils { + + /** + * @param value + * @param inputMin + * @param inputMax + * @param outputMin + * @param outputMax + * @param clamp + * @return + */ + public static float map(float value, float inputMin, float inputMax, float outputMin, float outputMax, boolean clamp) { + float outVal = ((value - inputMin) / (inputMax - inputMin) * (outputMax - outputMin) + outputMin); + + if (clamp) { + if (outputMax < outputMin) { + if (outVal < outputMax) outVal = outputMax; + else if (outVal > outputMin) outVal = outputMin; + } else { + if (outVal > outputMax) outVal = outputMax; + else if (outVal < outputMin) outVal = outputMin; + } + } + return outVal; + } + + /** + * @param start + * @param stop + * @param amt + * @return + */ + public static float lerp(float start, float stop, float amt) { + return start + (stop - start) * amt; + } + + /** + * @param touchPoint + * @param screenWidth + * @param screenHeight + * @param projectionMatrix + * @param viewMatrix + * @return + */ + public static Vector3f GetWorldCoords(Vector2f touchPoint, float screenWidth, float screenHeight, float[] projectionMatrix, float[] viewMatrix) { + Ray touchRay = projectRay(touchPoint, screenWidth, screenHeight, projectionMatrix, viewMatrix); + touchRay.direction.scale(AppSettings.getStrokeDrawDistance()); + touchRay.origin.add(touchRay.direction); + return touchRay.origin; + } + + /** + * @param point + * @param viewportSize + * @param viewProjMtx + * @return + */ + public static Ray screenPointToRay(Vector2f point, Vector2f viewportSize, float[] viewProjMtx) { + point.y = viewportSize.y - point.y; + float x = point.x * 2.0F / viewportSize.x - 1.0F; + float y = point.y * 2.0F / viewportSize.y - 1.0F; + float[] farScreenPoint = new float[]{x, y, 1.0F, 1.0F}; + float[] nearScreenPoint = new float[]{x, y, -1.0F, 1.0F}; + float[] nearPlanePoint = new float[4]; + float[] farPlanePoint = new float[4]; + float[] invertedProjectionMatrix = new float[16]; + Matrix.setIdentityM(invertedProjectionMatrix, 0); + Matrix.invertM(invertedProjectionMatrix, 0, viewProjMtx, 0); + Matrix.multiplyMV(nearPlanePoint, 0, invertedProjectionMatrix, 0, nearScreenPoint, 0); + Matrix.multiplyMV(farPlanePoint, 0, invertedProjectionMatrix, 0, farScreenPoint, 0); + Vector3f direction = new Vector3f(farPlanePoint[0] / farPlanePoint[3], farPlanePoint[1] / farPlanePoint[3], farPlanePoint[2] / farPlanePoint[3]); + Vector3f origin = new Vector3f(new Vector3f(nearPlanePoint[0] / nearPlanePoint[3], nearPlanePoint[1] / nearPlanePoint[3], nearPlanePoint[2] / nearPlanePoint[3])); + direction.sub(origin); + direction.normalize(); + return new Ray(origin, direction); + } + + /** + * @param touchPoint + * @param screenWidth + * @param screenHeight + * @param projectionMatrix + * @param viewMatrix + * @return + */ + public static Ray projectRay(Vector2f touchPoint, float screenWidth, float screenHeight, float[] projectionMatrix, float[] viewMatrix) { + float[] viewProjMtx = new float[16]; + Matrix.multiplyMM(viewProjMtx, 0, projectionMatrix, 0, viewMatrix, 0); + return screenPointToRay(touchPoint, new Vector2f(screenWidth, screenHeight), viewProjMtx); + } + + + /** + * @param newPoint + * @param lastPoint + * @return + */ + public static boolean distanceCheck(Vector3f newPoint, Vector3f lastPoint) { + Vector3f temp = new Vector3f(); + temp.sub(newPoint, lastPoint); + if (temp.length() > AppSettings.getMinDistance()) { + return true; + } + return false; + } +} diff --git a/app/src/main/java/drawar/rendering/Ray.java b/app/src/main/java/drawar/rendering/Ray.java new file mode 100644 index 0000000..223c584 --- /dev/null +++ b/app/src/main/java/drawar/rendering/Ray.java @@ -0,0 +1,27 @@ +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package drawar.rendering; + +import javax.vecmath.Vector3f; + +public class Ray { + public final Vector3f origin; + public final Vector3f direction; + public Ray(Vector3f origin, Vector3f direction) { + this.origin = origin; + this.direction = direction; + } +} \ No newline at end of file diff --git a/app/src/main/java/drawar/rendering/ShaderUtil.java b/app/src/main/java/drawar/rendering/ShaderUtil.java new file mode 100755 index 0000000..0192731 --- /dev/null +++ b/app/src/main/java/drawar/rendering/ShaderUtil.java @@ -0,0 +1,97 @@ +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package drawar.rendering; + +import android.content.Context; +import android.opengl.GLES20; +import android.util.Log; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +/** + * Shader helper functions. + */ +public class ShaderUtil { + /** + * Converts a raw text file, saved as a resource, into an OpenGL ES shader. + * + * @param type The type of shader we will be creating. + * @param resId The resource ID of the raw text file about to be turned into a shader. + * @return The shader object handler. + */ + public static int loadGLShader(String tag, Context context, int type, int resId) { + String code = readRawTextFile(context, resId); + int shader = GLES20.glCreateShader(type); + GLES20.glShaderSource(shader, code); + GLES20.glCompileShader(shader); + + // Get the compilation status. + final int[] compileStatus = new int[1]; + GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compileStatus, 0); + + // If the compilation failed, delete the shader. + if (compileStatus[0] == 0) { + Log.e(tag, "Error compiling shader: " + GLES20.glGetShaderInfoLog(shader)); + GLES20.glDeleteShader(shader); + shader = 0; + } + + if (shader == 0) { + throw new RuntimeException("Error creating shader."); + } + + return shader; + } + + /** + * Checks if we've had an error inside of OpenGL ES, and if so what that error is. + * + * @param label Label to report in case of error. + * @throws RuntimeException If an OpenGL error is detected. + */ + public static void checkGLError(String tag, String label) { + int error; + while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) { + Log.e(tag, label + ": glError " + error); + throw new RuntimeException(label + ": glError " + error); + } + } + + /** + * Converts a raw text file into a string. + * + * @param resId The resource ID of the raw text file about to be turned into a shader. + * @return The context of the text file, or null in case of error. + */ + private static String readRawTextFile(Context context, int resId) { + InputStream inputStream = context.getResources().openRawResource(resId); + try { + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); + } + reader.close(); + return sb.toString(); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } +} diff --git a/app/src/main/java/drawar/rendering/package-info.java b/app/src/main/java/drawar/rendering/package-info.java new file mode 100755 index 0000000..08169a6 --- /dev/null +++ b/app/src/main/java/drawar/rendering/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * This package contains classes that do the rendering for this example. + */ +package drawar.rendering; diff --git a/app/src/main/java/fr/geolabs/dev/mapmint4me/MapMint4ME.java b/app/src/main/java/fr/geolabs/dev/mapmint4me/MapMint4ME.java index c81766d..0a7bdd9 100644 --- a/app/src/main/java/fr/geolabs/dev/mapmint4me/MapMint4ME.java +++ b/app/src/main/java/fr/geolabs/dev/mapmint4me/MapMint4ME.java @@ -293,6 +293,31 @@ public boolean shouldOverrideUrlLoading(WebView webView, String webResourceReque } } + public void OnButtonClickS(View V) + { + + Toast.makeText(this, "AR Scale", Toast.LENGTH_SHORT).show(); + Intent i = new Intent(MapMint4ME.this, com.hl3hl3.arcoremeasure.ArMeasureActivity.class); + startActivity(i); + + } + public void OnButtonClickD(View V) + { + + Toast.makeText(this, "AR Draw", Toast.LENGTH_SHORT).show(); + Intent i = new Intent(MapMint4ME.this, drawar.DrawAR.class); + startActivity(i); + } + + + + + + + + + + private String channel_name="MapMint4ME"; private String channel_description="MapMint4ME channel used for notification"; public String CHANNEL_ID="MapMint4ME-11223344"; diff --git a/app/src/main/res/drawable/bg_btn.xml b/app/src/main/res/drawable/bg_btn.xml new file mode 100644 index 0000000..a72158f --- /dev/null +++ b/app/src/main/res/drawable/bg_btn.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/circle.xml b/app/src/main/res/drawable/circle.xml new file mode 100644 index 0000000..d3ed938 --- /dev/null +++ b/app/src/main/res/drawable/circle.xml @@ -0,0 +1,6 @@ + + + + + /> + diff --git a/app/src/main/res/drawable/draw.png b/app/src/main/res/drawable/draw.png new file mode 100755 index 0000000000000000000000000000000000000000..374d91c7358bb5fae9ad41706eb64f6612e59d87 GIT binary patch literal 66984 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%0AOq0xIaG{d)w|0``m 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�{YR9R@Y8E|@fOsx%P3dy-!dmBdVP47fP zQoj&?BTnD?xI#Ugz_3JyZbB?YXBx*5$cypFtbiZ*Ye3^g$^6o?B(gL>N-+jy-Q7DG2wXr0xLj(yR-$Xj0PF5jkZqf@`nwuTxz5J#$6vLj_8{! zQKPk1rBP;K%8bw8ro7(gxmXo@E3zyz<1d&x?k-M(64SjoAe_s^t_{MTS zBZsq#Y(gdC3#MB9u0?+~AuvIgfXvCkGaqkaL`M8lqEv;?_#;anA1Hk$!*Zg+_I(|J zRUe?Wf+Evx>`(kf*xv9}_}QB<5Zkk=*M%y<@)JEx#LYNng8dCMsoi_hqJXG=@|EBNdUle zio5tGy6QH=lmEz_5W4V(zmdA>kG}IB+HmeSuq;*w@yXUFaip{>{G?fPmLDZmX*s_Cbv_Fv(dHv@USwbTGHD`ZQB9D(Bb6)Sh>X4g>Dl3)-O^@T`tN-`E04RwZZPB$SF0K?; zf|=*|JWqkfvUsK~61&Tp@DqBA_^S_Jh~bt97ORn+0Re~g5{ths0$CBYV|M4JetdbI zSmNu8^UvK=Br&FdHQt}+TUoqV#R#ZqQ%lQDJ;&dfnuaoMz{r;C@g?3FJC3B->06Yn zV+y1R!LYad`TMyOc`_EmHcFOTO8k9~p5R#X@-?i@h0&mFDuY`L5w*u-i4MBY3ihAh zx2P86llh(YcpxUzuzQTJxbunw*0z!*@}|7*Ss8;H$tJVa#cD5fbc zX8A}5VCEJ-3z^KQGAo&jHu1BUNJ9BMT2EchcZSVREFjRJTuM=kk6HfYik(r0^2C;642%~m5J}NibrL=Azu{{)b8M+q1ePz z#ZDu4t+VHrBeReEPLXl@K1o3P4#pZC9 zg8tVVd%woVMAM6lH_>h*uJ+j&zhNNFimwI{p}eA>{ozC|3~| z^%#OMbp}uanK4PO?sLM+lJV42p^A0Zmf5^ny?+JF�n>Q@#%2)40_w{s$02@4ian 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/ic_check_empty.png b/app/src/main/res/drawable/ic_check_empty.png new file mode 100644 index 0000000000000000000000000000000000000000..2b9db14d196d9762784d378dc4c18b741df02106 GIT binary patch literal 286 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpUtr=Bj3Ar-gY-rUc1$UxxuM?Iwu zi&Ylo+9JQyzjRJ{v^j#S^y2ofd|Fo?Tw+-#yU%{d#)oz)FDG~lIdMeh&RS+UHMMf} z>s`~Uq{H9O+5N1tjwdV5&MrLks_um!{+|5(3N1A^^zQ!IUZU>AaJWR!(#!w!r~b=1 z4cmL2r=|Wj@!c+_TK@il@*>6{>DBYwm4FVy1O`lHT^lrNkM*$m$d{*Xyxw83`o;0s z$S3=zJuj8rbcRjDwJraNIlc#Vj S<61cnWVolRpUXO@geCx9A8rQ# literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/ic_check_pressed.png b/app/src/main/res/drawable/ic_check_pressed.png new file mode 100644 index 0000000000000000000000000000000000000000..dd38cd87ec4b29b735d99d634b465b1dce97a39b GIT binary patch literal 14925 zcmeI3ZA?>F7{?E=7p>E&WYNuN>P6>_wzsc9@7f|zw5UY}IMBH0<@T1|(c4>lFH#T_ z!jh?(Z$XW2XtpJD;!D67O_pfXWtkg`Sr%o^>0T0tGZL2x+Xpe(+_`OO52bi?@#AS| z`*6`4!_%%M{UL^xCAzOWC zfDaB#2Oxi;P*tPUc$PDqB@CFIC{w*dq)~Ao2>UjfjDu%tRbiG%o{VwV9^0QHfIT zpbFK3+8R%}wpa@DTB|-+#~IBgZJteUwAxJhmW5iA!I(=MEVR+4GnyIrXEA8wFUqNb zkv;77F%_=G@pAA#2jy3k5JS_Ajg9(7vtA0 zaq%)27D9?31+{8kwpMCT92BJ%O1$FNC594(f^wW4q)12D5N*^O=yWD8m+*ud!hz_} zyd2F3c##h(GV~eK`a*t5kz~I#M#)6?n1$h-csz;PX~!#yX-msW$p(lZjx?=Pu4)SL zbOkR<4PlNi*#IvkC*9R6f-gBaBQmKyV~(AV2xHjPo|rAV6k;=Ehu6v!hj~_!!c~$K zaK=_y`7lClak09pta_{VH4n=PL6z)$JDqAv&UrE;?_w3+3Eg=*gH313uQFN~Lq21& zEr6c}1Ck`BOv&Mqyn?SOO%9`#F&fYuh%z+?+(TYgVIM0Cszvg6ImRc2MOJYNB3sAP zpaYa`fQyP>NHpua%E+=fz3xb#7^3C2{uf?jf zTJuagi_Kusu_m)g=kr;tY_89iXU&a4^mtJFsMIcrYfyJgJhk@;QzQ0KsaJ#rc-pZ6 zM5B)WNlT*Ey)+ZI?L666cia ziSAy8<JqtVX?AGM)ksiLZ}lgN@!X=KBE zVI5o(kGtPvnMYbvOW0}kS0EsaoQzoO$T^B`eYk2zHK+!(kX;MmycJQ(ftsJ3L~ zn7&4x*FZy5pJy__7j0BO<5sjg)w=4@R|q`I#L9sfsF|+EFx4uaG^A?F~NmJgbD~Qlm<2?xUh&&0l|gR zz{Ug@77;2SxKJ9{nBc-9LIngDN&_1cTv$Y?fZ#%DU}J&{iwG4ETqq4}OmJZlp#p*n zrGbqJE-WHcKyaZnura}fMT80nE|dl~Cb+POPyxY((!j<97ZwpJAh=K(*qGqLB0>cO z7fJ&g6I@tCsDR)?X<%c53yTO95L_q?Y)o)r5upNt3#EaL2`(%mR6uZ{G_Wzjg++u4 z2riTcHYT{Rh)@B+h0?&rkHn=(J_5)G;d6hD@BzPXrL2qa;Xp0tuJ8cRJQsl0cK{fA z2*2+D&;Xx0>|X}}vmbzIQv2F37Xy&dQR*tJid?_@{^xIJ8YjP7xAU~)y$6~i?ZbkN z_a?4hy>wuv>+g(Hdz?jY-1}rjJsmvup75iX+o2nOsp)pdKkYBPY@4<7@~vIZ>;h9S z6g)e5O3z;{A03rf{(Q85b|{c>X`6A=lp~WCg~zSCQgm}#;ouk3uXJaf@V`DpflK}1 zZf!>S_afiArM;lDe|E;4)?f4bH5X3p26KLxvyo~)(k~wd?yh;ed$zZ9Z|hq&4}jxk zd-qMfkkh)U7t~!}bG*K1$@8I-j_z-koZouPpFMNYO7UzKxR$j)Gcw`h?bWT7gDWWN zw}Jaz8rNIm*^C2oKYVFz-o>BVTcnB^-Qeo=&2nqz&BFx))#BL&O50~O`hzY0E4{Ct zql@=63?B02eAT&ZUb~~c+j&~JJ1ca@b@KMobHQ6P&V0Rw8S4FXpyS%1-L%+tYU-qe zrnP%tygXrV!{EV|UB0gS+0N7Bf7$+^tGW5jQXpn3|E}NHXQEo0cu@aG=b@hEL*5&G zP0mhh?a8Y<-1-~eZSC99*VcF9>X!MQ`Pqvy-JtNM45s^vZ06qb-{F*k(vmV)SJAq* F{{Z@lVG95N literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/ic_checked.png b/app/src/main/res/drawable/ic_checked.png new file mode 100644 index 0000000000000000000000000000000000000000..e3ce43f98032049aa1e417fe7de1fe1fb60744c8 GIT binary patch literal 15654 zcmeI3TWl0n7{?DSlDY~3#7n#}E+871oy*SbW!x^*F4zi-wJo;Q2+Yo$X(!#?8F!}b zwlNU|BR(kTgUIE97!yzvVt7y^M&pC>W{4yt_5l+^LjV&Jqlv_XdS#AQ0Pxi3W^cdSpWdoyRxYIKRtAQOIUDr`AhDrntJ*MhnG75>^R2<_XTA+G zrrsLtlhb_KZiho=$3y}4Ol<4bCWf`R9^8-&B#H_Okb|zu6m!|UqZC_%rML?E?lp5k zrqsn9ZVk4137P(M7t?MPAR~t&tR{p-CK?Y3a$Jl_>lu+3A{;MqLYx)C3i^|Hrt%6V z1L#|#pc_heYI7wWvRZ>fu4^kCH#RmF8ViT4!XT2z;~Xz?qR65itTUc>)gqgBR+WQP zau1b3Ws;*V&wXAHi zyvw?(2$}YFZ&%47nRz0uYG&Re`(%Q{W%V_$M}#TWg-N82vV5EsW4(f;@G(Vl&Fb}8z+SsPOGgHTU$)!9`W{!gnnzq&|Q4?lb56FIX! zR?GVAdYbTVGrU@i>VjITfkJ|t)mGm(<^KI%P0fy!`bR|`y*R;puSorFTslCO`rWv+ zuH2qC-K1E_hBf-+hH;y%VBga#hYj^ZyB6KfFZCJS&Zt-{ngjzvHYqMLAIwW} zVL-?x#YN_Wc_}Uo2-&2#$b2v_#f1SOn-mwB59Xz~Fd$@;;v)0Gyc8D(gltk=WImXe z;=+KCO^S=m2lG-~7!a~aagq68UWy9?LN+NbG9S!KabZBnCdEbOgLx?~3<%kzxX64k zFU5rcA)6EznGfcrxG*4Olj0)t!Mqd~283)JkWGq<%m?#QTo@3tNpX?+ zU|xy~141?_E;1j?OL1X9$R@=_=7V`DE({3Sq`1g@FfYZ00U?`2T!H$-xG;~Nz#T)6 z+Wr*~rqE-$jMmYe24G?h0K4}BaAO*MUIAbPJ^y=k7XZo!0NiiAxAXhWsPEOzR9kOx z^0&iVmh9#4PMzJq>Z3!~POZIm>d?9|JNh!S@Z@)+%Rddjv|2jw^{e3d?fQ@VE#@#zl>x-6xFJ9bl9{=6Eck+GiFt{-F zOW!-(`HfRs9)Dx&Og;(9bWNuyeuDUr+aZbG$8aVEN>;M@Njyf6LQH z&K{a*TDlu7Y-v8YXX4S(Wj8-wi^g+KUWukTJ>3jet7~@1JDX1+6LfCcmOAzHuGjwo DFTMaz literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/ic_cube.xml b/app/src/main/res/drawable/ic_cube.xml new file mode 100644 index 0000000..de64e59 --- /dev/null +++ b/app/src/main/res/drawable/ic_cube.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_cube_disable.png b/app/src/main/res/drawable/ic_cube_disable.png new file mode 100644 index 0000000000000000000000000000000000000000..47ded7e68de9e84fd99fd3312fcc2ab25e74cdb1 GIT binary patch literal 1255 zcmZ9Mc~Db#5XXNnFT6y2NbpJ;KmxST2nse94hu{|Le^Dc0cpm{q8@zv-yDmJ|@OY zV*r4OpD!c_gvKl}C?MD$`1|`W@FB0?Lbo)tcd>C4H9dX@06EBywSxRh8OmR-)yxA=+V#x4cfvlB4C}R~Gi^2@&P)08UaTYW>%Q>h( z725(>7Ys#R+Z0E`%DVBsffjkyp5+nh@tVjY?fp&HpBV&u>%e&0(mESOIrfxz{-8YwCO88>Y0son)>W={oD@>ysgBCF*x^Mrid*sF&w;|~m^U=Q(Tty;x54(eclab# zZS1Za(nT)8_9wp?+L0tr>#(eOklTDFbT<^*YP<4+vOTqX&tkQof)5w8B+)(5kokMf zO~Kwu5c~J`#RWdiaJDaq*a4*Ca_E}M!~LwZn8fO7a?s{;sKLsdWC+rD0`G%%^>-zrNaSS{C;JkuQC`bO3VVH?IYE z!aLvBDnyDi9gxWn@ai3BMo`vmX*D~vcq%;y9^HSrwEna`AJKV(hKpo2{>O8?wW3=- ze(+@ixPI|cbZ*ZH5d>(25Ey`pQO@?Tli!iQt%O~Sg35p~U`_k~`q~yBGY4GcL)RO8 zlKkWa;J7|rw+W*r3>QNSVTKAXg-`(?03goz7mPN#H3FT0(3zB7)}+rBEfBFXGb6xb zy~+iLsDfEjpzi}WK)C0(80SCDXZeQ^RUy#;KSZpODaC^5is$WlAiSrK&!Xi3JqSZb+q2l7iFPpo!1Xa!nCq|sVogB$rV;? zn{oi2lN8q2PPdJZRzdOPMSFUt#sVlsry(~I<%K#)nIgEXFU$-my$Ll6U#snb*PJ_P zLlHvcxQUtZ3$VPgVHpd1-#Ojzu~)!OcPCM*tWf@F?FxOBp62%SNx05eEkt_j|MXsv z7PAB}GR$#Wt3(BTpK4xzvi%=gMMK#rKBsh*^~1QuP-f6Fo@Job<|E=X%Lu<;Y_&{z zgRpChngT@G)i$GengQXraTl?&lkrN#Lz83hkM=aAuFsiTChuVTG(N_wh{i9Kf7 MyaISNo`TGO0rOcIEC2ui literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/ic_cube_green.png b/app/src/main/res/drawable/ic_cube_green.png new file mode 100644 index 0000000000000000000000000000000000000000..93c637e61796867e17d7199ff4110618a3dba104 GIT binary patch literal 6922 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/ic_cube_pink.png b/app/src/main/res/drawable/ic_cube_pink.png new file mode 100644 index 0000000000000000000000000000000000000000..4f0650e7b3eb1eb86a5b544afafeccd5ce2dee7b GIT binary patch literal 7852 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 + + diff --git a/app/src/main/res/drawable/ic_launcher.xml b/app/src/main/res/drawable/ic_launcher.xml new file mode 100644 index 0000000..9d69828 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_my_location_black_24dp.xml b/app/src/main/res/drawable/ic_my_location_black_24dp.xml new file mode 100644 index 0000000..07d6e46 --- /dev/null +++ b/app/src/main/res/drawable/ic_my_location_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings_black_24dp.xml b/app/src/main/res/drawable/ic_settings_black_24dp.xml new file mode 100644 index 0000000..ace746c --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_undo_black_24dp.xml b/app/src/main/res/drawable/ic_undo_black_24dp.xml new file mode 100644 index 0000000..5558e37 --- /dev/null +++ b/app/src/main/res/drawable/ic_undo_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/scale.jpg b/app/src/main/res/drawable/scale.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4a77860a991e969314757374528b26766a892686 GIT binary patch literal 37749 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 + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/act_layout.xml b/app/src/main/res/layout/act_layout.xml new file mode 100644 index 0000000..9e47cd6 --- /dev/null +++ b/app/src/main/res/layout/act_layout.xml @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100755 index 0000000..b26d0ea --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_map_mint4_me.xml b/app/src/main/res/layout/activity_map_mint4_me.xml index 98de9ee..74a2626 100644 --- a/app/src/main/res/layout/activity_map_mint4_me.xml +++ b/app/src/main/res/layout/activity_map_mint4_me.xml @@ -9,7 +9,32 @@ + + + + ARScale + + +

+ + + \ No newline at end of file diff --git a/app/src/main/assets/index.html b/app/src/main/assets/index.html index dcde1b2..a217011 100644 --- a/app/src/main/assets/index.html +++ b/app/src/main/assets/index.html @@ -142,6 +142,7 @@ + '; + console.log(tmpStr); + return tmpStr; + } + if(definedSqlTypes[i]["code"]=="geometry"){ + console.log("CID: "+cid+" select type from mm4me_gc where f_table_schema||'_'||f_table_name = (select name from mm4me_tables where id="+cid+") ") + var geoType=getGeometryType("(select replace(name,'.','_') from mm4me_tables where id="+cid+")"); + var viewb=''; + var res=' '; + if(geoType=='POINT' || geoType=='MULTIPOINT' ) + return res+''+ + '

GPS Informations

Type
Coords
'+ + ''; + else if(geoType=='LINESTRING' || geoType=='MULTILINESTRING' ) + return res+''+ + ''+ + '
'+viewb+'
'; + else if(geoType=='POLYGON' || geoType=='MULTIPOLYGON' ) + return res+''+ + ''+ + '
'+viewb+'
'; + + } + if(definedSqlTypes[i]["code"]=="tbl_linked"){ + var tmp=obj["value"].split(';'); + //console.log(cleanupTableName('SELECT '+tmp[1]+' FROM '+tmp[2]+' WHERE '+tmp[0]+'='+tblName+'.id')); + var refs=JSON.parse(window.Android.displayTable(cleanupTableName(tmp[3]),[])); + var tmpStr='"; + + } + if(definedSqlTypes[i]["code"]=="link"){ + var strReturn; + if(!View_template) + $.ajax({ + async: false, + method: "GET", + url: './content/view_template.html', + error: function(){ + console.log("Nothing to run after"); + }, + success: function(data){ + console.log("**** \n\n Load View Template"); + View_template=data; + var tmp=obj["value"].split(";"); + var refs=JSON.parse(window.Android.displayTable("SELECT mm4me_tables.id as tid,mm4me_views.id as id,mm4me_tables.name as name,mm4me_tables.description,mm4me_views.name as title from mm4me_tables,mm4me_views where mm4me_tables.name=\""+tmp[1]+"\" and mm4me_tables.id=mm4me_views.ptid",[])); + var reg=new RegExp("\\[id\\]","g"); + if(!valuesOnLoad[cid]) + valuesOnLoad[cid]=[]; + if(refs!=""){ + valuesOnLoad[cid].push(refs); + if(!toRunOnLoad[cid]) + toRunOnLoad[cid]=[]; + toRunOnLoad[cid].push(function(){ + prefix="_"+arguments[0]["0"]["id"]; + //if(!mainTable[arguments[0]["0"]["id"]]) + //mainTable[arguments[0]["0"]["id"]]=arguments[0]["0"]["tid"]; + //console.log(arguments[0]["0"]["tid"]+" "+arguments[0]["0"]["name"]+" "+arguments[0]["0"]["title"]); + listInnerTable(arguments[0]["0"]["tid"],arguments[0]["0"]["id"],arguments[0]["0"]["name"],arguments[0]["0"]["title"],false,prefix,tmp[0]+"="+arguments[1]["local_id"]); + }); + refTables[refs["0"]["tid"]]={"oid":cid,"col":tmp[0],"vid":refs["0"]["id"],"name":refs["0"]["table"],"name":refs["0"]["title"]}; + strReturn=data.replace(reg,refs["0"]["tid"]); + } + } + }); + else{ + var tmp=obj["value"].split(";"); + var refs=JSON.parse(window.Android.displayTable("SELECT mm4me_tables.id as tid,mm4me_views.id as id,mm4me_tables.name as name,mm4me_tables.description,mm4me_views.name as title from mm4me_tables,mm4me_views where mm4me_tables.name=\""+tmp[1]+"\" and mm4me_tables.id=mm4me_views.ptid",[])); + var reg=new RegExp("\\[id\\]","g"); + if(!valuesOnLoad[cid]) + valuesOnLoad[cid]=[]; + valuesOnLoad[cid].push(refs); + if(!toRunOnLoad[cid]) + toRunOnLoad[cid]=[]; + toRunOnLoad[cid].push(function(){ + prefix="_"+arguments[0]["0"]["id"]; + //if(!mainTable[arguments[0]["0"]["id"]]) + //mainTable[arguments[0]["0"]["id"]]=arguments[0]["0"]["tid"]; + //console.log(arguments[0]["0"]["tid"]+" "+arguments[0]["0"]["name"]+" "+arguments[0]["0"]["title"]); + listInnerTable(arguments[0]["0"]["tid"],arguments[0]["0"]["id"],arguments[0]["0"]["name"],arguments[0]["0"]["title"],false,prefix,tmp[0]+"="+arguments[1]["local_id"]); + }); + refTables[refs["0"]["tid"]]={"oid":cid,"col":tmp[0],"vid":refs["0"]["id"],"name":refs["0"]["table"],"name":refs["0"]["title"]}; + strReturn=View_template.replace(reg,refs["0"]["tid"]); + + } + //console.log(strReturn); + return strReturn; + } + if(definedSqlTypes[i]["code"]=="ref"){ + var req=obj["value"];//.replace(/^\((\w+)\)$/g,"$1"); + if(req[0]=="(") + req="SELECT * FROM "+req; + var refs=JSON.parse(window.Android.displayTable(cleanupTableName(req),[])); + var tmpStr='"; + + if(obj["dependencies"]) + try{ + var lobj={}; + lobj[obj["id"]]={"dep":JSON.parse(obj["dependencies"])}; + for(var jj=0;jj=0){ + return ''; + } + if(definedSqlTypes[i]["code"]=="html") + return ''; + if(definedSqlTypes[i]["code"]=="text") + return ''; + if(definedSqlTypes[i]["code"]=="boolean") + return ''; + if(definedSqlTypes[i]["code"]=="date" || definedSqlTypes[i]["code"]=="datetime") + return ''; + if(definedSqlTypes[i]["code"]=="float") + return ''; + return definedSqlTypes[i]["code"]; + } + } + return null; +} + +function printOptionalCheckbox(obj,cid){ + if(definedSqlTypes.length==0){ + definedSqlTypes=JSON.parse(window.Android.displayTable("select id,code from mm4me_ftypes where ftype='e' order by name",[])); + } + + var res=' '; + for(var i in definedSqlTypes){ + if(definedSqlTypes[i]["id"]==obj["ftype"]){ + if(definedSqlTypes[i]["code"]=="bytea"){ + var tmpStr=""; + return res; + } + else if(definedSqlTypes[i]["code"]=="geometry"){ + return res + } + } + } + return ''; +} + +var refTypeId=null; +var editPrintedOnce=[]; +/***************************************************************************** + * Create HTML part to display the line containing both the title and the + * corresponding input for a given table's field. + *****************************************************************************/ +function printEditionFields(obj,myRoot,cid,mid){ + var list1=window.Android.displayTable("select * from mm4me_edition_fields where mm4me_edition_fields.edition>0 and eid="+obj["id"]+" order by mm4me_edition_fields.id asc",[]); + if(!editSchema[mid]) + editSchema[mid]={}; + editSchema[mid][obj["id"]]=JSON.parse(list1); + list1=JSON.parse(list1); + myRoot.find(".tab-content").first().append('
'+obj["description"]+'
'); + for(var j in list1) + if(list1[j]["edition"]>0) { + myRoot.find(".tab-content").first().children().last().append( + '
'+ + '
'+ + ''+ + '
'+ + '
'+ + printCurrentType(list1[j],mid)+ + '
'+ + '
'); + if(list1[j]["dependencies"]){ + try{ + editPrintedOnce.push(list1[j]["name"]); + console.log("JSON PARSE") + console.log(list1[j]["dependencies"]); + var objJson=JSON.parse(list1[j]["dependencies"]); + console.log("JSON PARSE OK"); + if(!refTypeId) + refTypeId=JSON.parse(window.Android.displayTable("select id from mm4me_ftypes where ftype='e' and code='ref'",[]))[0]["id"]; + console.log(refTypeId); + for(i in objJson){ + if(objJson[i]["myself"]){ + console.log("IS MYSELF!!"); + for(k in objJson[i]["myself"]){ + for(l in objJson[i]["myself"][k]){ + + + if(objJson[i]["myself"][k][l]["dependents"]){ + for(m in objJson[i]["myself"][k][l]["dependents"]){ + for(n in objJson[i]["myself"][k][l]["dependents"][m]){ + console.log(objJson[i]["myself"][k][l]["dependents"][m][n]["sql_query"]); + console.log(objJson[i]["myself"][k][l]["dependents"][m][n]["label"]); + var lObj={"id": n, "ftype":refTypeId,"value":objJson[i]["myself"][k][l]["dependents"][m][n]["sql_query"]}; + //if(!myRoot.find('select[name="field_'+list1[j]["id"]+'"]').parent().find('select[name="field_'+n+'"]').length) + myRoot.find('select[name="field_'+list1[j]["id"]+'"]').last().parent().prepend( + '
'+ + '
'+ + ''+ + '
'+ + '
'+ + printCurrentType(lObj,mid)+ + '
'+ + '
'); + console.log(myRoot.find('select[name="field_'+n+'"]')); + console.log(printCurrentType(lObj,mid)); + (function(a,b){ + myRoot.find('select[name="field_'+n+'"]').off('change'); + myRoot.find('select[name="field_'+n+'"]').on('change',function(){ + console.log('select[name="field_'+n+'"]'); + var req=cleanupTableName(a["value"]); + if(a["value"].indexOf("WHERE")<0){ + req=req.replace(/order by/g,"where "+b["tfieldf"]+" "+b["operator"]+" "+(b["operator"]=="like"?"'":"")+$(this).val()+(b["operator"]=="like"?"'":"")+" order by") + } + console.log(req); + var res=JSON.parse(window.Android.displayTable(req,[])); + myRoot.find('select[name="field_'+a["id"]+'"]').html(""); + for(ij in res){ + var tmpStr=' '; + cnt+=1; + } + cnt0+=1; + $("select[name=field_"+changingField[j][ckey]["id"]+"]").append(cStr); + } + if(i==0) + $("select[name=field_"+changingField[j][ckey]["id"]+"]").html(''); + $("select[name=field_"+changingField[j][ckey]["id"]+"]").change(); + }else{ + console.log("DISPLAY ELEMENT IF CINDEX >=0 "); + //console.log(JSON.stringify(changingField[j][ckey])); + console.log('input[name="field_'+ckey+'"],select[name="field_'+ckey+'"],textarea[name="field_'+ckey+'"]'); + console.log($('input[name="field_'+changingField[j][ckey]["id"]+'"],select[name="field_'+changingField[j][ckey]["id"]+'"],textarea[name="field_'+changingField[j][ckey]["id"]+'"]').parent().parent().html()); + var mycKey=changingField[j][ckey]["id"]; + if(cIndex<0) + $('input[name="field_'+mycKey+'"],select[name="field_'+mycKey+'"],textarea[name="field_'+mycKey+'"]').parent().parent().hide(); + else + $('input[name="field_'+mycKey+'"],select[name="field_'+mycKey+'"],textarea[name="field_'+mycKey+'"]').parent().parent().show(); + } + } + } + }; + }; + $("select[name=field_"+key+"]").off('change'); + $("select[name=field_"+key+"]").change(localFunc(changingFields[i][key]["dep"])); + $("select[name=field_"+key+"]").change(); + } + } + }catch(e){ + console.log(e); + setTimeout(function() { updateChangingFields(changingFields) }, 500); + } +} + +/***************************************************************************** + * Display the content of a table referencing the current edited table. + *****************************************************************************/ +function listInnerTable(id,vid,name,title,init,prefix,clause,ref){ + var list=JSON.parse(window.Android.displayTable("select mm4me_tables.id as tid,mm4me_tables.name as tname,mm4me_editions.id,mm4me_editions.name from mm4me_editions,mm4me_tables where mm4me_editions.ptid=mm4me_tables.id and mm4me_tables.id="+id+" order by mm4me_editions.step asc",[])); + var cnt=0; + var detectInit=true; + var tid=0; + for(var i in list){ + lastEdition[list[i]["id"]]=list[i]; + tid=list[i]["tid"]; + if(!allTables[list[i]["tid"]]){ + allTables[list[i]["tid"]]={"id":list[i]["id"],"name":list[i]["tname"],"title":list[i]["name"]}; + detectInit=false; + $("#sub_tableContent_"+id+"").find(".mm4me_edition").find("ul").first().append(''); + printEditionFields(list[i],$("#sub_tableContent_"+id+"").find(".mm4me_edition"),list[i]["id"],list[i]["tid"]); + if(cnt==0){ + try{ + var cid=list[i]["id"]+"_0"; + $("#sub_tableContent_"+id+"").find("ul").first().append('
  • '+window.Android.translate('table')+'
  • '); + $("#sub_tableContent_"+id+"").find("ul").first().append(''); + $("#sub_tableContent_"+id+"").find("ul").first().append(''); + $("#sub_tableContent_"+id+" .require-select").hide(); + printEditionFields(list[i],$("#sub_tableContent_"+id+""),cid,list[i]["tid"]); + }catch(e){ + console.log("**** ERROR ***> "+e); + } + } + var myRoot=$("#sub_tableContent_"+id+""); + myRoot.find('ul').first().find('a').click(function (e) { + e.preventDefault(); + if($(this).parent().hasClass('require-select')) + myRoot.find('.mm4me_edition').show(); + else + myRoot.find('.mm4me_edition').hide(); + $(this).tab('show'); + }) + myRoot.find('ul').first().find('a').first().click(); + myRoot.find('.mm4me_edition').find('ul').find('a').first().click(); + myRoot.find('.swagEditor').summernote(); + myRoot.find(".mm-act-add").click(function(){ + runInsertQuery($(this).parent().parent(),tid,editTableReact); + }); + myRoot.find(".mm-act-save").click(function(){ + runUpdateQuery($(this).parent().parent(),tid,editTableReact); + }); + + } + cnt+=1; + } + + var list=JSON.parse(window.Android.displayTable("SELECT id,name,value,alias,width,class from mm4me_view_fields where vid="+vid+" order by id",[])); + var columns=[]; + var columnNames=[]; + var sqlColumns=""; + var orderColumn="id"; + var orderType="asc"; + for(var i=0;i0){ + orderColumn=list[i]["name"]; + if(list[i]["class"]==1) + orderType="desc"; + } + } + var ccol=getPKey(cleanupTableName(name)); + var req="SELECT "+ccol+" as ogc_fid FROM "+cleanupTableName(name)+" WHERE "+clause+" ORDER BY "+cleanupTableName(name)+"."+orderColumn+" "+orderType; + var list1=JSON.parse(window.Android.displayTable(req,[])); + req="SELECT "+sqlColumns+" FROM "+cleanupTableName(name)+" WHERE "+clause+" ORDER BY "+cleanupTableName(name)+"."+orderColumn+" "+orderType; + var list=JSON.parse(window.Android.displayTable(req,[])); + var dataSet = []; + for(var i=0;i'; + cnt+=1; + } + dataSet.push(tmpData); + } + var selected = []; + + //$("#edition_form_table").html('
    '); + var localName="exampleTable_"+id; + ((function(localName,sid){ + + if(!detectInit){ + + var options={ + data: dataSet, + columns: columns, + "scrollX": true, + scrollY: '50vh', + scrollCollapse: true, + select: true, + "rowCallback": function( row, data ) { + if ( $.inArray(data.DT_RowId, selected) !== -1 ) { + $(row).addClass('selected'); + } + } + }; + if(langUrl!=null) + options["language"]={ + url: langUrl + }; + + $('#'+localName).DataTable( options ); + $('#'+localName+' tbody').on('click', 'tr', function () { + var id = this.id; + var index = $.inArray(id, selected); + + if ( index === -1 ) { + selected.push( id ); + } else { + selected.splice( index, 1 ); + } + displayEditForm(sid,$(this).find("input[name=id]").first().val(),true); + var tmp=$(this).hasClass('selected'); + $('#'+localName+' tbody tr').removeClass('selected'); + $(this).toggleClass('selected'); + if(!tmp) + $(this).toggleClass('selected'); + } ); + + }else{ + $('#'+localName).dataTable().fnClearTable(); + if(dataSet.length>0) + $('#'+localName).dataTable().fnAddData(dataSet); + } + $("#sub_tableContent_"+id+"").find("ul").first().find('a').first().click(); + })(localName,tid)); + +} + +var onFormFirstLoad=null; +/***************************************************************************** + * Show the edit form + *****************************************************************************/ +function displayEditForm(cid,selectedId,basic){ + if(basic && !$("#exampleTable"+(cid==mtable?"":"_"+cid)).find(".selected").find('input[type=hidden]').first().val()){ + if(cid==mtable){ + $("#main_tableContent").find(".require-select").hide(); + $("#main_tableContent").find("ul").first().find("a").first().click(); + if($(".breadcrumb").children().length==4) + $(".breadcrumb").children().last().remove(); + + }else{ + $("#sub_tableContent_"+cid).find(".require-select").hide(); + $("#sub_tableContent_"+cid).find("ul").first().find("a").first().click(); + } + return; + } + var fields=[] + var sizedFields=[]; + var sizedFieldsAlias=[]; + var notSizedFields=[]; + for(var i in editSchema[cid]){ + for(var j in editSchema[cid][i]){ + //console.log(JSON.stringify(editSchema[cid][i][j])); + if(editSchema[cid][i][j]["ftype"]=="5"){ + sizedFields.push(editSchema[cid][i][j]["name"].replace(/wkb_geometry/g,"geometry")); + sizedFieldsAlias.push(editSchema[cid][i][j]["id"]); + } + else{ + notSizedFields.push(editSchema[cid][i][j]["name"].replace(/wkb_geometry/g,"geometry")+" AS \""+editSchema[cid][i][j]["id"]+"\""); + try{ + var tmp=JSON.parse(editSchema[cid][i][j]["dependencies"]); + var sqlReq=""; + var sqlClause=""; + var sqlParams=""; + var sqlParam=0; + var alphabet = 'abcdefghijklmnopqrstuvwxyz'.split(''); + var hasDep=false; + var previousElements=[]; + for(k in tmp) + for(l in tmp[k]){ + //console.log(JSON.stringify(tmp[k][l])); + if(l=="myself"){ + for(m in tmp[k][l]) + for(n in tmp[k][l][m]){ + //console.log(JSON.stringify(tmp[k][l][m][n])); + if(sqlReq!=""){ + sqlReq+=", "; + sqlParams+=", "; + //sqlClause+=" WHERE "+alphabet[sqlParam-1]+"."+tmp[k][l][m][n]["tfield"]+"="+alphabet[sqlParam]+"."+tmp[k][l][m][n]["tfield"]; + + } + sqlReq+="("+cleanupTableName(tmp[k][l][m][n]["sql_query"])+") as "+alphabet[sqlParam]; + sqlParams+=alphabet[sqlParam]+"."+tmp[k][l][m][n]["tfield"]; + sqlParam+=1; + if(tmp[k][l][m][n]["dependents"]){ + //console.log(JSON.stringify(tmp[k][l][m][n]["dependents"])); + for(var o in tmp[k][l][m][n]["dependents"]){ + //console.log(JSON.stringify(tmp[k][l][m][n]["dependents"][o])); + for(var p in tmp[k][l][m][n]["dependents"][o]){ + console.log(tmp[k][l][m][n]["dependents"][o][p]["sql_query"].replace(/from/,","+tmp[k][l][m][n]["dependents"][o][p]["tfield"]+" from")); + sqlReq+=", ("+cleanupTableName(tmp[k][l][m][n]["dependents"][o][p]["sql_query"]).replace(/from/,","+tmp[k][l][m][n]["dependents"][o][p]["tfield"]+" from")+") as b"; + sqlParams+=", b."+tmp[k][l][m][n]["dependents"][o][p]["tfieldf"]; + sqlParam+=2; + sqlReq+=", ("+cleanupTableName(editSchema[cid][i][j]["value"]).replace(/from/,","+tmp[k][l][m][n]["dependents"][o][p]["tfieldf"]+" from")+") as c"; + + sqlClause+=" WHERE c."+tmp[k][l][m][n]["dependents"][o][p]["tfieldf"]+"=b."+tmp[k][l][m][n]["dependents"][o][p]["tfieldf"]; + sqlClause+=" AND b."+tmp[k][l][m][n]["tfield"]+"=a."+tmp[k][l][m][n]["tfield"]; + sqlClause+=" AND c.id=(SELECT "+editSchema[cid][i][j]["name"]+" FROM "+cleanupTableName(allTables[cid].name)+" where id="+selectedId+")"; + hasDep=true; + } + } + } + } + if(!hasDep){ + var tmpReq=cleanupTableName(editSchema[cid][i][j]["value"]); + for(m in tmp[k][l]) + for(n in tmp[k][l][m]){ + tmpReq=tmpReq.replace(/from/,","+tmp[k][l][m][n]["tfield"]+" from"); + if(sqlClause=="") + sqlClause+=" WHERE "; + else + sqlClause+=" "+tmp[k][l][m][n]["cond_join"]+" "; + sqlClause+=alphabet[m]+"."+tmp[k][l][m][n]["tfield"]+"=a1."+tmp[k][l][m][n]["tfield"]; + sqlClause+=""; + } + sqlClause+=" AND a1.id=(SELECT "+editSchema[cid][i][j]["name"]+" FROM "+cleanupTableName(allTables[cid].name)+" where id="+selectedId+")"; + //sqlReq=(tmpReq); + console.log(tmpReq); + sqlReq+=", ("+tmpReq+") as a1"; + } + //console.log(JSON.stringify(tmp[k][l])); + console.log("SELECT "+sqlParams+" FROM "+sqlReq+" "+sqlClause); + var localQuery="SELECT "+sqlParams+" FROM "+sqlReq+" "+sqlClause; + var res0=JSON.parse(window.Android.displayTable(localQuery,[])); + console.log(JSON.stringify(res0)); + for(m in res0) + for(n in res0[m]){ + try{ + console.log("input[name=field_"+n+"],select[name=field_"+n+"],textarea[name=field_"+n+"]"); + /**/ + if($('.mm4me_edition').find("input[name=field_"+n+"],select[name=field_"+n+"],textarea[name=field_"+n+"]").first().length) + $('.mm4me_edition').find("input[name=field_"+n+"],select[name=field_"+n+"],textarea[name=field_"+n+"]").first().val(res0[m][n]).change(); + else{ + for(m0 in tmp[k][l]) + for(n0 in tmp[k][l][m0]){ + /*console.log(n0.indexOf(n)<0); + console.log(n); + console.log(n0); + console.log(res0[m][n]);*/ + if(n0.indexOf(n)>=0){ + console.log("input[name=field_"+n0+"],select[name=field_"+n0+"],textarea[name=field_"+n0+"]"); + if(!$('.mm4me_edition').find("input[name=field_"+n+"],select[name=field_"+n+"],textarea[name=field_"+n+"]").first().length) + $('.mm4me_edition').find("input[name=field_"+n0+"],select[name=field_"+n0+"],textarea[name=field_"+n0+"]").first().val(res0[m][n]).change(); + } + } + } + }catch(e){ + console.log(e); + } + + } + //if(tmp[k][l]["dependents"]) + }else{ + console.log(JSON.stringify(tmp[k][l])); + if(tmp[k][l]["tfield"]=="none"){ + var isNull=JSON.parse(window.Android.displayTable("SELECT CASE WHEN "+l+" is null THEN 1 ELSE 0 END as p FROM "+cleanupTableName(allTables[cid].name)+" where id="+selectedId,[])); + console.log(JSON.stringify(isNull)); + if(isNull[0]["p"]=="0"){ + console.log(JSON.stringify(isNull)); + console.log("input[name=field_"+editSchema[cid][i][j]["id"]+"],select[name=field_"+editSchema[cid][i][j]["id"]+"],textarea[name=field_"+editSchema[cid][i][j]["id"]+"]"); + console.log(k+""); + $('.mm4me_edition').find("input[name=field_"+editSchema[cid][i][j]["id"]+"],select[name=field_"+editSchema[cid][i][j]["id"]+"],textarea[name=field_"+editSchema[cid][i][j]["id"]+"]").first().val(k+"").change(); + } + } + + } + } + + //if(sqlReq) + }catch(e){ + console.log(e); + } + } + if(editSchema[cid][i][j]["name"].indexOf("unamed")<0) + fields.push(editSchema[cid][i][j]["name"].replace(/wkb_geometry/g,"geometry")+" AS \""+editSchema[cid][i][j]["id"]+"\""); + } + } + var ccol=getPKey(cleanupTableName(allTables[cid].name)); + /* $("#exampleTable"+(cid==mtable?"":"_"+cid)).find(".selected").find('input[type=hidden]').first().val() */ + var editValues; + if(sizedFields.length>0){ + for(var i=0;i 1000000 and "+ccol+"="+selectedId,[])); + //console.log(JSON.stringify(hasElement[0])); + if(hasElement[0]["cnt"]!="0"){ + var nbIteration=parseInt(hasElement[0]["len"])/1000000; + var zfields=[] + var len=0; + var query=""; + var len1=parseInt(hasElement[0]["len"]); + for(var j=0;j0){ + $("#field_"+j+"_map").show(); + $("#field_"+j+"_map").off('click'); + $("#field_"+j+"_map").on('click',function(){ + console.log("Display table with a selected feature"); + console.log($(this).prev().val()); + showElementOnMap($(this).prev().val()); + }); + } + if($(".btn_field_"+j+"_lat").length>0){ + //alert(editValues[i][j]); + $(".btn_field_"+j+"_lat").html(editValues[i][j]); + $(".btn_field_"+j+"_long").html(""); + $(".btn_field_"+j+"_source").html(""); + $("input[name='field_"+j+"']").val("POINT"+editValues[i][j]); + + } + } + //$(".swagEditor").summernote(); + } + } + + for(var i in editSchema[cid]){ + for(var j in editSchema[cid][i]){ + if(editSchema[cid][i][j]["name"].indexOf("unamed")>=0){ + if(editSchema[cid][i][j]["ftype"]==6){ + var tmp=editSchema[cid][i][j]["value"].split(';'); + var list=JSON.parse(window.Android.displayTable("select "+tmp[1]+" from "+cleanupTableName(tmp[2])+" where "+tmp[0]+"=(SELECT id from "+cleanupTableName(tblName)+" WHERE ogc_fid="+selectedId+")",[])); + editValues["0"][editSchema[cid][i][j]["id"]]=list; + for(var k in list){ + for(var l in list[k]){ + $('.mm4me_edition').find("select[name=field_"+editSchema[cid][i][j]["id"]+"]").find('option').each(function(){ + if($(this).val()==list[k][l]) + $(this).prop("selected",true); + }); + } + } + } + } + } + } + + if(cid==mtable){ + if($(".breadcrumb").children().length==4) + $(".breadcrumb").children().last().remove(); + $(".breadcrumb").append('
  • '+editValues["0"]["local_id"]+'
  • '); + } + ((cid==mtable)?$(".require-select"):$("#sub_tableContent_"+cid).find(".require-select")).first().show(); + ((cid==mtable)?$(".require-select"):$("#sub_tableContent_"+cid).find(".require-select")).first().find("a").first().click(); + + if(toRunOnLoad[cid]) + for(var i=0;i=0 order by mm4me_editions.step asc",[]); + if(MM4ME_DEBUG) + console.log(list); + list=JSON.parse(list); + if(MM4ME_DEBUG) + console.log((!allTables[tblId])); + if(!allTables[tblId]){ + allTables[tblId]={"id":id,"name":name,"title":title}; + } + mtable=tblId; + + $(".mm4me_edition").find("ul").first().html(""); + $(".mm4me_edition").find(".well").first().html(""); + var cnt=0; + for(var i in list){ + lastEdition[list[i]["id"]]=list[i]; + var cid=(i==0?list[i]["id"]+"_0":list[i]["id"]); + $(".mm4me_edition").find("ul").first().append(''); + printEditionFields(list[i],$("#edition_form_edit"),cid,mainTable[id]); + } + if(list.length==1) + $(".mm4me_edition").find("ul").first().hide(); + + var aCnt=0; + $('.mm4me_edition').find('ul').first().find('a').each(function () { + if(aCnt>0) + $(this).parent().addClass('require-select'); + aCnt+=1; + }); + $(".require-select").hide(); + $('.mm4me_listing').find('ul').first().find('a').first().click(); + $('.mm4me_edition').find('ul').find('a').first().click(); + $('.swagEditor').summernote(); + $(".mm-act-add").click(function(){ + runInsertQuery($(this).parent().parent(),mainTable[id],editOnlyTableReact); + }); + $(".mm-act-save").click(function(){ + runUpdateQuery($(this).parent().parent(),mainTable[id],editOnlyTableReact); + }); + $(".breadcrumb").children().last().remove(); + $(".breadcrumb").append('
  • '+window.Android.translate('edit')+'
  • '); + $(".breadcrumb").append('
  • '+tblTitle+'
  • '); + + setTimeout(function() { updateChangingFields(changingFields) }, 1500); + + /*for(var i=0;i'; + else + cStr+=changingField[j][ckey]["values"][cIndex][i][lkey]+''; + cnt+=1; + } + $("select[name=field_"+changingField[j][ckey]["id"]+"]").append(cStr); + } + if(i==0) + $("select[name=field_"+changingField[j][ckey]["id"]+"]").html(''); + } + } + }; + }; + $("select[name=field_"+key+"]").off('change'); + $("select[name=field_"+key+"]").change(localFunc(changingFields[i][key]["dep"])); + $("select[name=field_"+key+"]").change(); + } + }*/ + $('.mm4me_listing').show(); + $('.mm4me_content').hide(); + +} + +/***************************************************************************** + * In case no server is configured + *****************************************************************************/ +function displayNoListing(){ + $.ajax({ + method: "GET", + url: 'content/nolisting.html', + success: function(data){ + if(MM4ME_DEBUG) + console.log('Display warning message on the UI !'); + $(".mm4me_content").html(data); + $(".mm4me_content").find(".pannel-body").find("p").first().html(window.Android.translate("mm_no_db_found")); + }, + error: function(){ + window.Android.showToast("error !"); + } + }); +} + + +/***************************************************************************** + * Authenticate a user + *****************************************************************************/ +function authenticate(url,login,passwd,func,func1){ + var curl=url+"?service=WPS&request=Execute&version=1.0.0&Identifier=authenticate.clogIn&DataInputs=login="+login+";password="+passwd+"&RawDataOutput=Result"; + if(MM4ME_DEBUG) + console.log(curl); + $.ajax({ + method: "GET", + url: curl, + success: function(data){ + if(MM4ME_DEBUG) + console.log(data); + if(func){ + console.log("Call func!") + func(); + } + }, + error: function(){ + if(func1){ + func1(); + } + else{ + disconnect(url); + if(MM4ME_DEBUG) + console.log("unable to login!"); + var hasBeenShown=false; + var xml=arguments[0].responseText; + $(xml).find("ows\\:ExceptionText").each(function(){ + window.Android.showToast($(this).text()); + hasBeenShown=true; + }); + if(!hasBeenShown){ + window.Android.showToast(JSON.stringify(arguments)); + } + } + + } + }); +} + +/***************************************************************************** + * Disconnect a user + *****************************************************************************/ +function disconnect(url,func,func1){ + var curl=url+"?service=WPS&request=Execute&version=1.0.0&Identifier=authenticate.clogOut&DataInputs=&RawDataOutput=Result"; + if(MM4ME_DEBUG) + console.log(curl); + $.ajax({ + method: "GET", + url: curl, + success: function(data){ + if(MM4ME_DEBUG){ + console.log(data); + console.log("** Your are no more connected!"); + } + if(func){ + func(); + } + }, + error: function(){ + if(MM4ME_DEBUG){ + console.log(curl); + console.log("unable to disconnect!"); + } + if(func1){ + func1(); + } + } + }); +} + +var geometries={"line":{"geom":null,"constructor": "ol.geom.Linestring"},"polygon":{"geom":null,"constructor":"ol.geom.Polygon","cline":null}}; +var stopTracking=false; +var currentGeometry="line"; +var currentGeometryField="none"; +var map=null; +var vectorLayer,vectorLayer1; +var position; + +/***************************************************************************** + * Track modification of the GPS location + *****************************************************************************/ +function trackCurrentGPSPosition(){ + console.log("## trackCurrentGPSPosition"); + updateCurrentMapLocation(); + + var tmp0=geometries["origin"].getCoordinates(); + if(geometries[currentGeometry]["geom"]!=null){ + var tmp1=geometries[currentGeometry]["geom"].getCoordinates(); + tmp0=tmp1[tmp1.length-1]; + } + tmp=ol.proj.transform([position[bestIndex].lon,position[bestIndex].lat], 'EPSG:4326','EPSG:3857'); + console.log(JSON.stringify(tmp0)); + console.log(JSON.stringify(tmp)); + $("#currentPosition").html("Position: "+position[bestIndex].lon.toFixed(6)+","+position[bestIndex].lat.toFixed(6)+""); + if(tmp0[0]==tmp[0] && tmp0[1]==tmp[1]){ + console.log(" !!!!!!!! Same position!!!!!!!! "); + if(!stopTracking) + setTimeout(function() { trackCurrentGPSPosition();}, 1000); + return; + } + + console.log(JSON.stringify(tmp0)); + console.log(JSON.stringify(tmp)); + if(geometries[currentGeometry]["geom"]==null){ + //geometries[currentGeometry]=new geometries[currentGeometry]["constructor"]([tmp0,tmp]); + if(currentGeometry=="line") + geometries[currentGeometry]["geom"]=new ol.geom.LineString([tmp0,tmp]); + else + geometries[currentGeometry]["geom"]=new ol.geom.LineString([tmp0,tmp]); + }else{ + if(currentGeometry=="line"){ + tmpCoordinates=geometries[currentGeometry]["geom"].getCoordinates(); + tmpCoordinates.push(tmp); + geometries[currentGeometry]["geom"]=new ol.geom.LineString(tmpCoordinates); + } + else{ + if(geometries[currentGeometry]["cline"]==null) + tmpCoordinates=geometries[currentGeometry]["geom"].getCoordinates(); + else + tmpCoordinates=geometries[currentGeometry]["cline"].getCoordinates(); + console.log(JSON.stringify(tmpCoordinates)); + tmpCoordinates.push(tmp); + console.log(JSON.stringify(tmpCoordinates)); + if(tmpCoordinates.length>2){ + geometries[currentGeometry]["cline"]=new ol.geom.LineString(tmpCoordinates); + tmpCoordinates.push(tmpCoordinates[0]); + console.log(JSON.stringify(tmpCoordinates)); + geometries[currentGeometry]["geom"]=new ol.geom.Polygon(); + console.log(JSON.stringify(tmpCoordinates)); + geometries[currentGeometry]["geom"].appendLinearRing(new ol.geom.LinearRing(tmpCoordinates)); + console.log(JSON.stringify(tmpCoordinates)); + } + } + + } + console.log(JSON.stringify(tmp0)); + console.log(JSON.stringify(tmp)); + try{ + console.log(JSON.stringify(geometries[currentGeometry]["geom"].getCoordinates())); + var feature=new ol.Feature({geometry: geometries[currentGeometry]["geom"],"name":"myFeature"}); + vectorLayer1.getSource().clear(); + vectorLayer1.getSource().addFeatures([feature]); + var wkt=new ol.format.WKT(); + var tmpGeometry=geometries[currentGeometry]["geom"].clone().transform('EPSG:3857', 'EPSG:4326'); + console.log(wkt.writeGeometry(tmpGeometry)); + $("input[name='"+currentGeometryField+"']").val(wkt.writeGeometry(tmpGeometry)); + }catch(e){ + console.log(e); + } + console.log(JSON.stringify(tmp0)); + console.log(JSON.stringify(tmp)); + if(!stopTracking) + setTimeout(function() { trackCurrentGPSPosition();}, 1000); + console.log(JSON.stringify(tmp0)); + console.log(JSON.stringify(tmp)); +} + +var modalCallback=null; + +function trackStepCurrentGPSPosition(){ + console.log("## trackStepCurrentGPSPosition"); + updateCurrentMapLocation(); + $("#currentPosition").html("Position: "+position[bestIndex].lon.toFixed(6)+","+position[bestIndex].lat.toFixed(6)+""); + if(!stopTracking) + setTimeout(function() { trackStepCurrentGPSPosition();}, 1000); +} + +function addCurrentLocation(){ + console.log("addCurrentLocation!!!!"); + if(geometries["origin"]==null) + geometries["origin"]=new ol.geom.Point(ol.proj.transform([position[bestIndex].lon,position[bestIndex].lat], 'EPSG:4326','EPSG:3857')); + else{ + var tmp0=geometries["origin"].getCoordinates(); + if(geometries[currentGeometry]["geom"]!=null){ + var tmp1=geometries[currentGeometry]["geom"].getCoordinates(); + tmp0=tmp1[tmp1.length-1]; + } + tmp=ol.proj.transform([position[bestIndex].lon,position[bestIndex].lat], 'EPSG:4326','EPSG:3857'); + console.log(JSON.stringify(tmp0)); + console.log(JSON.stringify(tmp)); + $("#currentPosition").html("Position: "+position[bestIndex].lon.toFixed(6)+","+position[bestIndex].lat.toFixed(6)+""); + if(geometries[currentGeometry]["geom"]==null){ + //geometries[currentGeometry]=new geometries[currentGeometry]["constructor"]([tmp0,tmp]); + if(currentGeometry=="line") + geometries[currentGeometry]["geom"]=new ol.geom.LineString([tmp0,tmp]); + else + geometries[currentGeometry]["geom"]=new ol.geom.LineString([tmp0,tmp]); + }else{ + if(currentGeometry=="line"){ + tmpCoordinates=geometries[currentGeometry]["geom"].getCoordinates(); + tmpCoordinates.push(tmp); + geometries[currentGeometry]["geom"]=new ol.geom.LineString(tmpCoordinates); + } + else{ + if(geometries[currentGeometry]["cline"]==null) + tmpCoordinates=geometries[currentGeometry]["geom"].getCoordinates(); + else + tmpCoordinates=geometries[currentGeometry]["cline"].getCoordinates(); + console.log(JSON.stringify(tmpCoordinates)); + tmpCoordinates.push(tmp); + console.log(JSON.stringify(tmpCoordinates)); + if(tmpCoordinates.length>2){ + geometries[currentGeometry]["cline"]=new ol.geom.LineString(tmpCoordinates); + tmpCoordinates.push(tmpCoordinates[0]); + console.log(JSON.stringify(tmpCoordinates)); + geometries[currentGeometry]["geom"]=new ol.geom.Polygon(); + console.log(JSON.stringify(tmpCoordinates)); + geometries[currentGeometry]["geom"].appendLinearRing(new ol.geom.LinearRing(tmpCoordinates)); + console.log(JSON.stringify(tmpCoordinates)); + } + } + + } + try{ + console.log(JSON.stringify(geometries[currentGeometry]["geom"].getCoordinates())); + var feature=new ol.Feature({geometry: geometries[currentGeometry]["geom"],"name":"myFeature"}); + vectorLayer1.getSource().clear(); + vectorLayer1.getSource().addFeatures([feature]); + var wkt=new ol.format.WKT(); + var tmpGeometry=geometries[currentGeometry]["geom"].clone().transform('EPSG:3857', 'EPSG:4326'); + console.log(wkt.writeGeometry(tmpGeometry)); + $("input[name='"+currentGeometryField+"']").val(wkt.writeGeometry(tmpGeometry)); + }catch(e){ + console.log(e); + } + + } +} + +function trackStepByStepPosition(elem,ltype){ + modalCallback=function(){ + //$("#myModal").find(".glyphicon-ok").parent().show(); + if(myVectorLayer) + myVectorLayer.getSource().clear(); + geometries["origin"]=null; + setTimeout(function() { trackStepCurrentGPSPosition();}, 1000); + $("#myModal").find("h4").html(' '+window.Android.translate("gps_track")); + $("#myModal").find(".modal-footer").html( + ''+ + ''+ + '' + ); + }; + currentGeometry=ltype; + currentGeometryField=elem; + geometries[ltype]["geom"]=null; + geometries[ltype]["cline"]=null; + if(!$("#map").length) + $.ajax({ + method: "GET", + url: './map-modal.html', + error: function(){ + }, + success: function(data){ + console.log(data); + $("body").append(data); + $("#map").css("height",($(window).height()-220)+"px"); + $('#myModal').modal(); + addOptionalLocalTiles(true); + $('#myModal').on('shown.bs.modal', function () { + console.log($("#mmm4me_ls").length); + initMapToLocation(); + localTileIndex=map.getLayers().getLength(); + /*geometries["origin"]=new ol.geom.Point(ol.proj.transform([position[bestIndex].lon,position[bestIndex].lat], 'EPSG:4326','EPSG:3857')); + setTimeout(function() { trackCurrentGPSPosition();}, 1000);*/ + modalCallback(); + }); + $('#myModal').on('hide.bs.modal', function () { + console.log("HIDE"); + stopTracking=true; + }); + } + }); + else{ + stopTracking=false; + $('#myModal').modal(); + vectorLayer1.getSource().clear(); + updateCurrentMapLocation(); + geometries["origin"]=null; + setTimeout(function() { trackStepCurrentGPSPosition();}, 1000); + } +} +/***************************************************************************** + * Start tracking GPS location + *****************************************************************************/ +function trackGPSPosition(elem,ltype){ + modalCallback=function(){ + //$("#myModal").find(".glyphicon-ok").parent().show(); + if(myVectorLayer) + myVectorLayer.getSource().clear(); + geometries["origin"]=new ol.geom.Point(ol.proj.transform([position[bestIndex].lon,position[bestIndex].lat], 'EPSG:4326','EPSG:3857')); + setTimeout(function() { trackCurrentGPSPosition();}, 1000); + $("#myModal").find("h4").html(' '+window.Android.translate("gps_track")); + $("#myModal").find(".modal-footer").html( + ''+ + '' + ); + }; + currentGeometry=ltype; + currentGeometryField=elem; + geometries[ltype]["geom"]=null; + geometries[ltype]["cline"]=null; + if(!$("#map").length) + $.ajax({ + method: "GET", + url: './map-modal.html', + error: function(){ + }, + success: function(data){ + console.log(data); + $("body").append(data); + $("#map").css("height",($(window).height()-220)+"px"); + $('#myModal').modal(); + addOptionalLocalTiles(true); + $('#myModal').on('shown.bs.modal', function () { + console.log($("#mmm4me_ls").length); + initMapToLocation(); + localTileIndex=map.getLayers().getLength(); + /*geometries["origin"]=new ol.geom.Point(ol.proj.transform([position[bestIndex].lon,position[bestIndex].lat], 'EPSG:4326','EPSG:3857')); + setTimeout(function() { trackCurrentGPSPosition();}, 1000);*/ + modalCallback(); + }); + $('#myModal').on('hide.bs.modal', function () { + console.log("HIDE"); + stopTracking=true; + }); + } + }); + else{ + stopTracking=false; + $('#myModal').modal(); + vectorLayer1.getSource().clear(); + updateCurrentMapLocation(); + geometries["origin"]=new ol.geom.Point(ol.proj.transform([position[bestIndex].lon,position[bestIndex].lat], 'EPSG:4326','EPSG:3857')); + setTimeout(function() { trackCurrentGPSPosition();}, 1000); + } +} + +var hasAddedElement=0; +var myVectorLayer=null; +function addSelectedElement(wktString){ + var features=[]; + try{ + var format = new ol.format.WKT(); + features.push( + format.readFeature(wktString, { + dataProjection: 'EPSG:4326', + featureProjection: 'EPSG:3857' + }) + ); + }catch(e){ + console.log("Unable to parse WKT ?!"+e) + } + if(!vectorLayer1){ + myVectorLayer = new ol.layer.Vector({ + source: new ol.source.Vector({ + features: features + }), + style: new ol.style.Style({ + stroke: new ol.style.Stroke({ + color: "#3333aa", + width: 1.4 + }) + }) + }); + map.addLayer(myVectorLayer); + } + else{ + vectorLayer1.getSource().clear(); + vectorLayer1.getSource().addFeatures(features); + } + console.log(vectorLayer1.getSource().getExtent()); + map.updateSize(); + map.getView().fit(vectorLayer1.getSource().getExtent(),map.getSize()); + hasAddedElement=1; +} + +/***************************************************************************** + * Show selected feature on map + *****************************************************************************/ + var addSelectedIndex=0; + var mySelectedElement=null; +function showElementOnMap(wktString){ + mySelectedElement=wktString; + modalCallback=function(){ + //$("#myModal").find(".glyphicon-ok").parent().hide(); + if(addSelectedIndex==0){ + updateCurrentMapLocation(); + $("#currentPosition").html("Position: "+position[bestIndex].lon.toFixed(6)+","+position[bestIndex].lat.toFixed(6)+""); + } + addSelectedElement(mySelectedElement); + $("#myModal").find("h4").html(' '+window.Android.translate("view_feature")); + addSelectedIndex++; + }; + //$("#currentPosition").html("Position: "+position[bestIndex].lon.toFixed(6)+","+position[bestIndex].lat.toFixed(6)+""); + if(!$("#map").length) + $.ajax({ + method: "GET", + url: './map-modal.html', + error: function(){ + }, + success: function(data){ + console.log(data); + $("body").append(data); + $("#map").css("height",($(window).height()-220)+"px"); + $('#myModal').modal(); + addOptionalLocalTiles(true); + $('#myModal').on('shown.bs.modal', function () { + console.log($("#mmm4me_ls").length); + initMapToLocation(); + localTileIndex=map.getLayers().getLength(); + modalCallback(); + }); + $('#myModal').on('hide.bs.modal', function () { + console.log("HIDE"); + stopTracking=true; + }); + } + }); + else{ + console.log("OK "); + $('#myModal').modal(); + console.log("OK "); + vectorLayer1.getSource().clear(); + console.log("OK "); + updateCurrentMapLocation(); + console.log("OK "); + addSelectedElement(wktString); + console.log("OK "); + } + + $("#currentPosition").html("Position: "+position[bestIndex].lon.toFixed(6)+","+position[bestIndex].lat.toFixed(6)+""); +} + + +/***************************************************************************** + * Store the current GPS location in the edit form + *****************************************************************************/ +function requireGPSPosition(elem){ + var position=JSON.parse(window.Android.getGPS()); + //elem=$("#"+elem); + if(position.lat){ + $(".btn_"+elem+"_lat").html(position.lat); + $(".btn_"+elem+"_long").html(position.lon); + $(".btn_"+elem+"_source").html(position.source); + $("input[name='"+elem+"']").val("POINT("+position.lon+" "+position.lat+")"); + }else{ + + } + console.log(JSON.stringify(position)); +} + +/***************************************************************************** + * Ajax setup to ensure seting "withCredentials" to true + *****************************************************************************/ +function ajaxSetup(){ + $.ajaxSetup({ + xhrFields: { + withCredentials: true + } + }); +} + +/***************************************************************************** + * Get the current Network and GPS availability + *****************************************************************************/ +function getCurrentStatus(){ + var tmp=JSON.parse(window.Android.getGNStatus()); + tmp["gps"]=JSON.parse(tmp["gps"]); + updateStatus(tmp['gps'],tmp['net']); +} + +/***************************************************************************** + * Display the status icons + *****************************************************************************/ +function addStatusControl(){ + $('.breadcrumb').append(' /'+ + ''+ + ''+ + ''+ + ''+ + ''); +} + +/***************************************************************************** + * Initialize the map and show the current GPS location + *****************************************************************************/ +var localTiles,localTiles0; +var tileLayers=[]; +function initMapToLocation(){ + if(map) + return; + var osmSource = new ol.source.OSM(); + + var otherLayers=[]; + var otherLayersSwitcher=""; + try{ + var BasesLayersStr=window.Android.getBaseLayers(); + console.log(BasesLayersStr); + var BasesLayers=JSON.parse(BasesLayersStr); + for(var i=0;i=4) + map.getLayers().item(otherLayers[i]["index"]).setVisible(false); + }else{ + $(this).parent().find("i").removeClass("glyphicon-eye-close").addClass("glyphicon-eye-open"); + $("#dmopacity_"+i).show(); + console.log(JSON.stringify(map.getLayers().getLength())); + if(!otherLayers[i]["index"]){ + var myTileLayer=new ol.layer.Tile({source: otherLayers[i]["olLayer"]}); + var layers = map.getLayers(); + layers.insertAt(tileLayers.length+1,myTileLayer); + otherLayers[i]["index"]=tileLayers.length+1; + tileLayers.push(myTileLayer); + if(localTileIndex>0) + localTileIndex+=1; + } + else + map.getLayers().item(otherLayers[i]["index"]).setVisible(true); + } + }); + $(".map").parent().find("input[type=range]").last().on('change',function(){ + console.log("change to "+$(this).val()); + map.getLayers().item(otherLayers[i]["index"]).setOpacity($(this).val()/100); + }); + + })(i); + $(".map").parent().find("input[type=checkbox]").parent().off('click'); + $(".map").parent().find("input[type=checkbox]").parent().on('click',function(){ + var tmp=$(this).find("input[type=checkbox]"); + if(tmp.is(":checked")){ + $(this).find("input[type=checkbox]").prop("checked",false).change(); + $(this).addClass("select"); + } + else{ + $(this).find("input[type=checkbox]").prop("checked",true).change(); + $(this).removeClass("select"); + } + }); + + },1); + })(i) + } + } + map = new ol.Map({ + layers: layers, + target: 'map', + controls: ol.control.defaults({ + attributionOptions: /** @type {olx.control.AttributionOptions} */ ({ + collapsible: true + }) + }), + view: new ol.View({ + center: ol.proj.transform([0,0], 'EPSG:4326', 'EPSG:3857'), + zoom: 14, + maxZoom: 22, + minZoom: 1 + }) + }); + + var bstyle = function (feature, resolution) { + + /*var iconFont = 'glyphicon'; + var iconFontText = '\e062';*/ + //var iconFont = 'glyphicon'; + var iconFont = 'Glyphicons Halflings'; + var iconFontText = '\ue062'; + var iconSize = 24; + var opacity=0.4; + var col = 'rgba(0,255,0,0.6)'; + if(feature.get("source")=="GPS") + col = 'rgba(41,136,54,0.5)';//#298836 + else if(feature.get("source")=="Network"){ + col = 'rgba(91,176,75,0.4)';//#5bb04b + iconSize = 32; + opacity=0.2; + } + else if(feature.get("source")=="other"){ + col='rgba(129,208,113,0.5)';//#81d071 + iconSize = 36; + opacity=0.2; + } + else + col='rgba(166,63,39,0.5)'; //#a63f27 //"#EE0000"; + var styles = []; + + var styleIcon = new ol.style.Style({ + text: new ol.style.Text({ + font: 'Normal ' + iconSize + 'px ' + iconFont, + text: iconFontText, + fill: new ol.style.Fill({ color: col }) + }) + }); + styles.push(styleIcon); + + //console.log(feature.get("type")); + return styles; + /*return function (feature, resolution) { + styles.styleIcon.getText().setText(feature.get("iconCode")); + return styles; + };*/ + }; + position=JSON.parse(window.Android.getFullGPS()); + //console.log(JSON.stringify(position)); + if(position.length==0){ + position=[{lon:3.5,lat:43.5,source:"none"}]; + } + var iconFeatures = []; + for(var i=0;i=4) + map.getLayers().item(localTileIndex).setVisible(false); + }else{ + $(this).parent().find("i").removeClass("glyphicon-eye-close").addClass("glyphicon-eye-open"); + $("#dmopacity").show(); + console.log(JSON.stringify(map.getLayers().getLength())); + if(map.getLayers().getLength()0.05){ + map.getView().setRotation(-direction); + oldBearer=direction; + } + }else{ + window.Android.stopReportDirection(); + } +} \ No newline at end of file diff --git a/app/src/main/assets/scripts/gene2.js b/app/src/main/assets/scripts/gene2.js new file mode 100644 index 0000000..7387afa --- /dev/null +++ b/app/src/main/assets/scripts/gene2.js @@ -0,0 +1,2461 @@ +var MM4ME_DEBUG=true; +var EDITION_TYPE_FILE=5; +var mainTable={}; +var mtable=null; +var refTables={}; +var tblId=null; +var allTables={}; +var editSchema={}; +var referenceIds={}; +var lastEdition={}; +var tblName=null; +var toRunOnLoad={}; +var valuesOnLoad=[]; +var definedSqlTypes=[]; +var changingFields=[]; +var lang=window.Android.getLang(); +var langUrl=null; +if(lang=="fr"){ + langUrl="file:///android_asset/localisation/French.json"; +} + + + + +function loadWelcome3(){ + window.Android.startWelcomeScreen("draw"); +} + +/***************************************************************************** + * Update status icon color depending on network and GPS availability + *****************************************************************************/ +function updateStatus(gps,internet){ + if(internet){ + $('.glyphicon-signal').css('color','#00EE00'); + }else + $('.glyphicon-signal').css('color','#EE0000'); + $('#gpsMenu1').next().html(""); + if(gps.length==0){ + $('#gpsMenu1').css('color','#EE0000'); + $('#gpsMenu1').next().html('
  • '+window.Android.translate("no_gps")+'
  • '); + }else{ + $('#gpsMenu1').css('color','#00EE00'); + for(var i=0;i '+gps[i].source+''); + } +} + +/***************************************************************************** + * List all available table for a given theme + *****************************************************************************/ +function fetchTableForTheme(obj,id){ + var hasTable=false; + var tables=JSON.parse(window.Android.displayTable( + "SELECT mm4me_tables.id as tid,mm4me_views.id as id,"+ + "mm4me_tables.name as name, "+ + "mm4me_tables.description, "+ + "mm4me_views.name as title "+ + " from mm4me_tables,mm4me_views"+ + " where mm4me_tables.id=mm4me_views.ptid "+ + "and mm4me_views.visible "+ + "and mm4me_views.id in (select vid from mm4me_views_themes where tid="+id+")",[])); + for(var i=0;i0; +} + +/***************************************************************************** + * Create a JSON Object containing the themes hierarchy + *****************************************************************************/ +function fetchThemes(obj,id){ + var cthemes=JSON.parse(window.Android.displayTable("SELECT id,name FROM mm4me_themes where pid = "+id,[])); + for(var i=0;i'); + $('#tree').treeview({ + data: allThemes, + onNodeSelected: function(event, data) { + if(data["myId"]){ + try{ + func(data["myId"],data["myName"],data["myTitle"],true); + }catch(e){ + console.log(JSON.stringify(data)); + console.log(e); + window.Android.showToast(e); + //exit(); + } + } + else{ + $('#tree').treeview('toggleNodeExpanded',[$("#tree").treeview('getSelected')[0], { silent: true }]); + $('#tree').treeview('toggleNodeSelected',[$("#tree").treeview('getSelected')[0], { silent: true }]); + //console.log(JSON.stringify(data)); + //$(this).open(); + } + }, + showTags: true + }); + var list=JSON.parse(window.Android.displayTable("SELECT mm4me_tables.id as tid,mm4me_views.id as id,mm4me_tables.name as name,mm4me_tables.description,mm4me_views.name as title from mm4me_tables,mm4me_views where mm4me_tables.id=mm4me_views.ptid and mm4me_views.visible",[])); + var tableList=list; + var total=0; + contents=[]; + for(var i in list){ + mainTable[list[i]["id"]]=list[i]["tid"]; + } + } + catch(e){ + console.log(e); + displayNoListing(); + } +} + +/***************************************************************************** + * Update the breadcrumbs text to translated string + *****************************************************************************/ +function updateBreadcrumbs(breadcrumbs){ + //var breadcrumbs=["home","view"]; + var lcnt0=0; + $('.breadcrumb').find("a").each(function(){ + $(this).append(window.Android.translate(breadcrumbs[lcnt0])); + lcnt0+=1; + }); + $('.breadcrumb').find("li").last().each(function(){ + $(this).append(window.Android.translate(breadcrumbs[lcnt0])); + lcnt0+=1; + }); +} + +/***************************************************************************** + * Convert table names in a string from the PostgreSQL to our SQLite format. + * So, convert "AAAXXX.YYYBBB" to "AAAXXX_YYYBBB" + *****************************************************************************/ +function cleanupTableName(name){ + return name.replace(/(\w+)(\d*)\.(\d*)(\w+)/g,"$1$2_$3$4"); +} + +/***************************************************************************** + * Display an HTML part containing the image produced by Camera or picked from + * the photo library. + *****************************************************************************/ +function loadNewPicture(cid,id,picture){ + $(".tab-pane").each(function(){ + if($(this).is(":visible")) + $(this).find("#value_"+id) + .html('
    '+picture+'
    '); + }); +} + + +/***************************************************************************** + * Create a JSON Object representing the dependency values. + *****************************************************************************/ +function fetchDependencies(obj,cid,changingField){ + var list1=null; + //console.log(cid+" "+JSON.stringify(obj)); + + for(var key in changingField){ + for(var i=0;i'; + tmpStr+=res+ + '
    '; + console.log(tmpStr); + return tmpStr; + } + if(definedSqlTypes[i]["code"]=="geometry"){ + console.log("CID: "+cid+" select type from mm4me_gc where f_table_schema||'_'||f_table_name = (select name from mm4me_tables where id="+cid+") ") + var geoType=getGeometryType("(select replace(name,'.','_') from mm4me_tables where id="+cid+")"); + var viewb=''; + var res=' '; + if(geoType=='POINT' || geoType=='MULTIPOINT' ) + return res+''+ + '

    GPS Informations

    Type
    Coords
    '+ + ''; + else if(geoType=='LINESTRING' || geoType=='MULTILINESTRING' ) + return res+''+ + ''+ + '
    '+viewb+'
    '; + else if(geoType=='POLYGON' || geoType=='MULTIPOLYGON' ) + return res+''+ + ''+ + '
    '+viewb+'
    '; + + } + if(definedSqlTypes[i]["code"]=="tbl_linked"){ + var tmp=obj["value"].split(';'); + //console.log(cleanupTableName('SELECT '+tmp[1]+' FROM '+tmp[2]+' WHERE '+tmp[0]+'='+tblName+'.id')); + var refs=JSON.parse(window.Android.displayTable(cleanupTableName(tmp[3]),[])); + var tmpStr='"; + + } + if(definedSqlTypes[i]["code"]=="link"){ + var strReturn; + if(!View_template) + $.ajax({ + async: false, + method: "GET", + url: './content/view_template.html', + error: function(){ + console.log("Nothing to run after"); + }, + success: function(data){ + console.log("**** \n\n Load View Template"); + View_template=data; + var tmp=obj["value"].split(";"); + var refs=JSON.parse(window.Android.displayTable("SELECT mm4me_tables.id as tid,mm4me_views.id as id,mm4me_tables.name as name,mm4me_tables.description,mm4me_views.name as title from mm4me_tables,mm4me_views where mm4me_tables.name=\""+tmp[1]+"\" and mm4me_tables.id=mm4me_views.ptid",[])); + var reg=new RegExp("\\[id\\]","g"); + if(!valuesOnLoad[cid]) + valuesOnLoad[cid]=[]; + if(refs!=""){ + valuesOnLoad[cid].push(refs); + if(!toRunOnLoad[cid]) + toRunOnLoad[cid]=[]; + toRunOnLoad[cid].push(function(){ + prefix="_"+arguments[0]["0"]["id"]; + //if(!mainTable[arguments[0]["0"]["id"]]) + //mainTable[arguments[0]["0"]["id"]]=arguments[0]["0"]["tid"]; + //console.log(arguments[0]["0"]["tid"]+" "+arguments[0]["0"]["name"]+" "+arguments[0]["0"]["title"]); + listInnerTable(arguments[0]["0"]["tid"],arguments[0]["0"]["id"],arguments[0]["0"]["name"],arguments[0]["0"]["title"],false,prefix,tmp[0]+"="+arguments[1]["local_id"]); + }); + refTables[refs["0"]["tid"]]={"oid":cid,"col":tmp[0],"vid":refs["0"]["id"],"name":refs["0"]["table"],"name":refs["0"]["title"]}; + strReturn=data.replace(reg,refs["0"]["tid"]); + } + } + }); + else{ + var tmp=obj["value"].split(";"); + var refs=JSON.parse(window.Android.displayTable("SELECT mm4me_tables.id as tid,mm4me_views.id as id,mm4me_tables.name as name,mm4me_tables.description,mm4me_views.name as title from mm4me_tables,mm4me_views where mm4me_tables.name=\""+tmp[1]+"\" and mm4me_tables.id=mm4me_views.ptid",[])); + var reg=new RegExp("\\[id\\]","g"); + if(!valuesOnLoad[cid]) + valuesOnLoad[cid]=[]; + valuesOnLoad[cid].push(refs); + if(!toRunOnLoad[cid]) + toRunOnLoad[cid]=[]; + toRunOnLoad[cid].push(function(){ + prefix="_"+arguments[0]["0"]["id"]; + //if(!mainTable[arguments[0]["0"]["id"]]) + //mainTable[arguments[0]["0"]["id"]]=arguments[0]["0"]["tid"]; + //console.log(arguments[0]["0"]["tid"]+" "+arguments[0]["0"]["name"]+" "+arguments[0]["0"]["title"]); + listInnerTable(arguments[0]["0"]["tid"],arguments[0]["0"]["id"],arguments[0]["0"]["name"],arguments[0]["0"]["title"],false,prefix,tmp[0]+"="+arguments[1]["local_id"]); + }); + refTables[refs["0"]["tid"]]={"oid":cid,"col":tmp[0],"vid":refs["0"]["id"],"name":refs["0"]["table"],"name":refs["0"]["title"]}; + strReturn=View_template.replace(reg,refs["0"]["tid"]); + + } + //console.log(strReturn); + return strReturn; + } + if(definedSqlTypes[i]["code"]=="ref"){ + var req=obj["value"];//.replace(/^\((\w+)\)$/g,"$1"); + if(req[0]=="(") + req="SELECT * FROM "+req; + var refs=JSON.parse(window.Android.displayTable(cleanupTableName(req),[])); + var tmpStr='"; + + if(obj["dependencies"]) + try{ + var lobj={}; + lobj[obj["id"]]={"dep":JSON.parse(obj["dependencies"])}; + for(var jj=0;jj=0){ + return ''; + } + if(definedSqlTypes[i]["code"]=="html") + return ''; + if(definedSqlTypes[i]["code"]=="text") + return ''; + if(definedSqlTypes[i]["code"]=="boolean") + return ''; + if(definedSqlTypes[i]["code"]=="date" || definedSqlTypes[i]["code"]=="datetime") + return ''; + if(definedSqlTypes[i]["code"]=="float") + return ''; + return definedSqlTypes[i]["code"]; + } + } + return null; +} + +function printOptionalCheckbox(obj,cid){ + if(definedSqlTypes.length==0){ + definedSqlTypes=JSON.parse(window.Android.displayTable("select id,code from mm4me_ftypes where ftype='e' order by name",[])); + } + + var res=' '; + for(var i in definedSqlTypes){ + if(definedSqlTypes[i]["id"]==obj["ftype"]){ + if(definedSqlTypes[i]["code"]=="bytea"){ + var tmpStr=""; + return res; + } + else if(definedSqlTypes[i]["code"]=="geometry"){ + return res + } + } + } + return ''; +} + +var refTypeId=null; +var editPrintedOnce=[]; +/***************************************************************************** + * Create HTML part to display the line containing both the title and the + * corresponding input for a given table's field. + *****************************************************************************/ +function printEditionFields(obj,myRoot,cid,mid){ + var list1=window.Android.displayTable("select * from mm4me_edition_fields where mm4me_edition_fields.edition>0 and eid="+obj["id"]+" order by mm4me_edition_fields.id asc",[]); + if(!editSchema[mid]) + editSchema[mid]={}; + editSchema[mid][obj["id"]]=JSON.parse(list1); + list1=JSON.parse(list1); + myRoot.find(".tab-content").first().append('
    '+obj["description"]+'
    '); + for(var j in list1) + if(list1[j]["edition"]>0) { + myRoot.find(".tab-content").first().children().last().append( + '
    '+ + '
    '+ + ''+ + '
    '+ + '
    '+ + printCurrentType(list1[j],mid)+ + '
    '+ + '
    '); + if(list1[j]["dependencies"]){ + try{ + editPrintedOnce.push(list1[j]["name"]); + console.log("JSON PARSE") + console.log(list1[j]["dependencies"]); + var objJson=JSON.parse(list1[j]["dependencies"]); + console.log("JSON PARSE OK"); + if(!refTypeId) + refTypeId=JSON.parse(window.Android.displayTable("select id from mm4me_ftypes where ftype='e' and code='ref'",[]))[0]["id"]; + console.log(refTypeId); + for(i in objJson){ + if(objJson[i]["myself"]){ + console.log("IS MYSELF!!"); + for(k in objJson[i]["myself"]){ + for(l in objJson[i]["myself"][k]){ + + + if(objJson[i]["myself"][k][l]["dependents"]){ + for(m in objJson[i]["myself"][k][l]["dependents"]){ + for(n in objJson[i]["myself"][k][l]["dependents"][m]){ + console.log(objJson[i]["myself"][k][l]["dependents"][m][n]["sql_query"]); + console.log(objJson[i]["myself"][k][l]["dependents"][m][n]["label"]); + var lObj={"id": n, "ftype":refTypeId,"value":objJson[i]["myself"][k][l]["dependents"][m][n]["sql_query"]}; + //if(!myRoot.find('select[name="field_'+list1[j]["id"]+'"]').parent().find('select[name="field_'+n+'"]').length) + myRoot.find('select[name="field_'+list1[j]["id"]+'"]').last().parent().prepend( + '
    '+ + '
    '+ + ''+ + '
    '+ + '
    '+ + printCurrentType(lObj,mid)+ + '
    '+ + '
    '); + console.log(myRoot.find('select[name="field_'+n+'"]')); + console.log(printCurrentType(lObj,mid)); + (function(a,b){ + myRoot.find('select[name="field_'+n+'"]').off('change'); + myRoot.find('select[name="field_'+n+'"]').on('change',function(){ + console.log('select[name="field_'+n+'"]'); + var req=cleanupTableName(a["value"]); + if(a["value"].indexOf("WHERE")<0){ + req=req.replace(/order by/g,"where "+b["tfieldf"]+" "+b["operator"]+" "+(b["operator"]=="like"?"'":"")+$(this).val()+(b["operator"]=="like"?"'":"")+" order by") + } + console.log(req); + var res=JSON.parse(window.Android.displayTable(req,[])); + myRoot.find('select[name="field_'+a["id"]+'"]').html(""); + for(ij in res){ + var tmpStr=' '; + cnt+=1; + } + cnt0+=1; + $("select[name=field_"+changingField[j][ckey]["id"]+"]").append(cStr); + } + if(i==0) + $("select[name=field_"+changingField[j][ckey]["id"]+"]").html(''); + $("select[name=field_"+changingField[j][ckey]["id"]+"]").change(); + }else{ + console.log("DISPLAY ELEMENT IF CINDEX >=0 "); + //console.log(JSON.stringify(changingField[j][ckey])); + console.log('input[name="field_'+ckey+'"],select[name="field_'+ckey+'"],textarea[name="field_'+ckey+'"]'); + console.log($('input[name="field_'+changingField[j][ckey]["id"]+'"],select[name="field_'+changingField[j][ckey]["id"]+'"],textarea[name="field_'+changingField[j][ckey]["id"]+'"]').parent().parent().html()); + var mycKey=changingField[j][ckey]["id"]; + if(cIndex<0) + $('input[name="field_'+mycKey+'"],select[name="field_'+mycKey+'"],textarea[name="field_'+mycKey+'"]').parent().parent().hide(); + else + $('input[name="field_'+mycKey+'"],select[name="field_'+mycKey+'"],textarea[name="field_'+mycKey+'"]').parent().parent().show(); + } + } + } + }; + }; + $("select[name=field_"+key+"]").off('change'); + $("select[name=field_"+key+"]").change(localFunc(changingFields[i][key]["dep"])); + $("select[name=field_"+key+"]").change(); + } + } + }catch(e){ + console.log(e); + setTimeout(function() { updateChangingFields(changingFields) }, 500); + } +} + +/***************************************************************************** + * Display the content of a table referencing the current edited table. + *****************************************************************************/ +function listInnerTable(id,vid,name,title,init,prefix,clause,ref){ + var list=JSON.parse(window.Android.displayTable("select mm4me_tables.id as tid,mm4me_tables.name as tname,mm4me_editions.id,mm4me_editions.name from mm4me_editions,mm4me_tables where mm4me_editions.ptid=mm4me_tables.id and mm4me_tables.id="+id+" order by mm4me_editions.step asc",[])); + var cnt=0; + var detectInit=true; + var tid=0; + for(var i in list){ + lastEdition[list[i]["id"]]=list[i]; + tid=list[i]["tid"]; + if(!allTables[list[i]["tid"]]){ + allTables[list[i]["tid"]]={"id":list[i]["id"],"name":list[i]["tname"],"title":list[i]["name"]}; + detectInit=false; + $("#sub_tableContent_"+id+"").find(".mm4me_edition").find("ul").first().append(''); + printEditionFields(list[i],$("#sub_tableContent_"+id+"").find(".mm4me_edition"),list[i]["id"],list[i]["tid"]); + if(cnt==0){ + try{ + var cid=list[i]["id"]+"_0"; + $("#sub_tableContent_"+id+"").find("ul").first().append('
  • '+window.Android.translate('table')+'
  • '); + $("#sub_tableContent_"+id+"").find("ul").first().append(''); + $("#sub_tableContent_"+id+"").find("ul").first().append(''); + $("#sub_tableContent_"+id+" .require-select").hide(); + printEditionFields(list[i],$("#sub_tableContent_"+id+""),cid,list[i]["tid"]); + }catch(e){ + console.log("**** ERROR ***> "+e); + } + } + var myRoot=$("#sub_tableContent_"+id+""); + myRoot.find('ul').first().find('a').click(function (e) { + e.preventDefault(); + if($(this).parent().hasClass('require-select')) + myRoot.find('.mm4me_edition').show(); + else + myRoot.find('.mm4me_edition').hide(); + $(this).tab('show'); + }) + myRoot.find('ul').first().find('a').first().click(); + myRoot.find('.mm4me_edition').find('ul').find('a').first().click(); + myRoot.find('.swagEditor').summernote(); + myRoot.find(".mm-act-add").click(function(){ + runInsertQuery($(this).parent().parent(),tid,editTableReact); + }); + myRoot.find(".mm-act-save").click(function(){ + runUpdateQuery($(this).parent().parent(),tid,editTableReact); + }); + + } + cnt+=1; + } + + var list=JSON.parse(window.Android.displayTable("SELECT id,name,value,alias,width,class from mm4me_view_fields where vid="+vid+" order by id",[])); + var columns=[]; + var columnNames=[]; + var sqlColumns=""; + var orderColumn="id"; + var orderType="asc"; + for(var i=0;i0){ + orderColumn=list[i]["name"]; + if(list[i]["class"]==1) + orderType="desc"; + } + } + var ccol=getPKey(cleanupTableName(name)); + var req="SELECT "+ccol+" as ogc_fid FROM "+cleanupTableName(name)+" WHERE "+clause+" ORDER BY "+cleanupTableName(name)+"."+orderColumn+" "+orderType; + var list1=JSON.parse(window.Android.displayTable(req,[])); + req="SELECT "+sqlColumns+" FROM "+cleanupTableName(name)+" WHERE "+clause+" ORDER BY "+cleanupTableName(name)+"."+orderColumn+" "+orderType; + var list=JSON.parse(window.Android.displayTable(req,[])); + var dataSet = []; + for(var i=0;i'; + cnt+=1; + } + dataSet.push(tmpData); + } + var selected = []; + + //$("#edition_form_table").html('
    '); + var localName="exampleTable_"+id; + ((function(localName,sid){ + + if(!detectInit){ + + var options={ + data: dataSet, + columns: columns, + "scrollX": true, + scrollY: '50vh', + scrollCollapse: true, + select: true, + "rowCallback": function( row, data ) { + if ( $.inArray(data.DT_RowId, selected) !== -1 ) { + $(row).addClass('selected'); + } + } + }; + if(langUrl!=null) + options["language"]={ + url: langUrl + }; + + $('#'+localName).DataTable( options ); + $('#'+localName+' tbody').on('click', 'tr', function () { + var id = this.id; + var index = $.inArray(id, selected); + + if ( index === -1 ) { + selected.push( id ); + } else { + selected.splice( index, 1 ); + } + displayEditForm(sid,$(this).find("input[name=id]").first().val(),true); + var tmp=$(this).hasClass('selected'); + $('#'+localName+' tbody tr').removeClass('selected'); + $(this).toggleClass('selected'); + if(!tmp) + $(this).toggleClass('selected'); + } ); + + }else{ + $('#'+localName).dataTable().fnClearTable(); + if(dataSet.length>0) + $('#'+localName).dataTable().fnAddData(dataSet); + } + $("#sub_tableContent_"+id+"").find("ul").first().find('a').first().click(); + })(localName,tid)); + +} + +var onFormFirstLoad=null; +/***************************************************************************** + * Show the edit form + *****************************************************************************/ +function displayEditForm(cid,selectedId,basic){ + if(basic && !$("#exampleTable"+(cid==mtable?"":"_"+cid)).find(".selected").find('input[type=hidden]').first().val()){ + if(cid==mtable){ + $("#main_tableContent").find(".require-select").hide(); + $("#main_tableContent").find("ul").first().find("a").first().click(); + if($(".breadcrumb").children().length==4) + $(".breadcrumb").children().last().remove(); + + }else{ + $("#sub_tableContent_"+cid).find(".require-select").hide(); + $("#sub_tableContent_"+cid).find("ul").first().find("a").first().click(); + } + return; + } + var fields=[] + var sizedFields=[]; + var sizedFieldsAlias=[]; + var notSizedFields=[]; + for(var i in editSchema[cid]){ + for(var j in editSchema[cid][i]){ + //console.log(JSON.stringify(editSchema[cid][i][j])); + if(editSchema[cid][i][j]["ftype"]=="5"){ + sizedFields.push(editSchema[cid][i][j]["name"].replace(/wkb_geometry/g,"geometry")); + sizedFieldsAlias.push(editSchema[cid][i][j]["id"]); + } + else{ + notSizedFields.push(editSchema[cid][i][j]["name"].replace(/wkb_geometry/g,"geometry")+" AS \""+editSchema[cid][i][j]["id"]+"\""); + try{ + var tmp=JSON.parse(editSchema[cid][i][j]["dependencies"]); + var sqlReq=""; + var sqlClause=""; + var sqlParams=""; + var sqlParam=0; + var alphabet = 'abcdefghijklmnopqrstuvwxyz'.split(''); + var hasDep=false; + var previousElements=[]; + for(k in tmp) + for(l in tmp[k]){ + //console.log(JSON.stringify(tmp[k][l])); + if(l=="myself"){ + for(m in tmp[k][l]) + for(n in tmp[k][l][m]){ + //console.log(JSON.stringify(tmp[k][l][m][n])); + if(sqlReq!=""){ + sqlReq+=", "; + sqlParams+=", "; + //sqlClause+=" WHERE "+alphabet[sqlParam-1]+"."+tmp[k][l][m][n]["tfield"]+"="+alphabet[sqlParam]+"."+tmp[k][l][m][n]["tfield"]; + + } + sqlReq+="("+cleanupTableName(tmp[k][l][m][n]["sql_query"])+") as "+alphabet[sqlParam]; + sqlParams+=alphabet[sqlParam]+"."+tmp[k][l][m][n]["tfield"]; + sqlParam+=1; + if(tmp[k][l][m][n]["dependents"]){ + //console.log(JSON.stringify(tmp[k][l][m][n]["dependents"])); + for(var o in tmp[k][l][m][n]["dependents"]){ + //console.log(JSON.stringify(tmp[k][l][m][n]["dependents"][o])); + for(var p in tmp[k][l][m][n]["dependents"][o]){ + console.log(tmp[k][l][m][n]["dependents"][o][p]["sql_query"].replace(/from/,","+tmp[k][l][m][n]["dependents"][o][p]["tfield"]+" from")); + sqlReq+=", ("+cleanupTableName(tmp[k][l][m][n]["dependents"][o][p]["sql_query"]).replace(/from/,","+tmp[k][l][m][n]["dependents"][o][p]["tfield"]+" from")+") as b"; + sqlParams+=", b."+tmp[k][l][m][n]["dependents"][o][p]["tfieldf"]; + sqlParam+=2; + sqlReq+=", ("+cleanupTableName(editSchema[cid][i][j]["value"]).replace(/from/,","+tmp[k][l][m][n]["dependents"][o][p]["tfieldf"]+" from")+") as c"; + + sqlClause+=" WHERE c."+tmp[k][l][m][n]["dependents"][o][p]["tfieldf"]+"=b."+tmp[k][l][m][n]["dependents"][o][p]["tfieldf"]; + sqlClause+=" AND b."+tmp[k][l][m][n]["tfield"]+"=a."+tmp[k][l][m][n]["tfield"]; + sqlClause+=" AND c.id=(SELECT "+editSchema[cid][i][j]["name"]+" FROM "+cleanupTableName(allTables[cid].name)+" where id="+selectedId+")"; + hasDep=true; + } + } + } + } + if(!hasDep){ + var tmpReq=cleanupTableName(editSchema[cid][i][j]["value"]); + for(m in tmp[k][l]) + for(n in tmp[k][l][m]){ + tmpReq=tmpReq.replace(/from/,","+tmp[k][l][m][n]["tfield"]+" from"); + if(sqlClause=="") + sqlClause+=" WHERE "; + else + sqlClause+=" "+tmp[k][l][m][n]["cond_join"]+" "; + sqlClause+=alphabet[m]+"."+tmp[k][l][m][n]["tfield"]+"=a1."+tmp[k][l][m][n]["tfield"]; + sqlClause+=""; + } + sqlClause+=" AND a1.id=(SELECT "+editSchema[cid][i][j]["name"]+" FROM "+cleanupTableName(allTables[cid].name)+" where id="+selectedId+")"; + //sqlReq=(tmpReq); + console.log(tmpReq); + sqlReq+=", ("+tmpReq+") as a1"; + } + //console.log(JSON.stringify(tmp[k][l])); + console.log("SELECT "+sqlParams+" FROM "+sqlReq+" "+sqlClause); + var localQuery="SELECT "+sqlParams+" FROM "+sqlReq+" "+sqlClause; + var res0=JSON.parse(window.Android.displayTable(localQuery,[])); + console.log(JSON.stringify(res0)); + for(m in res0) + for(n in res0[m]){ + try{ + console.log("input[name=field_"+n+"],select[name=field_"+n+"],textarea[name=field_"+n+"]"); + /**/ + if($('.mm4me_edition').find("input[name=field_"+n+"],select[name=field_"+n+"],textarea[name=field_"+n+"]").first().length) + $('.mm4me_edition').find("input[name=field_"+n+"],select[name=field_"+n+"],textarea[name=field_"+n+"]").first().val(res0[m][n]).change(); + else{ + for(m0 in tmp[k][l]) + for(n0 in tmp[k][l][m0]){ + /*console.log(n0.indexOf(n)<0); + console.log(n); + console.log(n0); + console.log(res0[m][n]);*/ + if(n0.indexOf(n)>=0){ + console.log("input[name=field_"+n0+"],select[name=field_"+n0+"],textarea[name=field_"+n0+"]"); + if(!$('.mm4me_edition').find("input[name=field_"+n+"],select[name=field_"+n+"],textarea[name=field_"+n+"]").first().length) + $('.mm4me_edition').find("input[name=field_"+n0+"],select[name=field_"+n0+"],textarea[name=field_"+n0+"]").first().val(res0[m][n]).change(); + } + } + } + }catch(e){ + console.log(e); + } + + } + //if(tmp[k][l]["dependents"]) + }else{ + console.log(JSON.stringify(tmp[k][l])); + if(tmp[k][l]["tfield"]=="none"){ + var isNull=JSON.parse(window.Android.displayTable("SELECT CASE WHEN "+l+" is null THEN 1 ELSE 0 END as p FROM "+cleanupTableName(allTables[cid].name)+" where id="+selectedId,[])); + console.log(JSON.stringify(isNull)); + if(isNull[0]["p"]=="0"){ + console.log(JSON.stringify(isNull)); + console.log("input[name=field_"+editSchema[cid][i][j]["id"]+"],select[name=field_"+editSchema[cid][i][j]["id"]+"],textarea[name=field_"+editSchema[cid][i][j]["id"]+"]"); + console.log(k+""); + $('.mm4me_edition').find("input[name=field_"+editSchema[cid][i][j]["id"]+"],select[name=field_"+editSchema[cid][i][j]["id"]+"],textarea[name=field_"+editSchema[cid][i][j]["id"]+"]").first().val(k+"").change(); + } + } + + } + } + + //if(sqlReq) + }catch(e){ + console.log(e); + } + } + if(editSchema[cid][i][j]["name"].indexOf("unamed")<0) + fields.push(editSchema[cid][i][j]["name"].replace(/wkb_geometry/g,"geometry")+" AS \""+editSchema[cid][i][j]["id"]+"\""); + } + } + var ccol=getPKey(cleanupTableName(allTables[cid].name)); + /* $("#exampleTable"+(cid==mtable?"":"_"+cid)).find(".selected").find('input[type=hidden]').first().val() */ + var editValues; + if(sizedFields.length>0){ + for(var i=0;i 1000000 and "+ccol+"="+selectedId,[])); + //console.log(JSON.stringify(hasElement[0])); + if(hasElement[0]["cnt"]!="0"){ + var nbIteration=parseInt(hasElement[0]["len"])/1000000; + var zfields=[] + var len=0; + var query=""; + var len1=parseInt(hasElement[0]["len"]); + for(var j=0;j0){ + $("#field_"+j+"_map").show(); + $("#field_"+j+"_map").off('click'); + $("#field_"+j+"_map").on('click',function(){ + console.log("Display table with a selected feature"); + console.log($(this).prev().val()); + showElementOnMap($(this).prev().val()); + }); + } + if($(".btn_field_"+j+"_lat").length>0){ + //alert(editValues[i][j]); + $(".btn_field_"+j+"_lat").html(editValues[i][j]); + $(".btn_field_"+j+"_long").html(""); + $(".btn_field_"+j+"_source").html(""); + $("input[name='field_"+j+"']").val("POINT"+editValues[i][j]); + + } + } + //$(".swagEditor").summernote(); + } + } + + for(var i in editSchema[cid]){ + for(var j in editSchema[cid][i]){ + if(editSchema[cid][i][j]["name"].indexOf("unamed")>=0){ + if(editSchema[cid][i][j]["ftype"]==6){ + var tmp=editSchema[cid][i][j]["value"].split(';'); + var list=JSON.parse(window.Android.displayTable("select "+tmp[1]+" from "+cleanupTableName(tmp[2])+" where "+tmp[0]+"=(SELECT id from "+cleanupTableName(tblName)+" WHERE ogc_fid="+selectedId+")",[])); + editValues["0"][editSchema[cid][i][j]["id"]]=list; + for(var k in list){ + for(var l in list[k]){ + $('.mm4me_edition').find("select[name=field_"+editSchema[cid][i][j]["id"]+"]").find('option').each(function(){ + if($(this).val()==list[k][l]) + $(this).prop("selected",true); + }); + } + } + } + } + } + } + + if(cid==mtable){ + if($(".breadcrumb").children().length==4) + $(".breadcrumb").children().last().remove(); + $(".breadcrumb").append('
  • '+editValues["0"]["local_id"]+'
  • '); + } + ((cid==mtable)?$(".require-select"):$("#sub_tableContent_"+cid).find(".require-select")).first().show(); + ((cid==mtable)?$(".require-select"):$("#sub_tableContent_"+cid).find(".require-select")).first().find("a").first().click(); + + if(toRunOnLoad[cid]) + for(var i=0;i=0 order by mm4me_editions.step asc",[]); + if(MM4ME_DEBUG) + console.log(list); + list=JSON.parse(list); + if(MM4ME_DEBUG) + console.log((!allTables[tblId])); + if(!allTables[tblId]){ + allTables[tblId]={"id":id,"name":name,"title":title}; + } + mtable=tblId; + + $(".mm4me_edition").find("ul").first().html(""); + $(".mm4me_edition").find(".well").first().html(""); + var cnt=0; + for(var i in list){ + lastEdition[list[i]["id"]]=list[i]; + var cid=(i==0?list[i]["id"]+"_0":list[i]["id"]); + $(".mm4me_edition").find("ul").first().append(''); + printEditionFields(list[i],$("#edition_form_edit"),cid,mainTable[id]); + } + if(list.length==1) + $(".mm4me_edition").find("ul").first().hide(); + + var aCnt=0; + $('.mm4me_edition').find('ul').first().find('a').each(function () { + if(aCnt>0) + $(this).parent().addClass('require-select'); + aCnt+=1; + }); + $(".require-select").hide(); + $('.mm4me_listing').find('ul').first().find('a').first().click(); + $('.mm4me_edition').find('ul').find('a').first().click(); + $('.swagEditor').summernote(); + $(".mm-act-add").click(function(){ + runInsertQuery($(this).parent().parent(),mainTable[id],editOnlyTableReact); + }); + $(".mm-act-save").click(function(){ + runUpdateQuery($(this).parent().parent(),mainTable[id],editOnlyTableReact); + }); + $(".breadcrumb").children().last().remove(); + $(".breadcrumb").append('
  • '+window.Android.translate('edit')+'
  • '); + $(".breadcrumb").append('
  • '+tblTitle+'
  • '); + + setTimeout(function() { updateChangingFields(changingFields) }, 1500); + + /*for(var i=0;i'; + else + cStr+=changingField[j][ckey]["values"][cIndex][i][lkey]+''; + cnt+=1; + } + $("select[name=field_"+changingField[j][ckey]["id"]+"]").append(cStr); + } + if(i==0) + $("select[name=field_"+changingField[j][ckey]["id"]+"]").html(''); + } + } + }; + }; + $("select[name=field_"+key+"]").off('change'); + $("select[name=field_"+key+"]").change(localFunc(changingFields[i][key]["dep"])); + $("select[name=field_"+key+"]").change(); + } + }*/ + $('.mm4me_listing').show(); + $('.mm4me_content').hide(); + +} + +/***************************************************************************** + * In case no server is configured + *****************************************************************************/ +function displayNoListing(){ + $.ajax({ + method: "GET", + url: 'content/nolisting.html', + success: function(data){ + if(MM4ME_DEBUG) + console.log('Display warning message on the UI !'); + $(".mm4me_content").html(data); + $(".mm4me_content").find(".pannel-body").find("p").first().html(window.Android.translate("mm_no_db_found")); + }, + error: function(){ + window.Android.showToast("error !"); + } + }); +} + + +/***************************************************************************** + * Authenticate a user + *****************************************************************************/ +function authenticate(url,login,passwd,func,func1){ + var curl=url+"?service=WPS&request=Execute&version=1.0.0&Identifier=authenticate.clogIn&DataInputs=login="+login+";password="+passwd+"&RawDataOutput=Result"; + if(MM4ME_DEBUG) + console.log(curl); + $.ajax({ + method: "GET", + url: curl, + success: function(data){ + if(MM4ME_DEBUG) + console.log(data); + if(func){ + console.log("Call func!") + func(); + } + }, + error: function(){ + if(func1){ + func1(); + } + else{ + disconnect(url); + if(MM4ME_DEBUG) + console.log("unable to login!"); + var hasBeenShown=false; + var xml=arguments[0].responseText; + $(xml).find("ows\\:ExceptionText").each(function(){ + window.Android.showToast($(this).text()); + hasBeenShown=true; + }); + if(!hasBeenShown){ + window.Android.showToast(JSON.stringify(arguments)); + } + } + + } + }); +} + +/***************************************************************************** + * Disconnect a user + *****************************************************************************/ +function disconnect(url,func,func1){ + var curl=url+"?service=WPS&request=Execute&version=1.0.0&Identifier=authenticate.clogOut&DataInputs=&RawDataOutput=Result"; + if(MM4ME_DEBUG) + console.log(curl); + $.ajax({ + method: "GET", + url: curl, + success: function(data){ + if(MM4ME_DEBUG){ + console.log(data); + console.log("** Your are no more connected!"); + } + if(func){ + func(); + } + }, + error: function(){ + if(MM4ME_DEBUG){ + console.log(curl); + console.log("unable to disconnect!"); + } + if(func1){ + func1(); + } + } + }); +} + +var geometries={"line":{"geom":null,"constructor": "ol.geom.Linestring"},"polygon":{"geom":null,"constructor":"ol.geom.Polygon","cline":null}}; +var stopTracking=false; +var currentGeometry="line"; +var currentGeometryField="none"; +var map=null; +var vectorLayer,vectorLayer1; +var position; + +/***************************************************************************** + * Track modification of the GPS location + *****************************************************************************/ +function trackCurrentGPSPosition(){ + console.log("## trackCurrentGPSPosition"); + updateCurrentMapLocation(); + + var tmp0=geometries["origin"].getCoordinates(); + if(geometries[currentGeometry]["geom"]!=null){ + var tmp1=geometries[currentGeometry]["geom"].getCoordinates(); + tmp0=tmp1[tmp1.length-1]; + } + tmp=ol.proj.transform([position[bestIndex].lon,position[bestIndex].lat], 'EPSG:4326','EPSG:3857'); + console.log(JSON.stringify(tmp0)); + console.log(JSON.stringify(tmp)); + $("#currentPosition").html("Position: "+position[bestIndex].lon.toFixed(6)+","+position[bestIndex].lat.toFixed(6)+""); + if(tmp0[0]==tmp[0] && tmp0[1]==tmp[1]){ + console.log(" !!!!!!!! Same position!!!!!!!! "); + if(!stopTracking) + setTimeout(function() { trackCurrentGPSPosition();}, 1000); + return; + } + + console.log(JSON.stringify(tmp0)); + console.log(JSON.stringify(tmp)); + if(geometries[currentGeometry]["geom"]==null){ + //geometries[currentGeometry]=new geometries[currentGeometry]["constructor"]([tmp0,tmp]); + if(currentGeometry=="line") + geometries[currentGeometry]["geom"]=new ol.geom.LineString([tmp0,tmp]); + else + geometries[currentGeometry]["geom"]=new ol.geom.LineString([tmp0,tmp]); + }else{ + if(currentGeometry=="line"){ + tmpCoordinates=geometries[currentGeometry]["geom"].getCoordinates(); + tmpCoordinates.push(tmp); + geometries[currentGeometry]["geom"]=new ol.geom.LineString(tmpCoordinates); + } + else{ + if(geometries[currentGeometry]["cline"]==null) + tmpCoordinates=geometries[currentGeometry]["geom"].getCoordinates(); + else + tmpCoordinates=geometries[currentGeometry]["cline"].getCoordinates(); + console.log(JSON.stringify(tmpCoordinates)); + tmpCoordinates.push(tmp); + console.log(JSON.stringify(tmpCoordinates)); + if(tmpCoordinates.length>2){ + geometries[currentGeometry]["cline"]=new ol.geom.LineString(tmpCoordinates); + tmpCoordinates.push(tmpCoordinates[0]); + console.log(JSON.stringify(tmpCoordinates)); + geometries[currentGeometry]["geom"]=new ol.geom.Polygon(); + console.log(JSON.stringify(tmpCoordinates)); + geometries[currentGeometry]["geom"].appendLinearRing(new ol.geom.LinearRing(tmpCoordinates)); + console.log(JSON.stringify(tmpCoordinates)); + } + } + + } + console.log(JSON.stringify(tmp0)); + console.log(JSON.stringify(tmp)); + try{ + console.log(JSON.stringify(geometries[currentGeometry]["geom"].getCoordinates())); + var feature=new ol.Feature({geometry: geometries[currentGeometry]["geom"],"name":"myFeature"}); + vectorLayer1.getSource().clear(); + vectorLayer1.getSource().addFeatures([feature]); + var wkt=new ol.format.WKT(); + var tmpGeometry=geometries[currentGeometry]["geom"].clone().transform('EPSG:3857', 'EPSG:4326'); + console.log(wkt.writeGeometry(tmpGeometry)); + $("input[name='"+currentGeometryField+"']").val(wkt.writeGeometry(tmpGeometry)); + }catch(e){ + console.log(e); + } + console.log(JSON.stringify(tmp0)); + console.log(JSON.stringify(tmp)); + if(!stopTracking) + setTimeout(function() { trackCurrentGPSPosition();}, 1000); + console.log(JSON.stringify(tmp0)); + console.log(JSON.stringify(tmp)); +} + +var modalCallback=null; + +function trackStepCurrentGPSPosition(){ + console.log("## trackStepCurrentGPSPosition"); + updateCurrentMapLocation(); + $("#currentPosition").html("Position: "+position[bestIndex].lon.toFixed(6)+","+position[bestIndex].lat.toFixed(6)+""); + if(!stopTracking) + setTimeout(function() { trackStepCurrentGPSPosition();}, 1000); +} + +function addCurrentLocation(){ + console.log("addCurrentLocation!!!!"); + if(geometries["origin"]==null) + geometries["origin"]=new ol.geom.Point(ol.proj.transform([position[bestIndex].lon,position[bestIndex].lat], 'EPSG:4326','EPSG:3857')); + else{ + var tmp0=geometries["origin"].getCoordinates(); + if(geometries[currentGeometry]["geom"]!=null){ + var tmp1=geometries[currentGeometry]["geom"].getCoordinates(); + tmp0=tmp1[tmp1.length-1]; + } + tmp=ol.proj.transform([position[bestIndex].lon,position[bestIndex].lat], 'EPSG:4326','EPSG:3857'); + console.log(JSON.stringify(tmp0)); + console.log(JSON.stringify(tmp)); + $("#currentPosition").html("Position: "+position[bestIndex].lon.toFixed(6)+","+position[bestIndex].lat.toFixed(6)+""); + if(geometries[currentGeometry]["geom"]==null){ + //geometries[currentGeometry]=new geometries[currentGeometry]["constructor"]([tmp0,tmp]); + if(currentGeometry=="line") + geometries[currentGeometry]["geom"]=new ol.geom.LineString([tmp0,tmp]); + else + geometries[currentGeometry]["geom"]=new ol.geom.LineString([tmp0,tmp]); + }else{ + if(currentGeometry=="line"){ + tmpCoordinates=geometries[currentGeometry]["geom"].getCoordinates(); + tmpCoordinates.push(tmp); + geometries[currentGeometry]["geom"]=new ol.geom.LineString(tmpCoordinates); + } + else{ + if(geometries[currentGeometry]["cline"]==null) + tmpCoordinates=geometries[currentGeometry]["geom"].getCoordinates(); + else + tmpCoordinates=geometries[currentGeometry]["cline"].getCoordinates(); + console.log(JSON.stringify(tmpCoordinates)); + tmpCoordinates.push(tmp); + console.log(JSON.stringify(tmpCoordinates)); + if(tmpCoordinates.length>2){ + geometries[currentGeometry]["cline"]=new ol.geom.LineString(tmpCoordinates); + tmpCoordinates.push(tmpCoordinates[0]); + console.log(JSON.stringify(tmpCoordinates)); + geometries[currentGeometry]["geom"]=new ol.geom.Polygon(); + console.log(JSON.stringify(tmpCoordinates)); + geometries[currentGeometry]["geom"].appendLinearRing(new ol.geom.LinearRing(tmpCoordinates)); + console.log(JSON.stringify(tmpCoordinates)); + } + } + + } + try{ + console.log(JSON.stringify(geometries[currentGeometry]["geom"].getCoordinates())); + var feature=new ol.Feature({geometry: geometries[currentGeometry]["geom"],"name":"myFeature"}); + vectorLayer1.getSource().clear(); + vectorLayer1.getSource().addFeatures([feature]); + var wkt=new ol.format.WKT(); + var tmpGeometry=geometries[currentGeometry]["geom"].clone().transform('EPSG:3857', 'EPSG:4326'); + console.log(wkt.writeGeometry(tmpGeometry)); + $("input[name='"+currentGeometryField+"']").val(wkt.writeGeometry(tmpGeometry)); + }catch(e){ + console.log(e); + } + + } +} + +function trackStepByStepPosition(elem,ltype){ + modalCallback=function(){ + //$("#myModal").find(".glyphicon-ok").parent().show(); + if(myVectorLayer) + myVectorLayer.getSource().clear(); + geometries["origin"]=null; + setTimeout(function() { trackStepCurrentGPSPosition();}, 1000); + $("#myModal").find("h4").html(' '+window.Android.translate("gps_track")); + $("#myModal").find(".modal-footer").html( + ''+ + ''+ + '' + ); + }; + currentGeometry=ltype; + currentGeometryField=elem; + geometries[ltype]["geom"]=null; + geometries[ltype]["cline"]=null; + if(!$("#map").length) + $.ajax({ + method: "GET", + url: './map-modal.html', + error: function(){ + }, + success: function(data){ + console.log(data); + $("body").append(data); + $("#map").css("height",($(window).height()-220)+"px"); + $('#myModal').modal(); + addOptionalLocalTiles(true); + $('#myModal').on('shown.bs.modal', function () { + console.log($("#mmm4me_ls").length); + initMapToLocation(); + localTileIndex=map.getLayers().getLength(); + /*geometries["origin"]=new ol.geom.Point(ol.proj.transform([position[bestIndex].lon,position[bestIndex].lat], 'EPSG:4326','EPSG:3857')); + setTimeout(function() { trackCurrentGPSPosition();}, 1000);*/ + modalCallback(); + }); + $('#myModal').on('hide.bs.modal', function () { + console.log("HIDE"); + stopTracking=true; + }); + } + }); + else{ + stopTracking=false; + $('#myModal').modal(); + vectorLayer1.getSource().clear(); + updateCurrentMapLocation(); + geometries["origin"]=null; + setTimeout(function() { trackStepCurrentGPSPosition();}, 1000); + } +} +/***************************************************************************** + * Start tracking GPS location + *****************************************************************************/ +function trackGPSPosition(elem,ltype){ + modalCallback=function(){ + //$("#myModal").find(".glyphicon-ok").parent().show(); + if(myVectorLayer) + myVectorLayer.getSource().clear(); + geometries["origin"]=new ol.geom.Point(ol.proj.transform([position[bestIndex].lon,position[bestIndex].lat], 'EPSG:4326','EPSG:3857')); + setTimeout(function() { trackCurrentGPSPosition();}, 1000); + $("#myModal").find("h4").html(' '+window.Android.translate("gps_track")); + $("#myModal").find(".modal-footer").html( + ''+ + '' + ); + }; + currentGeometry=ltype; + currentGeometryField=elem; + geometries[ltype]["geom"]=null; + geometries[ltype]["cline"]=null; + if(!$("#map").length) + $.ajax({ + method: "GET", + url: './map-modal.html', + error: function(){ + }, + success: function(data){ + console.log(data); + $("body").append(data); + $("#map").css("height",($(window).height()-220)+"px"); + $('#myModal').modal(); + addOptionalLocalTiles(true); + $('#myModal').on('shown.bs.modal', function () { + console.log($("#mmm4me_ls").length); + initMapToLocation(); + localTileIndex=map.getLayers().getLength(); + /*geometries["origin"]=new ol.geom.Point(ol.proj.transform([position[bestIndex].lon,position[bestIndex].lat], 'EPSG:4326','EPSG:3857')); + setTimeout(function() { trackCurrentGPSPosition();}, 1000);*/ + modalCallback(); + }); + $('#myModal').on('hide.bs.modal', function () { + console.log("HIDE"); + stopTracking=true; + }); + } + }); + else{ + stopTracking=false; + $('#myModal').modal(); + vectorLayer1.getSource().clear(); + updateCurrentMapLocation(); + geometries["origin"]=new ol.geom.Point(ol.proj.transform([position[bestIndex].lon,position[bestIndex].lat], 'EPSG:4326','EPSG:3857')); + setTimeout(function() { trackCurrentGPSPosition();}, 1000); + } +} + +var hasAddedElement=0; +var myVectorLayer=null; +function addSelectedElement(wktString){ + var features=[]; + try{ + var format = new ol.format.WKT(); + features.push( + format.readFeature(wktString, { + dataProjection: 'EPSG:4326', + featureProjection: 'EPSG:3857' + }) + ); + }catch(e){ + console.log("Unable to parse WKT ?!"+e) + } + if(!vectorLayer1){ + myVectorLayer = new ol.layer.Vector({ + source: new ol.source.Vector({ + features: features + }), + style: new ol.style.Style({ + stroke: new ol.style.Stroke({ + color: "#3333aa", + width: 1.4 + }) + }) + }); + map.addLayer(myVectorLayer); + } + else{ + vectorLayer1.getSource().clear(); + vectorLayer1.getSource().addFeatures(features); + } + console.log(vectorLayer1.getSource().getExtent()); + map.updateSize(); + map.getView().fit(vectorLayer1.getSource().getExtent(),map.getSize()); + hasAddedElement=1; +} + +/***************************************************************************** + * Show selected feature on map + *****************************************************************************/ + var addSelectedIndex=0; + var mySelectedElement=null; +function showElementOnMap(wktString){ + mySelectedElement=wktString; + modalCallback=function(){ + //$("#myModal").find(".glyphicon-ok").parent().hide(); + if(addSelectedIndex==0){ + updateCurrentMapLocation(); + $("#currentPosition").html("Position: "+position[bestIndex].lon.toFixed(6)+","+position[bestIndex].lat.toFixed(6)+""); + } + addSelectedElement(mySelectedElement); + $("#myModal").find("h4").html(' '+window.Android.translate("view_feature")); + addSelectedIndex++; + }; + //$("#currentPosition").html("Position: "+position[bestIndex].lon.toFixed(6)+","+position[bestIndex].lat.toFixed(6)+""); + if(!$("#map").length) + $.ajax({ + method: "GET", + url: './map-modal.html', + error: function(){ + }, + success: function(data){ + console.log(data); + $("body").append(data); + $("#map").css("height",($(window).height()-220)+"px"); + $('#myModal').modal(); + addOptionalLocalTiles(true); + $('#myModal').on('shown.bs.modal', function () { + console.log($("#mmm4me_ls").length); + initMapToLocation(); + localTileIndex=map.getLayers().getLength(); + modalCallback(); + }); + $('#myModal').on('hide.bs.modal', function () { + console.log("HIDE"); + stopTracking=true; + }); + } + }); + else{ + console.log("OK "); + $('#myModal').modal(); + console.log("OK "); + vectorLayer1.getSource().clear(); + console.log("OK "); + updateCurrentMapLocation(); + console.log("OK "); + addSelectedElement(wktString); + console.log("OK "); + } + + $("#currentPosition").html("Position: "+position[bestIndex].lon.toFixed(6)+","+position[bestIndex].lat.toFixed(6)+""); +} + + +/***************************************************************************** + * Store the current GPS location in the edit form + *****************************************************************************/ +function requireGPSPosition(elem){ + var position=JSON.parse(window.Android.getGPS()); + //elem=$("#"+elem); + if(position.lat){ + $(".btn_"+elem+"_lat").html(position.lat); + $(".btn_"+elem+"_long").html(position.lon); + $(".btn_"+elem+"_source").html(position.source); + $("input[name='"+elem+"']").val("POINT("+position.lon+" "+position.lat+")"); + }else{ + + } + console.log(JSON.stringify(position)); +} + +/***************************************************************************** + * Ajax setup to ensure seting "withCredentials" to true + *****************************************************************************/ +function ajaxSetup(){ + $.ajaxSetup({ + xhrFields: { + withCredentials: true + } + }); +} + +/***************************************************************************** + * Get the current Network and GPS availability + *****************************************************************************/ +function getCurrentStatus(){ + var tmp=JSON.parse(window.Android.getGNStatus()); + tmp["gps"]=JSON.parse(tmp["gps"]); + updateStatus(tmp['gps'],tmp['net']); +} + +/***************************************************************************** + * Display the status icons + *****************************************************************************/ +function addStatusControl(){ + $('.breadcrumb').append(' /'+ + ''+ + ''+ + ''+ + ''+ + ''); +} + +/***************************************************************************** + * Initialize the map and show the current GPS location + *****************************************************************************/ +var localTiles,localTiles0; +var tileLayers=[]; +function initMapToLocation(){ + if(map) + return; + var osmSource = new ol.source.OSM(); + + var otherLayers=[]; + var otherLayersSwitcher=""; + try{ + var BasesLayersStr=window.Android.getBaseLayers(); + console.log(BasesLayersStr); + var BasesLayers=JSON.parse(BasesLayersStr); + for(var i=0;i=4) + map.getLayers().item(otherLayers[i]["index"]).setVisible(false); + }else{ + $(this).parent().find("i").removeClass("glyphicon-eye-close").addClass("glyphicon-eye-open"); + $("#dmopacity_"+i).show(); + console.log(JSON.stringify(map.getLayers().getLength())); + if(!otherLayers[i]["index"]){ + var myTileLayer=new ol.layer.Tile({source: otherLayers[i]["olLayer"]}); + var layers = map.getLayers(); + layers.insertAt(tileLayers.length+1,myTileLayer); + otherLayers[i]["index"]=tileLayers.length+1; + tileLayers.push(myTileLayer); + if(localTileIndex>0) + localTileIndex+=1; + } + else + map.getLayers().item(otherLayers[i]["index"]).setVisible(true); + } + }); + $(".map").parent().find("input[type=range]").last().on('change',function(){ + console.log("change to "+$(this).val()); + map.getLayers().item(otherLayers[i]["index"]).setOpacity($(this).val()/100); + }); + + })(i); + $(".map").parent().find("input[type=checkbox]").parent().off('click'); + $(".map").parent().find("input[type=checkbox]").parent().on('click',function(){ + var tmp=$(this).find("input[type=checkbox]"); + if(tmp.is(":checked")){ + $(this).find("input[type=checkbox]").prop("checked",false).change(); + $(this).addClass("select"); + } + else{ + $(this).find("input[type=checkbox]").prop("checked",true).change(); + $(this).removeClass("select"); + } + }); + + },1); + })(i) + } + } + map = new ol.Map({ + layers: layers, + target: 'map', + controls: ol.control.defaults({ + attributionOptions: /** @type {olx.control.AttributionOptions} */ ({ + collapsible: true + }) + }), + view: new ol.View({ + center: ol.proj.transform([0,0], 'EPSG:4326', 'EPSG:3857'), + zoom: 14, + maxZoom: 22, + minZoom: 1 + }) + }); + + var bstyle = function (feature, resolution) { + + /*var iconFont = 'glyphicon'; + var iconFontText = '\e062';*/ + //var iconFont = 'glyphicon'; + var iconFont = 'Glyphicons Halflings'; + var iconFontText = '\ue062'; + var iconSize = 24; + var opacity=0.4; + var col = 'rgba(0,255,0,0.6)'; + if(feature.get("source")=="GPS") + col = 'rgba(41,136,54,0.5)';//#298836 + else if(feature.get("source")=="Network"){ + col = 'rgba(91,176,75,0.4)';//#5bb04b + iconSize = 32; + opacity=0.2; + } + else if(feature.get("source")=="other"){ + col='rgba(129,208,113,0.5)';//#81d071 + iconSize = 36; + opacity=0.2; + } + else + col='rgba(166,63,39,0.5)'; //#a63f27 //"#EE0000"; + var styles = []; + + var styleIcon = new ol.style.Style({ + text: new ol.style.Text({ + font: 'Normal ' + iconSize + 'px ' + iconFont, + text: iconFontText, + fill: new ol.style.Fill({ color: col }) + }) + }); + styles.push(styleIcon); + + //console.log(feature.get("type")); + return styles; + /*return function (feature, resolution) { + styles.styleIcon.getText().setText(feature.get("iconCode")); + return styles; + };*/ + }; + position=JSON.parse(window.Android.getFullGPS()); + //console.log(JSON.stringify(position)); + if(position.length==0){ + position=[{lon:3.5,lat:43.5,source:"none"}]; + } + var iconFeatures = []; + for(var i=0;i=4) + map.getLayers().item(localTileIndex).setVisible(false); + }else{ + $(this).parent().find("i").removeClass("glyphicon-eye-close").addClass("glyphicon-eye-open"); + $("#dmopacity").show(); + console.log(JSON.stringify(map.getLayers().getLength())); + if(map.getLayers().getLength()0.05){ + map.getView().setRotation(-direction); + oldBearer=direction; + } + }else{ + window.Android.stopReportDirection(); + } +} \ No newline at end of file diff --git a/app/src/main/assets/scripts/gene3.js b/app/src/main/assets/scripts/gene3.js new file mode 100644 index 0000000..323dc08 --- /dev/null +++ b/app/src/main/assets/scripts/gene3.js @@ -0,0 +1,2461 @@ +var MM4ME_DEBUG=true; +var EDITION_TYPE_FILE=5; +var mainTable={}; +var mtable=null; +var refTables={}; +var tblId=null; +var allTables={}; +var editSchema={}; +var referenceIds={}; +var lastEdition={}; +var tblName=null; +var toRunOnLoad={}; +var valuesOnLoad=[]; +var definedSqlTypes=[]; +var changingFields=[]; +var lang=window.Android.getLang(); +var langUrl=null; +if(lang=="fr"){ + langUrl="file:///android_asset/localisation/French.json"; +} + + + + +function loadWelcome4(){ + window.Android.startWelcomeScreen("cloud"); +} + +/***************************************************************************** + * Update status icon color depending on network and GPS availability + *****************************************************************************/ +function updateStatus(gps,internet){ + if(internet){ + $('.glyphicon-signal').css('color','#00EE00'); + }else + $('.glyphicon-signal').css('color','#EE0000'); + $('#gpsMenu1').next().html(""); + if(gps.length==0){ + $('#gpsMenu1').css('color','#EE0000'); + $('#gpsMenu1').next().html('
  • '+window.Android.translate("no_gps")+'
  • '); + }else{ + $('#gpsMenu1').css('color','#00EE00'); + for(var i=0;i '+gps[i].source+''); + } +} + +/***************************************************************************** + * List all available table for a given theme + *****************************************************************************/ +function fetchTableForTheme(obj,id){ + var hasTable=false; + var tables=JSON.parse(window.Android.displayTable( + "SELECT mm4me_tables.id as tid,mm4me_views.id as id,"+ + "mm4me_tables.name as name, "+ + "mm4me_tables.description, "+ + "mm4me_views.name as title "+ + " from mm4me_tables,mm4me_views"+ + " where mm4me_tables.id=mm4me_views.ptid "+ + "and mm4me_views.visible "+ + "and mm4me_views.id in (select vid from mm4me_views_themes where tid="+id+")",[])); + for(var i=0;i0; +} + +/***************************************************************************** + * Create a JSON Object containing the themes hierarchy + *****************************************************************************/ +function fetchThemes(obj,id){ + var cthemes=JSON.parse(window.Android.displayTable("SELECT id,name FROM mm4me_themes where pid = "+id,[])); + for(var i=0;i'); + $('#tree').treeview({ + data: allThemes, + onNodeSelected: function(event, data) { + if(data["myId"]){ + try{ + func(data["myId"],data["myName"],data["myTitle"],true); + }catch(e){ + console.log(JSON.stringify(data)); + console.log(e); + window.Android.showToast(e); + //exit(); + } + } + else{ + $('#tree').treeview('toggleNodeExpanded',[$("#tree").treeview('getSelected')[0], { silent: true }]); + $('#tree').treeview('toggleNodeSelected',[$("#tree").treeview('getSelected')[0], { silent: true }]); + //console.log(JSON.stringify(data)); + //$(this).open(); + } + }, + showTags: true + }); + var list=JSON.parse(window.Android.displayTable("SELECT mm4me_tables.id as tid,mm4me_views.id as id,mm4me_tables.name as name,mm4me_tables.description,mm4me_views.name as title from mm4me_tables,mm4me_views where mm4me_tables.id=mm4me_views.ptid and mm4me_views.visible",[])); + var tableList=list; + var total=0; + contents=[]; + for(var i in list){ + mainTable[list[i]["id"]]=list[i]["tid"]; + } + } + catch(e){ + console.log(e); + displayNoListing(); + } +} + +/***************************************************************************** + * Update the breadcrumbs text to translated string + *****************************************************************************/ +function updateBreadcrumbs(breadcrumbs){ + //var breadcrumbs=["home","view"]; + var lcnt0=0; + $('.breadcrumb').find("a").each(function(){ + $(this).append(window.Android.translate(breadcrumbs[lcnt0])); + lcnt0+=1; + }); + $('.breadcrumb').find("li").last().each(function(){ + $(this).append(window.Android.translate(breadcrumbs[lcnt0])); + lcnt0+=1; + }); +} + +/***************************************************************************** + * Convert table names in a string from the PostgreSQL to our SQLite format. + * So, convert "AAAXXX.YYYBBB" to "AAAXXX_YYYBBB" + *****************************************************************************/ +function cleanupTableName(name){ + return name.replace(/(\w+)(\d*)\.(\d*)(\w+)/g,"$1$2_$3$4"); +} + +/***************************************************************************** + * Display an HTML part containing the image produced by Camera or picked from + * the photo library. + *****************************************************************************/ +function loadNewPicture(cid,id,picture){ + $(".tab-pane").each(function(){ + if($(this).is(":visible")) + $(this).find("#value_"+id) + .html('
    '+picture+'
    '); + }); +} + + +/***************************************************************************** + * Create a JSON Object representing the dependency values. + *****************************************************************************/ +function fetchDependencies(obj,cid,changingField){ + var list1=null; + //console.log(cid+" "+JSON.stringify(obj)); + + for(var key in changingField){ + for(var i=0;i'; + tmpStr+=res+ + '
    '; + console.log(tmpStr); + return tmpStr; + } + if(definedSqlTypes[i]["code"]=="geometry"){ + console.log("CID: "+cid+" select type from mm4me_gc where f_table_schema||'_'||f_table_name = (select name from mm4me_tables where id="+cid+") ") + var geoType=getGeometryType("(select replace(name,'.','_') from mm4me_tables where id="+cid+")"); + var viewb=''; + var res=' '; + if(geoType=='POINT' || geoType=='MULTIPOINT' ) + return res+''+ + '

    GPS Informations

    Type
    Coords
    '+ + ''; + else if(geoType=='LINESTRING' || geoType=='MULTILINESTRING' ) + return res+''+ + ''+ + '
    '+viewb+'
    '; + else if(geoType=='POLYGON' || geoType=='MULTIPOLYGON' ) + return res+''+ + ''+ + '
    '+viewb+'
    '; + + } + if(definedSqlTypes[i]["code"]=="tbl_linked"){ + var tmp=obj["value"].split(';'); + //console.log(cleanupTableName('SELECT '+tmp[1]+' FROM '+tmp[2]+' WHERE '+tmp[0]+'='+tblName+'.id')); + var refs=JSON.parse(window.Android.displayTable(cleanupTableName(tmp[3]),[])); + var tmpStr='"; + + } + if(definedSqlTypes[i]["code"]=="link"){ + var strReturn; + if(!View_template) + $.ajax({ + async: false, + method: "GET", + url: './content/view_template.html', + error: function(){ + console.log("Nothing to run after"); + }, + success: function(data){ + console.log("**** \n\n Load View Template"); + View_template=data; + var tmp=obj["value"].split(";"); + var refs=JSON.parse(window.Android.displayTable("SELECT mm4me_tables.id as tid,mm4me_views.id as id,mm4me_tables.name as name,mm4me_tables.description,mm4me_views.name as title from mm4me_tables,mm4me_views where mm4me_tables.name=\""+tmp[1]+"\" and mm4me_tables.id=mm4me_views.ptid",[])); + var reg=new RegExp("\\[id\\]","g"); + if(!valuesOnLoad[cid]) + valuesOnLoad[cid]=[]; + if(refs!=""){ + valuesOnLoad[cid].push(refs); + if(!toRunOnLoad[cid]) + toRunOnLoad[cid]=[]; + toRunOnLoad[cid].push(function(){ + prefix="_"+arguments[0]["0"]["id"]; + //if(!mainTable[arguments[0]["0"]["id"]]) + //mainTable[arguments[0]["0"]["id"]]=arguments[0]["0"]["tid"]; + //console.log(arguments[0]["0"]["tid"]+" "+arguments[0]["0"]["name"]+" "+arguments[0]["0"]["title"]); + listInnerTable(arguments[0]["0"]["tid"],arguments[0]["0"]["id"],arguments[0]["0"]["name"],arguments[0]["0"]["title"],false,prefix,tmp[0]+"="+arguments[1]["local_id"]); + }); + refTables[refs["0"]["tid"]]={"oid":cid,"col":tmp[0],"vid":refs["0"]["id"],"name":refs["0"]["table"],"name":refs["0"]["title"]}; + strReturn=data.replace(reg,refs["0"]["tid"]); + } + } + }); + else{ + var tmp=obj["value"].split(";"); + var refs=JSON.parse(window.Android.displayTable("SELECT mm4me_tables.id as tid,mm4me_views.id as id,mm4me_tables.name as name,mm4me_tables.description,mm4me_views.name as title from mm4me_tables,mm4me_views where mm4me_tables.name=\""+tmp[1]+"\" and mm4me_tables.id=mm4me_views.ptid",[])); + var reg=new RegExp("\\[id\\]","g"); + if(!valuesOnLoad[cid]) + valuesOnLoad[cid]=[]; + valuesOnLoad[cid].push(refs); + if(!toRunOnLoad[cid]) + toRunOnLoad[cid]=[]; + toRunOnLoad[cid].push(function(){ + prefix="_"+arguments[0]["0"]["id"]; + //if(!mainTable[arguments[0]["0"]["id"]]) + //mainTable[arguments[0]["0"]["id"]]=arguments[0]["0"]["tid"]; + //console.log(arguments[0]["0"]["tid"]+" "+arguments[0]["0"]["name"]+" "+arguments[0]["0"]["title"]); + listInnerTable(arguments[0]["0"]["tid"],arguments[0]["0"]["id"],arguments[0]["0"]["name"],arguments[0]["0"]["title"],false,prefix,tmp[0]+"="+arguments[1]["local_id"]); + }); + refTables[refs["0"]["tid"]]={"oid":cid,"col":tmp[0],"vid":refs["0"]["id"],"name":refs["0"]["table"],"name":refs["0"]["title"]}; + strReturn=View_template.replace(reg,refs["0"]["tid"]); + + } + //console.log(strReturn); + return strReturn; + } + if(definedSqlTypes[i]["code"]=="ref"){ + var req=obj["value"];//.replace(/^\((\w+)\)$/g,"$1"); + if(req[0]=="(") + req="SELECT * FROM "+req; + var refs=JSON.parse(window.Android.displayTable(cleanupTableName(req),[])); + var tmpStr='"; + + if(obj["dependencies"]) + try{ + var lobj={}; + lobj[obj["id"]]={"dep":JSON.parse(obj["dependencies"])}; + for(var jj=0;jj=0){ + return ''; + } + if(definedSqlTypes[i]["code"]=="html") + return ''; + if(definedSqlTypes[i]["code"]=="text") + return ''; + if(definedSqlTypes[i]["code"]=="boolean") + return ''; + if(definedSqlTypes[i]["code"]=="date" || definedSqlTypes[i]["code"]=="datetime") + return ''; + if(definedSqlTypes[i]["code"]=="float") + return ''; + return definedSqlTypes[i]["code"]; + } + } + return null; +} + +function printOptionalCheckbox(obj,cid){ + if(definedSqlTypes.length==0){ + definedSqlTypes=JSON.parse(window.Android.displayTable("select id,code from mm4me_ftypes where ftype='e' order by name",[])); + } + + var res=' '; + for(var i in definedSqlTypes){ + if(definedSqlTypes[i]["id"]==obj["ftype"]){ + if(definedSqlTypes[i]["code"]=="bytea"){ + var tmpStr=""; + return res; + } + else if(definedSqlTypes[i]["code"]=="geometry"){ + return res + } + } + } + return ''; +} + +var refTypeId=null; +var editPrintedOnce=[]; +/***************************************************************************** + * Create HTML part to display the line containing both the title and the + * corresponding input for a given table's field. + *****************************************************************************/ +function printEditionFields(obj,myRoot,cid,mid){ + var list1=window.Android.displayTable("select * from mm4me_edition_fields where mm4me_edition_fields.edition>0 and eid="+obj["id"]+" order by mm4me_edition_fields.id asc",[]); + if(!editSchema[mid]) + editSchema[mid]={}; + editSchema[mid][obj["id"]]=JSON.parse(list1); + list1=JSON.parse(list1); + myRoot.find(".tab-content").first().append('
    '+obj["description"]+'
    '); + for(var j in list1) + if(list1[j]["edition"]>0) { + myRoot.find(".tab-content").first().children().last().append( + '
    '+ + '
    '+ + ''+ + '
    '+ + '
    '+ + printCurrentType(list1[j],mid)+ + '
    '+ + '
    '); + if(list1[j]["dependencies"]){ + try{ + editPrintedOnce.push(list1[j]["name"]); + console.log("JSON PARSE") + console.log(list1[j]["dependencies"]); + var objJson=JSON.parse(list1[j]["dependencies"]); + console.log("JSON PARSE OK"); + if(!refTypeId) + refTypeId=JSON.parse(window.Android.displayTable("select id from mm4me_ftypes where ftype='e' and code='ref'",[]))[0]["id"]; + console.log(refTypeId); + for(i in objJson){ + if(objJson[i]["myself"]){ + console.log("IS MYSELF!!"); + for(k in objJson[i]["myself"]){ + for(l in objJson[i]["myself"][k]){ + + + if(objJson[i]["myself"][k][l]["dependents"]){ + for(m in objJson[i]["myself"][k][l]["dependents"]){ + for(n in objJson[i]["myself"][k][l]["dependents"][m]){ + console.log(objJson[i]["myself"][k][l]["dependents"][m][n]["sql_query"]); + console.log(objJson[i]["myself"][k][l]["dependents"][m][n]["label"]); + var lObj={"id": n, "ftype":refTypeId,"value":objJson[i]["myself"][k][l]["dependents"][m][n]["sql_query"]}; + //if(!myRoot.find('select[name="field_'+list1[j]["id"]+'"]').parent().find('select[name="field_'+n+'"]').length) + myRoot.find('select[name="field_'+list1[j]["id"]+'"]').last().parent().prepend( + '
    '+ + '
    '+ + ''+ + '
    '+ + '
    '+ + printCurrentType(lObj,mid)+ + '
    '+ + '
    '); + console.log(myRoot.find('select[name="field_'+n+'"]')); + console.log(printCurrentType(lObj,mid)); + (function(a,b){ + myRoot.find('select[name="field_'+n+'"]').off('change'); + myRoot.find('select[name="field_'+n+'"]').on('change',function(){ + console.log('select[name="field_'+n+'"]'); + var req=cleanupTableName(a["value"]); + if(a["value"].indexOf("WHERE")<0){ + req=req.replace(/order by/g,"where "+b["tfieldf"]+" "+b["operator"]+" "+(b["operator"]=="like"?"'":"")+$(this).val()+(b["operator"]=="like"?"'":"")+" order by") + } + console.log(req); + var res=JSON.parse(window.Android.displayTable(req,[])); + myRoot.find('select[name="field_'+a["id"]+'"]').html(""); + for(ij in res){ + var tmpStr=' '; + cnt+=1; + } + cnt0+=1; + $("select[name=field_"+changingField[j][ckey]["id"]+"]").append(cStr); + } + if(i==0) + $("select[name=field_"+changingField[j][ckey]["id"]+"]").html(''); + $("select[name=field_"+changingField[j][ckey]["id"]+"]").change(); + }else{ + console.log("DISPLAY ELEMENT IF CINDEX >=0 "); + //console.log(JSON.stringify(changingField[j][ckey])); + console.log('input[name="field_'+ckey+'"],select[name="field_'+ckey+'"],textarea[name="field_'+ckey+'"]'); + console.log($('input[name="field_'+changingField[j][ckey]["id"]+'"],select[name="field_'+changingField[j][ckey]["id"]+'"],textarea[name="field_'+changingField[j][ckey]["id"]+'"]').parent().parent().html()); + var mycKey=changingField[j][ckey]["id"]; + if(cIndex<0) + $('input[name="field_'+mycKey+'"],select[name="field_'+mycKey+'"],textarea[name="field_'+mycKey+'"]').parent().parent().hide(); + else + $('input[name="field_'+mycKey+'"],select[name="field_'+mycKey+'"],textarea[name="field_'+mycKey+'"]').parent().parent().show(); + } + } + } + }; + }; + $("select[name=field_"+key+"]").off('change'); + $("select[name=field_"+key+"]").change(localFunc(changingFields[i][key]["dep"])); + $("select[name=field_"+key+"]").change(); + } + } + }catch(e){ + console.log(e); + setTimeout(function() { updateChangingFields(changingFields) }, 500); + } +} + +/***************************************************************************** + * Display the content of a table referencing the current edited table. + *****************************************************************************/ +function listInnerTable(id,vid,name,title,init,prefix,clause,ref){ + var list=JSON.parse(window.Android.displayTable("select mm4me_tables.id as tid,mm4me_tables.name as tname,mm4me_editions.id,mm4me_editions.name from mm4me_editions,mm4me_tables where mm4me_editions.ptid=mm4me_tables.id and mm4me_tables.id="+id+" order by mm4me_editions.step asc",[])); + var cnt=0; + var detectInit=true; + var tid=0; + for(var i in list){ + lastEdition[list[i]["id"]]=list[i]; + tid=list[i]["tid"]; + if(!allTables[list[i]["tid"]]){ + allTables[list[i]["tid"]]={"id":list[i]["id"],"name":list[i]["tname"],"title":list[i]["name"]}; + detectInit=false; + $("#sub_tableContent_"+id+"").find(".mm4me_edition").find("ul").first().append(''); + printEditionFields(list[i],$("#sub_tableContent_"+id+"").find(".mm4me_edition"),list[i]["id"],list[i]["tid"]); + if(cnt==0){ + try{ + var cid=list[i]["id"]+"_0"; + $("#sub_tableContent_"+id+"").find("ul").first().append('
  • '+window.Android.translate('table')+'
  • '); + $("#sub_tableContent_"+id+"").find("ul").first().append(''); + $("#sub_tableContent_"+id+"").find("ul").first().append(''); + $("#sub_tableContent_"+id+" .require-select").hide(); + printEditionFields(list[i],$("#sub_tableContent_"+id+""),cid,list[i]["tid"]); + }catch(e){ + console.log("**** ERROR ***> "+e); + } + } + var myRoot=$("#sub_tableContent_"+id+""); + myRoot.find('ul').first().find('a').click(function (e) { + e.preventDefault(); + if($(this).parent().hasClass('require-select')) + myRoot.find('.mm4me_edition').show(); + else + myRoot.find('.mm4me_edition').hide(); + $(this).tab('show'); + }) + myRoot.find('ul').first().find('a').first().click(); + myRoot.find('.mm4me_edition').find('ul').find('a').first().click(); + myRoot.find('.swagEditor').summernote(); + myRoot.find(".mm-act-add").click(function(){ + runInsertQuery($(this).parent().parent(),tid,editTableReact); + }); + myRoot.find(".mm-act-save").click(function(){ + runUpdateQuery($(this).parent().parent(),tid,editTableReact); + }); + + } + cnt+=1; + } + + var list=JSON.parse(window.Android.displayTable("SELECT id,name,value,alias,width,class from mm4me_view_fields where vid="+vid+" order by id",[])); + var columns=[]; + var columnNames=[]; + var sqlColumns=""; + var orderColumn="id"; + var orderType="asc"; + for(var i=0;i0){ + orderColumn=list[i]["name"]; + if(list[i]["class"]==1) + orderType="desc"; + } + } + var ccol=getPKey(cleanupTableName(name)); + var req="SELECT "+ccol+" as ogc_fid FROM "+cleanupTableName(name)+" WHERE "+clause+" ORDER BY "+cleanupTableName(name)+"."+orderColumn+" "+orderType; + var list1=JSON.parse(window.Android.displayTable(req,[])); + req="SELECT "+sqlColumns+" FROM "+cleanupTableName(name)+" WHERE "+clause+" ORDER BY "+cleanupTableName(name)+"."+orderColumn+" "+orderType; + var list=JSON.parse(window.Android.displayTable(req,[])); + var dataSet = []; + for(var i=0;i'; + cnt+=1; + } + dataSet.push(tmpData); + } + var selected = []; + + //$("#edition_form_table").html('
    '); + var localName="exampleTable_"+id; + ((function(localName,sid){ + + if(!detectInit){ + + var options={ + data: dataSet, + columns: columns, + "scrollX": true, + scrollY: '50vh', + scrollCollapse: true, + select: true, + "rowCallback": function( row, data ) { + if ( $.inArray(data.DT_RowId, selected) !== -1 ) { + $(row).addClass('selected'); + } + } + }; + if(langUrl!=null) + options["language"]={ + url: langUrl + }; + + $('#'+localName).DataTable( options ); + $('#'+localName+' tbody').on('click', 'tr', function () { + var id = this.id; + var index = $.inArray(id, selected); + + if ( index === -1 ) { + selected.push( id ); + } else { + selected.splice( index, 1 ); + } + displayEditForm(sid,$(this).find("input[name=id]").first().val(),true); + var tmp=$(this).hasClass('selected'); + $('#'+localName+' tbody tr').removeClass('selected'); + $(this).toggleClass('selected'); + if(!tmp) + $(this).toggleClass('selected'); + } ); + + }else{ + $('#'+localName).dataTable().fnClearTable(); + if(dataSet.length>0) + $('#'+localName).dataTable().fnAddData(dataSet); + } + $("#sub_tableContent_"+id+"").find("ul").first().find('a').first().click(); + })(localName,tid)); + +} + +var onFormFirstLoad=null; +/***************************************************************************** + * Show the edit form + *****************************************************************************/ +function displayEditForm(cid,selectedId,basic){ + if(basic && !$("#exampleTable"+(cid==mtable?"":"_"+cid)).find(".selected").find('input[type=hidden]').first().val()){ + if(cid==mtable){ + $("#main_tableContent").find(".require-select").hide(); + $("#main_tableContent").find("ul").first().find("a").first().click(); + if($(".breadcrumb").children().length==4) + $(".breadcrumb").children().last().remove(); + + }else{ + $("#sub_tableContent_"+cid).find(".require-select").hide(); + $("#sub_tableContent_"+cid).find("ul").first().find("a").first().click(); + } + return; + } + var fields=[] + var sizedFields=[]; + var sizedFieldsAlias=[]; + var notSizedFields=[]; + for(var i in editSchema[cid]){ + for(var j in editSchema[cid][i]){ + //console.log(JSON.stringify(editSchema[cid][i][j])); + if(editSchema[cid][i][j]["ftype"]=="5"){ + sizedFields.push(editSchema[cid][i][j]["name"].replace(/wkb_geometry/g,"geometry")); + sizedFieldsAlias.push(editSchema[cid][i][j]["id"]); + } + else{ + notSizedFields.push(editSchema[cid][i][j]["name"].replace(/wkb_geometry/g,"geometry")+" AS \""+editSchema[cid][i][j]["id"]+"\""); + try{ + var tmp=JSON.parse(editSchema[cid][i][j]["dependencies"]); + var sqlReq=""; + var sqlClause=""; + var sqlParams=""; + var sqlParam=0; + var alphabet = 'abcdefghijklmnopqrstuvwxyz'.split(''); + var hasDep=false; + var previousElements=[]; + for(k in tmp) + for(l in tmp[k]){ + //console.log(JSON.stringify(tmp[k][l])); + if(l=="myself"){ + for(m in tmp[k][l]) + for(n in tmp[k][l][m]){ + //console.log(JSON.stringify(tmp[k][l][m][n])); + if(sqlReq!=""){ + sqlReq+=", "; + sqlParams+=", "; + //sqlClause+=" WHERE "+alphabet[sqlParam-1]+"."+tmp[k][l][m][n]["tfield"]+"="+alphabet[sqlParam]+"."+tmp[k][l][m][n]["tfield"]; + + } + sqlReq+="("+cleanupTableName(tmp[k][l][m][n]["sql_query"])+") as "+alphabet[sqlParam]; + sqlParams+=alphabet[sqlParam]+"."+tmp[k][l][m][n]["tfield"]; + sqlParam+=1; + if(tmp[k][l][m][n]["dependents"]){ + //console.log(JSON.stringify(tmp[k][l][m][n]["dependents"])); + for(var o in tmp[k][l][m][n]["dependents"]){ + //console.log(JSON.stringify(tmp[k][l][m][n]["dependents"][o])); + for(var p in tmp[k][l][m][n]["dependents"][o]){ + console.log(tmp[k][l][m][n]["dependents"][o][p]["sql_query"].replace(/from/,","+tmp[k][l][m][n]["dependents"][o][p]["tfield"]+" from")); + sqlReq+=", ("+cleanupTableName(tmp[k][l][m][n]["dependents"][o][p]["sql_query"]).replace(/from/,","+tmp[k][l][m][n]["dependents"][o][p]["tfield"]+" from")+") as b"; + sqlParams+=", b."+tmp[k][l][m][n]["dependents"][o][p]["tfieldf"]; + sqlParam+=2; + sqlReq+=", ("+cleanupTableName(editSchema[cid][i][j]["value"]).replace(/from/,","+tmp[k][l][m][n]["dependents"][o][p]["tfieldf"]+" from")+") as c"; + + sqlClause+=" WHERE c."+tmp[k][l][m][n]["dependents"][o][p]["tfieldf"]+"=b."+tmp[k][l][m][n]["dependents"][o][p]["tfieldf"]; + sqlClause+=" AND b."+tmp[k][l][m][n]["tfield"]+"=a."+tmp[k][l][m][n]["tfield"]; + sqlClause+=" AND c.id=(SELECT "+editSchema[cid][i][j]["name"]+" FROM "+cleanupTableName(allTables[cid].name)+" where id="+selectedId+")"; + hasDep=true; + } + } + } + } + if(!hasDep){ + var tmpReq=cleanupTableName(editSchema[cid][i][j]["value"]); + for(m in tmp[k][l]) + for(n in tmp[k][l][m]){ + tmpReq=tmpReq.replace(/from/,","+tmp[k][l][m][n]["tfield"]+" from"); + if(sqlClause=="") + sqlClause+=" WHERE "; + else + sqlClause+=" "+tmp[k][l][m][n]["cond_join"]+" "; + sqlClause+=alphabet[m]+"."+tmp[k][l][m][n]["tfield"]+"=a1."+tmp[k][l][m][n]["tfield"]; + sqlClause+=""; + } + sqlClause+=" AND a1.id=(SELECT "+editSchema[cid][i][j]["name"]+" FROM "+cleanupTableName(allTables[cid].name)+" where id="+selectedId+")"; + //sqlReq=(tmpReq); + console.log(tmpReq); + sqlReq+=", ("+tmpReq+") as a1"; + } + //console.log(JSON.stringify(tmp[k][l])); + console.log("SELECT "+sqlParams+" FROM "+sqlReq+" "+sqlClause); + var localQuery="SELECT "+sqlParams+" FROM "+sqlReq+" "+sqlClause; + var res0=JSON.parse(window.Android.displayTable(localQuery,[])); + console.log(JSON.stringify(res0)); + for(m in res0) + for(n in res0[m]){ + try{ + console.log("input[name=field_"+n+"],select[name=field_"+n+"],textarea[name=field_"+n+"]"); + /**/ + if($('.mm4me_edition').find("input[name=field_"+n+"],select[name=field_"+n+"],textarea[name=field_"+n+"]").first().length) + $('.mm4me_edition').find("input[name=field_"+n+"],select[name=field_"+n+"],textarea[name=field_"+n+"]").first().val(res0[m][n]).change(); + else{ + for(m0 in tmp[k][l]) + for(n0 in tmp[k][l][m0]){ + /*console.log(n0.indexOf(n)<0); + console.log(n); + console.log(n0); + console.log(res0[m][n]);*/ + if(n0.indexOf(n)>=0){ + console.log("input[name=field_"+n0+"],select[name=field_"+n0+"],textarea[name=field_"+n0+"]"); + if(!$('.mm4me_edition').find("input[name=field_"+n+"],select[name=field_"+n+"],textarea[name=field_"+n+"]").first().length) + $('.mm4me_edition').find("input[name=field_"+n0+"],select[name=field_"+n0+"],textarea[name=field_"+n0+"]").first().val(res0[m][n]).change(); + } + } + } + }catch(e){ + console.log(e); + } + + } + //if(tmp[k][l]["dependents"]) + }else{ + console.log(JSON.stringify(tmp[k][l])); + if(tmp[k][l]["tfield"]=="none"){ + var isNull=JSON.parse(window.Android.displayTable("SELECT CASE WHEN "+l+" is null THEN 1 ELSE 0 END as p FROM "+cleanupTableName(allTables[cid].name)+" where id="+selectedId,[])); + console.log(JSON.stringify(isNull)); + if(isNull[0]["p"]=="0"){ + console.log(JSON.stringify(isNull)); + console.log("input[name=field_"+editSchema[cid][i][j]["id"]+"],select[name=field_"+editSchema[cid][i][j]["id"]+"],textarea[name=field_"+editSchema[cid][i][j]["id"]+"]"); + console.log(k+""); + $('.mm4me_edition').find("input[name=field_"+editSchema[cid][i][j]["id"]+"],select[name=field_"+editSchema[cid][i][j]["id"]+"],textarea[name=field_"+editSchema[cid][i][j]["id"]+"]").first().val(k+"").change(); + } + } + + } + } + + //if(sqlReq) + }catch(e){ + console.log(e); + } + } + if(editSchema[cid][i][j]["name"].indexOf("unamed")<0) + fields.push(editSchema[cid][i][j]["name"].replace(/wkb_geometry/g,"geometry")+" AS \""+editSchema[cid][i][j]["id"]+"\""); + } + } + var ccol=getPKey(cleanupTableName(allTables[cid].name)); + /* $("#exampleTable"+(cid==mtable?"":"_"+cid)).find(".selected").find('input[type=hidden]').first().val() */ + var editValues; + if(sizedFields.length>0){ + for(var i=0;i 1000000 and "+ccol+"="+selectedId,[])); + //console.log(JSON.stringify(hasElement[0])); + if(hasElement[0]["cnt"]!="0"){ + var nbIteration=parseInt(hasElement[0]["len"])/1000000; + var zfields=[] + var len=0; + var query=""; + var len1=parseInt(hasElement[0]["len"]); + for(var j=0;j0){ + $("#field_"+j+"_map").show(); + $("#field_"+j+"_map").off('click'); + $("#field_"+j+"_map").on('click',function(){ + console.log("Display table with a selected feature"); + console.log($(this).prev().val()); + showElementOnMap($(this).prev().val()); + }); + } + if($(".btn_field_"+j+"_lat").length>0){ + //alert(editValues[i][j]); + $(".btn_field_"+j+"_lat").html(editValues[i][j]); + $(".btn_field_"+j+"_long").html(""); + $(".btn_field_"+j+"_source").html(""); + $("input[name='field_"+j+"']").val("POINT"+editValues[i][j]); + + } + } + //$(".swagEditor").summernote(); + } + } + + for(var i in editSchema[cid]){ + for(var j in editSchema[cid][i]){ + if(editSchema[cid][i][j]["name"].indexOf("unamed")>=0){ + if(editSchema[cid][i][j]["ftype"]==6){ + var tmp=editSchema[cid][i][j]["value"].split(';'); + var list=JSON.parse(window.Android.displayTable("select "+tmp[1]+" from "+cleanupTableName(tmp[2])+" where "+tmp[0]+"=(SELECT id from "+cleanupTableName(tblName)+" WHERE ogc_fid="+selectedId+")",[])); + editValues["0"][editSchema[cid][i][j]["id"]]=list; + for(var k in list){ + for(var l in list[k]){ + $('.mm4me_edition').find("select[name=field_"+editSchema[cid][i][j]["id"]+"]").find('option').each(function(){ + if($(this).val()==list[k][l]) + $(this).prop("selected",true); + }); + } + } + } + } + } + } + + if(cid==mtable){ + if($(".breadcrumb").children().length==4) + $(".breadcrumb").children().last().remove(); + $(".breadcrumb").append('
  • '+editValues["0"]["local_id"]+'
  • '); + } + ((cid==mtable)?$(".require-select"):$("#sub_tableContent_"+cid).find(".require-select")).first().show(); + ((cid==mtable)?$(".require-select"):$("#sub_tableContent_"+cid).find(".require-select")).first().find("a").first().click(); + + if(toRunOnLoad[cid]) + for(var i=0;i=0 order by mm4me_editions.step asc",[]); + if(MM4ME_DEBUG) + console.log(list); + list=JSON.parse(list); + if(MM4ME_DEBUG) + console.log((!allTables[tblId])); + if(!allTables[tblId]){ + allTables[tblId]={"id":id,"name":name,"title":title}; + } + mtable=tblId; + + $(".mm4me_edition").find("ul").first().html(""); + $(".mm4me_edition").find(".well").first().html(""); + var cnt=0; + for(var i in list){ + lastEdition[list[i]["id"]]=list[i]; + var cid=(i==0?list[i]["id"]+"_0":list[i]["id"]); + $(".mm4me_edition").find("ul").first().append(''); + printEditionFields(list[i],$("#edition_form_edit"),cid,mainTable[id]); + } + if(list.length==1) + $(".mm4me_edition").find("ul").first().hide(); + + var aCnt=0; + $('.mm4me_edition').find('ul').first().find('a').each(function () { + if(aCnt>0) + $(this).parent().addClass('require-select'); + aCnt+=1; + }); + $(".require-select").hide(); + $('.mm4me_listing').find('ul').first().find('a').first().click(); + $('.mm4me_edition').find('ul').find('a').first().click(); + $('.swagEditor').summernote(); + $(".mm-act-add").click(function(){ + runInsertQuery($(this).parent().parent(),mainTable[id],editOnlyTableReact); + }); + $(".mm-act-save").click(function(){ + runUpdateQuery($(this).parent().parent(),mainTable[id],editOnlyTableReact); + }); + $(".breadcrumb").children().last().remove(); + $(".breadcrumb").append('
  • '+window.Android.translate('edit')+'
  • '); + $(".breadcrumb").append('
  • '+tblTitle+'
  • '); + + setTimeout(function() { updateChangingFields(changingFields) }, 1500); + + /*for(var i=0;i'; + else + cStr+=changingField[j][ckey]["values"][cIndex][i][lkey]+''; + cnt+=1; + } + $("select[name=field_"+changingField[j][ckey]["id"]+"]").append(cStr); + } + if(i==0) + $("select[name=field_"+changingField[j][ckey]["id"]+"]").html(''); + } + } + }; + }; + $("select[name=field_"+key+"]").off('change'); + $("select[name=field_"+key+"]").change(localFunc(changingFields[i][key]["dep"])); + $("select[name=field_"+key+"]").change(); + } + }*/ + $('.mm4me_listing').show(); + $('.mm4me_content').hide(); + +} + +/***************************************************************************** + * In case no server is configured + *****************************************************************************/ +function displayNoListing(){ + $.ajax({ + method: "GET", + url: 'content/nolisting.html', + success: function(data){ + if(MM4ME_DEBUG) + console.log('Display warning message on the UI !'); + $(".mm4me_content").html(data); + $(".mm4me_content").find(".pannel-body").find("p").first().html(window.Android.translate("mm_no_db_found")); + }, + error: function(){ + window.Android.showToast("error !"); + } + }); +} + + +/***************************************************************************** + * Authenticate a user + *****************************************************************************/ +function authenticate(url,login,passwd,func,func1){ + var curl=url+"?service=WPS&request=Execute&version=1.0.0&Identifier=authenticate.clogIn&DataInputs=login="+login+";password="+passwd+"&RawDataOutput=Result"; + if(MM4ME_DEBUG) + console.log(curl); + $.ajax({ + method: "GET", + url: curl, + success: function(data){ + if(MM4ME_DEBUG) + console.log(data); + if(func){ + console.log("Call func!") + func(); + } + }, + error: function(){ + if(func1){ + func1(); + } + else{ + disconnect(url); + if(MM4ME_DEBUG) + console.log("unable to login!"); + var hasBeenShown=false; + var xml=arguments[0].responseText; + $(xml).find("ows\\:ExceptionText").each(function(){ + window.Android.showToast($(this).text()); + hasBeenShown=true; + }); + if(!hasBeenShown){ + window.Android.showToast(JSON.stringify(arguments)); + } + } + + } + }); +} + +/***************************************************************************** + * Disconnect a user + *****************************************************************************/ +function disconnect(url,func,func1){ + var curl=url+"?service=WPS&request=Execute&version=1.0.0&Identifier=authenticate.clogOut&DataInputs=&RawDataOutput=Result"; + if(MM4ME_DEBUG) + console.log(curl); + $.ajax({ + method: "GET", + url: curl, + success: function(data){ + if(MM4ME_DEBUG){ + console.log(data); + console.log("** Your are no more connected!"); + } + if(func){ + func(); + } + }, + error: function(){ + if(MM4ME_DEBUG){ + console.log(curl); + console.log("unable to disconnect!"); + } + if(func1){ + func1(); + } + } + }); +} + +var geometries={"line":{"geom":null,"constructor": "ol.geom.Linestring"},"polygon":{"geom":null,"constructor":"ol.geom.Polygon","cline":null}}; +var stopTracking=false; +var currentGeometry="line"; +var currentGeometryField="none"; +var map=null; +var vectorLayer,vectorLayer1; +var position; + +/***************************************************************************** + * Track modification of the GPS location + *****************************************************************************/ +function trackCurrentGPSPosition(){ + console.log("## trackCurrentGPSPosition"); + updateCurrentMapLocation(); + + var tmp0=geometries["origin"].getCoordinates(); + if(geometries[currentGeometry]["geom"]!=null){ + var tmp1=geometries[currentGeometry]["geom"].getCoordinates(); + tmp0=tmp1[tmp1.length-1]; + } + tmp=ol.proj.transform([position[bestIndex].lon,position[bestIndex].lat], 'EPSG:4326','EPSG:3857'); + console.log(JSON.stringify(tmp0)); + console.log(JSON.stringify(tmp)); + $("#currentPosition").html("Position: "+position[bestIndex].lon.toFixed(6)+","+position[bestIndex].lat.toFixed(6)+""); + if(tmp0[0]==tmp[0] && tmp0[1]==tmp[1]){ + console.log(" !!!!!!!! Same position!!!!!!!! "); + if(!stopTracking) + setTimeout(function() { trackCurrentGPSPosition();}, 1000); + return; + } + + console.log(JSON.stringify(tmp0)); + console.log(JSON.stringify(tmp)); + if(geometries[currentGeometry]["geom"]==null){ + //geometries[currentGeometry]=new geometries[currentGeometry]["constructor"]([tmp0,tmp]); + if(currentGeometry=="line") + geometries[currentGeometry]["geom"]=new ol.geom.LineString([tmp0,tmp]); + else + geometries[currentGeometry]["geom"]=new ol.geom.LineString([tmp0,tmp]); + }else{ + if(currentGeometry=="line"){ + tmpCoordinates=geometries[currentGeometry]["geom"].getCoordinates(); + tmpCoordinates.push(tmp); + geometries[currentGeometry]["geom"]=new ol.geom.LineString(tmpCoordinates); + } + else{ + if(geometries[currentGeometry]["cline"]==null) + tmpCoordinates=geometries[currentGeometry]["geom"].getCoordinates(); + else + tmpCoordinates=geometries[currentGeometry]["cline"].getCoordinates(); + console.log(JSON.stringify(tmpCoordinates)); + tmpCoordinates.push(tmp); + console.log(JSON.stringify(tmpCoordinates)); + if(tmpCoordinates.length>2){ + geometries[currentGeometry]["cline"]=new ol.geom.LineString(tmpCoordinates); + tmpCoordinates.push(tmpCoordinates[0]); + console.log(JSON.stringify(tmpCoordinates)); + geometries[currentGeometry]["geom"]=new ol.geom.Polygon(); + console.log(JSON.stringify(tmpCoordinates)); + geometries[currentGeometry]["geom"].appendLinearRing(new ol.geom.LinearRing(tmpCoordinates)); + console.log(JSON.stringify(tmpCoordinates)); + } + } + + } + console.log(JSON.stringify(tmp0)); + console.log(JSON.stringify(tmp)); + try{ + console.log(JSON.stringify(geometries[currentGeometry]["geom"].getCoordinates())); + var feature=new ol.Feature({geometry: geometries[currentGeometry]["geom"],"name":"myFeature"}); + vectorLayer1.getSource().clear(); + vectorLayer1.getSource().addFeatures([feature]); + var wkt=new ol.format.WKT(); + var tmpGeometry=geometries[currentGeometry]["geom"].clone().transform('EPSG:3857', 'EPSG:4326'); + console.log(wkt.writeGeometry(tmpGeometry)); + $("input[name='"+currentGeometryField+"']").val(wkt.writeGeometry(tmpGeometry)); + }catch(e){ + console.log(e); + } + console.log(JSON.stringify(tmp0)); + console.log(JSON.stringify(tmp)); + if(!stopTracking) + setTimeout(function() { trackCurrentGPSPosition();}, 1000); + console.log(JSON.stringify(tmp0)); + console.log(JSON.stringify(tmp)); +} + +var modalCallback=null; + +function trackStepCurrentGPSPosition(){ + console.log("## trackStepCurrentGPSPosition"); + updateCurrentMapLocation(); + $("#currentPosition").html("Position: "+position[bestIndex].lon.toFixed(6)+","+position[bestIndex].lat.toFixed(6)+""); + if(!stopTracking) + setTimeout(function() { trackStepCurrentGPSPosition();}, 1000); +} + +function addCurrentLocation(){ + console.log("addCurrentLocation!!!!"); + if(geometries["origin"]==null) + geometries["origin"]=new ol.geom.Point(ol.proj.transform([position[bestIndex].lon,position[bestIndex].lat], 'EPSG:4326','EPSG:3857')); + else{ + var tmp0=geometries["origin"].getCoordinates(); + if(geometries[currentGeometry]["geom"]!=null){ + var tmp1=geometries[currentGeometry]["geom"].getCoordinates(); + tmp0=tmp1[tmp1.length-1]; + } + tmp=ol.proj.transform([position[bestIndex].lon,position[bestIndex].lat], 'EPSG:4326','EPSG:3857'); + console.log(JSON.stringify(tmp0)); + console.log(JSON.stringify(tmp)); + $("#currentPosition").html("Position: "+position[bestIndex].lon.toFixed(6)+","+position[bestIndex].lat.toFixed(6)+""); + if(geometries[currentGeometry]["geom"]==null){ + //geometries[currentGeometry]=new geometries[currentGeometry]["constructor"]([tmp0,tmp]); + if(currentGeometry=="line") + geometries[currentGeometry]["geom"]=new ol.geom.LineString([tmp0,tmp]); + else + geometries[currentGeometry]["geom"]=new ol.geom.LineString([tmp0,tmp]); + }else{ + if(currentGeometry=="line"){ + tmpCoordinates=geometries[currentGeometry]["geom"].getCoordinates(); + tmpCoordinates.push(tmp); + geometries[currentGeometry]["geom"]=new ol.geom.LineString(tmpCoordinates); + } + else{ + if(geometries[currentGeometry]["cline"]==null) + tmpCoordinates=geometries[currentGeometry]["geom"].getCoordinates(); + else + tmpCoordinates=geometries[currentGeometry]["cline"].getCoordinates(); + console.log(JSON.stringify(tmpCoordinates)); + tmpCoordinates.push(tmp); + console.log(JSON.stringify(tmpCoordinates)); + if(tmpCoordinates.length>2){ + geometries[currentGeometry]["cline"]=new ol.geom.LineString(tmpCoordinates); + tmpCoordinates.push(tmpCoordinates[0]); + console.log(JSON.stringify(tmpCoordinates)); + geometries[currentGeometry]["geom"]=new ol.geom.Polygon(); + console.log(JSON.stringify(tmpCoordinates)); + geometries[currentGeometry]["geom"].appendLinearRing(new ol.geom.LinearRing(tmpCoordinates)); + console.log(JSON.stringify(tmpCoordinates)); + } + } + + } + try{ + console.log(JSON.stringify(geometries[currentGeometry]["geom"].getCoordinates())); + var feature=new ol.Feature({geometry: geometries[currentGeometry]["geom"],"name":"myFeature"}); + vectorLayer1.getSource().clear(); + vectorLayer1.getSource().addFeatures([feature]); + var wkt=new ol.format.WKT(); + var tmpGeometry=geometries[currentGeometry]["geom"].clone().transform('EPSG:3857', 'EPSG:4326'); + console.log(wkt.writeGeometry(tmpGeometry)); + $("input[name='"+currentGeometryField+"']").val(wkt.writeGeometry(tmpGeometry)); + }catch(e){ + console.log(e); + } + + } +} + +function trackStepByStepPosition(elem,ltype){ + modalCallback=function(){ + //$("#myModal").find(".glyphicon-ok").parent().show(); + if(myVectorLayer) + myVectorLayer.getSource().clear(); + geometries["origin"]=null; + setTimeout(function() { trackStepCurrentGPSPosition();}, 1000); + $("#myModal").find("h4").html(' '+window.Android.translate("gps_track")); + $("#myModal").find(".modal-footer").html( + ''+ + ''+ + '' + ); + }; + currentGeometry=ltype; + currentGeometryField=elem; + geometries[ltype]["geom"]=null; + geometries[ltype]["cline"]=null; + if(!$("#map").length) + $.ajax({ + method: "GET", + url: './map-modal.html', + error: function(){ + }, + success: function(data){ + console.log(data); + $("body").append(data); + $("#map").css("height",($(window).height()-220)+"px"); + $('#myModal').modal(); + addOptionalLocalTiles(true); + $('#myModal').on('shown.bs.modal', function () { + console.log($("#mmm4me_ls").length); + initMapToLocation(); + localTileIndex=map.getLayers().getLength(); + /*geometries["origin"]=new ol.geom.Point(ol.proj.transform([position[bestIndex].lon,position[bestIndex].lat], 'EPSG:4326','EPSG:3857')); + setTimeout(function() { trackCurrentGPSPosition();}, 1000);*/ + modalCallback(); + }); + $('#myModal').on('hide.bs.modal', function () { + console.log("HIDE"); + stopTracking=true; + }); + } + }); + else{ + stopTracking=false; + $('#myModal').modal(); + vectorLayer1.getSource().clear(); + updateCurrentMapLocation(); + geometries["origin"]=null; + setTimeout(function() { trackStepCurrentGPSPosition();}, 1000); + } +} +/***************************************************************************** + * Start tracking GPS location + *****************************************************************************/ +function trackGPSPosition(elem,ltype){ + modalCallback=function(){ + //$("#myModal").find(".glyphicon-ok").parent().show(); + if(myVectorLayer) + myVectorLayer.getSource().clear(); + geometries["origin"]=new ol.geom.Point(ol.proj.transform([position[bestIndex].lon,position[bestIndex].lat], 'EPSG:4326','EPSG:3857')); + setTimeout(function() { trackCurrentGPSPosition();}, 1000); + $("#myModal").find("h4").html(' '+window.Android.translate("gps_track")); + $("#myModal").find(".modal-footer").html( + ''+ + '' + ); + }; + currentGeometry=ltype; + currentGeometryField=elem; + geometries[ltype]["geom"]=null; + geometries[ltype]["cline"]=null; + if(!$("#map").length) + $.ajax({ + method: "GET", + url: './map-modal.html', + error: function(){ + }, + success: function(data){ + console.log(data); + $("body").append(data); + $("#map").css("height",($(window).height()-220)+"px"); + $('#myModal').modal(); + addOptionalLocalTiles(true); + $('#myModal').on('shown.bs.modal', function () { + console.log($("#mmm4me_ls").length); + initMapToLocation(); + localTileIndex=map.getLayers().getLength(); + /*geometries["origin"]=new ol.geom.Point(ol.proj.transform([position[bestIndex].lon,position[bestIndex].lat], 'EPSG:4326','EPSG:3857')); + setTimeout(function() { trackCurrentGPSPosition();}, 1000);*/ + modalCallback(); + }); + $('#myModal').on('hide.bs.modal', function () { + console.log("HIDE"); + stopTracking=true; + }); + } + }); + else{ + stopTracking=false; + $('#myModal').modal(); + vectorLayer1.getSource().clear(); + updateCurrentMapLocation(); + geometries["origin"]=new ol.geom.Point(ol.proj.transform([position[bestIndex].lon,position[bestIndex].lat], 'EPSG:4326','EPSG:3857')); + setTimeout(function() { trackCurrentGPSPosition();}, 1000); + } +} + +var hasAddedElement=0; +var myVectorLayer=null; +function addSelectedElement(wktString){ + var features=[]; + try{ + var format = new ol.format.WKT(); + features.push( + format.readFeature(wktString, { + dataProjection: 'EPSG:4326', + featureProjection: 'EPSG:3857' + }) + ); + }catch(e){ + console.log("Unable to parse WKT ?!"+e) + } + if(!vectorLayer1){ + myVectorLayer = new ol.layer.Vector({ + source: new ol.source.Vector({ + features: features + }), + style: new ol.style.Style({ + stroke: new ol.style.Stroke({ + color: "#3333aa", + width: 1.4 + }) + }) + }); + map.addLayer(myVectorLayer); + } + else{ + vectorLayer1.getSource().clear(); + vectorLayer1.getSource().addFeatures(features); + } + console.log(vectorLayer1.getSource().getExtent()); + map.updateSize(); + map.getView().fit(vectorLayer1.getSource().getExtent(),map.getSize()); + hasAddedElement=1; +} + +/***************************************************************************** + * Show selected feature on map + *****************************************************************************/ + var addSelectedIndex=0; + var mySelectedElement=null; +function showElementOnMap(wktString){ + mySelectedElement=wktString; + modalCallback=function(){ + //$("#myModal").find(".glyphicon-ok").parent().hide(); + if(addSelectedIndex==0){ + updateCurrentMapLocation(); + $("#currentPosition").html("Position: "+position[bestIndex].lon.toFixed(6)+","+position[bestIndex].lat.toFixed(6)+""); + } + addSelectedElement(mySelectedElement); + $("#myModal").find("h4").html(' '+window.Android.translate("view_feature")); + addSelectedIndex++; + }; + //$("#currentPosition").html("Position: "+position[bestIndex].lon.toFixed(6)+","+position[bestIndex].lat.toFixed(6)+""); + if(!$("#map").length) + $.ajax({ + method: "GET", + url: './map-modal.html', + error: function(){ + }, + success: function(data){ + console.log(data); + $("body").append(data); + $("#map").css("height",($(window).height()-220)+"px"); + $('#myModal').modal(); + addOptionalLocalTiles(true); + $('#myModal').on('shown.bs.modal', function () { + console.log($("#mmm4me_ls").length); + initMapToLocation(); + localTileIndex=map.getLayers().getLength(); + modalCallback(); + }); + $('#myModal').on('hide.bs.modal', function () { + console.log("HIDE"); + stopTracking=true; + }); + } + }); + else{ + console.log("OK "); + $('#myModal').modal(); + console.log("OK "); + vectorLayer1.getSource().clear(); + console.log("OK "); + updateCurrentMapLocation(); + console.log("OK "); + addSelectedElement(wktString); + console.log("OK "); + } + + $("#currentPosition").html("Position: "+position[bestIndex].lon.toFixed(6)+","+position[bestIndex].lat.toFixed(6)+""); +} + + +/***************************************************************************** + * Store the current GPS location in the edit form + *****************************************************************************/ +function requireGPSPosition(elem){ + var position=JSON.parse(window.Android.getGPS()); + //elem=$("#"+elem); + if(position.lat){ + $(".btn_"+elem+"_lat").html(position.lat); + $(".btn_"+elem+"_long").html(position.lon); + $(".btn_"+elem+"_source").html(position.source); + $("input[name='"+elem+"']").val("POINT("+position.lon+" "+position.lat+")"); + }else{ + + } + console.log(JSON.stringify(position)); +} + +/***************************************************************************** + * Ajax setup to ensure seting "withCredentials" to true + *****************************************************************************/ +function ajaxSetup(){ + $.ajaxSetup({ + xhrFields: { + withCredentials: true + } + }); +} + +/***************************************************************************** + * Get the current Network and GPS availability + *****************************************************************************/ +function getCurrentStatus(){ + var tmp=JSON.parse(window.Android.getGNStatus()); + tmp["gps"]=JSON.parse(tmp["gps"]); + updateStatus(tmp['gps'],tmp['net']); +} + +/***************************************************************************** + * Display the status icons + *****************************************************************************/ +function addStatusControl(){ + $('.breadcrumb').append(' /'+ + ''+ + ''+ + ''+ + ''+ + ''); +} + +/***************************************************************************** + * Initialize the map and show the current GPS location + *****************************************************************************/ +var localTiles,localTiles0; +var tileLayers=[]; +function initMapToLocation(){ + if(map) + return; + var osmSource = new ol.source.OSM(); + + var otherLayers=[]; + var otherLayersSwitcher=""; + try{ + var BasesLayersStr=window.Android.getBaseLayers(); + console.log(BasesLayersStr); + var BasesLayers=JSON.parse(BasesLayersStr); + for(var i=0;i=4) + map.getLayers().item(otherLayers[i]["index"]).setVisible(false); + }else{ + $(this).parent().find("i").removeClass("glyphicon-eye-close").addClass("glyphicon-eye-open"); + $("#dmopacity_"+i).show(); + console.log(JSON.stringify(map.getLayers().getLength())); + if(!otherLayers[i]["index"]){ + var myTileLayer=new ol.layer.Tile({source: otherLayers[i]["olLayer"]}); + var layers = map.getLayers(); + layers.insertAt(tileLayers.length+1,myTileLayer); + otherLayers[i]["index"]=tileLayers.length+1; + tileLayers.push(myTileLayer); + if(localTileIndex>0) + localTileIndex+=1; + } + else + map.getLayers().item(otherLayers[i]["index"]).setVisible(true); + } + }); + $(".map").parent().find("input[type=range]").last().on('change',function(){ + console.log("change to "+$(this).val()); + map.getLayers().item(otherLayers[i]["index"]).setOpacity($(this).val()/100); + }); + + })(i); + $(".map").parent().find("input[type=checkbox]").parent().off('click'); + $(".map").parent().find("input[type=checkbox]").parent().on('click',function(){ + var tmp=$(this).find("input[type=checkbox]"); + if(tmp.is(":checked")){ + $(this).find("input[type=checkbox]").prop("checked",false).change(); + $(this).addClass("select"); + } + else{ + $(this).find("input[type=checkbox]").prop("checked",true).change(); + $(this).removeClass("select"); + } + }); + + },1); + })(i) + } + } + map = new ol.Map({ + layers: layers, + target: 'map', + controls: ol.control.defaults({ + attributionOptions: /** @type {olx.control.AttributionOptions} */ ({ + collapsible: true + }) + }), + view: new ol.View({ + center: ol.proj.transform([0,0], 'EPSG:4326', 'EPSG:3857'), + zoom: 14, + maxZoom: 22, + minZoom: 1 + }) + }); + + var bstyle = function (feature, resolution) { + + /*var iconFont = 'glyphicon'; + var iconFontText = '\e062';*/ + //var iconFont = 'glyphicon'; + var iconFont = 'Glyphicons Halflings'; + var iconFontText = '\ue062'; + var iconSize = 24; + var opacity=0.4; + var col = 'rgba(0,255,0,0.6)'; + if(feature.get("source")=="GPS") + col = 'rgba(41,136,54,0.5)';//#298836 + else if(feature.get("source")=="Network"){ + col = 'rgba(91,176,75,0.4)';//#5bb04b + iconSize = 32; + opacity=0.2; + } + else if(feature.get("source")=="other"){ + col='rgba(129,208,113,0.5)';//#81d071 + iconSize = 36; + opacity=0.2; + } + else + col='rgba(166,63,39,0.5)'; //#a63f27 //"#EE0000"; + var styles = []; + + var styleIcon = new ol.style.Style({ + text: new ol.style.Text({ + font: 'Normal ' + iconSize + 'px ' + iconFont, + text: iconFontText, + fill: new ol.style.Fill({ color: col }) + }) + }); + styles.push(styleIcon); + + //console.log(feature.get("type")); + return styles; + /*return function (feature, resolution) { + styles.styleIcon.getText().setText(feature.get("iconCode")); + return styles; + };*/ + }; + position=JSON.parse(window.Android.getFullGPS()); + //console.log(JSON.stringify(position)); + if(position.length==0){ + position=[{lon:3.5,lat:43.5,source:"none"}]; + } + var iconFeatures = []; + for(var i=0;i=4) + map.getLayers().item(localTileIndex).setVisible(false); + }else{ + $(this).parent().find("i").removeClass("glyphicon-eye-close").addClass("glyphicon-eye-open"); + $("#dmopacity").show(); + console.log(JSON.stringify(map.getLayers().getLength())); + if(map.getLayers().getLength()0.05){ + map.getView().setRotation(-direction); + oldBearer=direction; + } + }else{ + window.Android.stopReportDirection(); + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/geolabs/dev/mapmint4me/MapMint4ME.java b/app/src/main/java/fr/geolabs/dev/mapmint4me/MapMint4ME.java index 0a7bdd9..555324a 100644 --- a/app/src/main/java/fr/geolabs/dev/mapmint4me/MapMint4ME.java +++ b/app/src/main/java/fr/geolabs/dev/mapmint4me/MapMint4ME.java @@ -293,21 +293,6 @@ public boolean shouldOverrideUrlLoading(WebView webView, String webResourceReque } } - public void OnButtonClickS(View V) - { - - Toast.makeText(this, "AR Scale", Toast.LENGTH_SHORT).show(); - Intent i = new Intent(MapMint4ME.this, com.hl3hl3.arcoremeasure.ArMeasureActivity.class); - startActivity(i); - - } - public void OnButtonClickD(View V) - { - - Toast.makeText(this, "AR Draw", Toast.LENGTH_SHORT).show(); - Intent i = new Intent(MapMint4ME.this, drawar.DrawAR.class); - startActivity(i); - } @@ -339,10 +324,22 @@ public void createNotificationChannel() { } } + public void launchWelcomeScreen() { startActivity(new Intent(getApplicationContext(), WelcomeScreen.class)); finish(); } + public void launchWelcomeScreen2() { + startActivity(new Intent(getApplicationContext(), com.hl3hl3.arcoremeasure.ArMeasureActivity.class)); + finish(); + } + public void launchWelcomeScreen3() { + startActivity(new Intent(getApplicationContext(), drawar.DrawAR.class)); + finish(); + } public void launchWelcomeScreen4() { + startActivity(new Intent(getApplicationContext(), CloudAnchor.class)); + finish(); + } public boolean mInternetActivated=false; diff --git a/app/src/main/java/fr/geolabs/dev/mapmint4me/WebAppInterface.java b/app/src/main/java/fr/geolabs/dev/mapmint4me/WebAppInterface.java index 398d765..997142e 100644 --- a/app/src/main/java/fr/geolabs/dev/mapmint4me/WebAppInterface.java +++ b/app/src/main/java/fr/geolabs/dev/mapmint4me/WebAppInterface.java @@ -114,9 +114,9 @@ public void notify(String msg) { .setContentText(msg) .setStyle(new NotificationCompat.BigTextStyle().bigText(msg)) .setPriority(NotificationCompat.PRIORITY_DEFAULT); - // Set the intent that will fire when the user taps the notification - //.setContentIntent(pendingIntent); - //.setAutoCancel(true); + // Set the intent that will fire when the user taps the notification + //.setContentIntent(pendingIntent); + //.setAutoCancel(true); NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mContext); currentId++; @@ -441,25 +441,25 @@ public String translate(String text) { /** * Set mTop to true/false from the web page - @JavascriptInterface - public String getOrientation() throws JSONException{ - try { - ((MapMint4ME)mContext).updateOrientationAngles(); - float[] res=((MapMint4ME)mContext).getOrientationAngles(); - Log.e("Error", "" + res.toString()); - JSONArray jsonArray = new JSONArray(); - for(int i=0;i<3;i++) - jsonArray.put(i,res[i]); - float[] res1=((MapMint4ME)mContext).getRotationMatrix(); - Log.e("Error", "" + res.toString()); - for(int i=0;i<3;i++) - jsonArray.put(i+3,res1[i]); - return jsonArray.toString(); - } catch (Exception e) { - Log.e("Error", "" + e.toString()); - return e.toString(); - } - }*/ + @JavascriptInterface + public String getOrientation() throws JSONException{ + try { + ((MapMint4ME)mContext).updateOrientationAngles(); + float[] res=((MapMint4ME)mContext).getOrientationAngles(); + Log.e("Error", "" + res.toString()); + JSONArray jsonArray = new JSONArray(); + for(int i=0;i<3;i++) + jsonArray.put(i,res[i]); + float[] res1=((MapMint4ME)mContext).getRotationMatrix(); + Log.e("Error", "" + res.toString()); + for(int i=0;i<3;i++) + jsonArray.put(i+3,res1[i]); + return jsonArray.toString(); + } catch (Exception e) { + Log.e("Error", "" + e.toString()); + return e.toString(); + } + }*/ private LocalDB db = null; @@ -570,11 +570,33 @@ public String downloadedFile() { } @JavascriptInterface - public void startWelcomeScreen(){ - ((MapMint4ME) mContext).launchWelcomeScreen(); - ((MapMint4ME) mContext).finish(); + + public void startWelcomeScreen(String s) { + + if (s.equals("scale")) { + + ((MapMint4ME) mContext).launchWelcomeScreen2(); + ((MapMint4ME) mContext).finish(); + } + if (s.equals("draw")) { + + ((MapMint4ME) mContext).launchWelcomeScreen3(); + ((MapMint4ME) mContext).finish(); + } + if (s.equals("cloud")) { + + ((MapMint4ME) mContext).launchWelcomeScreen4(); + ((MapMint4ME) mContext).finish(); + } + if (s.equals("help")) { + ((MapMint4ME) mContext).launchWelcomeScreen(); + ((MapMint4ME) mContext).finish(); + } } + + + @JavascriptInterface public void keepScreenOn(){ ((MapMint4ME) mContext).runOnUiThread(new Runnable() { @@ -759,7 +781,7 @@ public String getBaseLayers() throws JSONException, IOException { public String getGNStatus() throws JSONException { JSONObject json = new JSONObject(); ConnectivityManager connectivityManager - = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); + = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo(); json.put("net",(activeNetworkInfo!=null&&activeNetworkInfo.isConnected())); try{ diff --git a/app/src/main/res/layout/act_layout.xml b/app/src/main/res/layout/act_layout.xml index 9e47cd6..da03977 100644 --- a/app/src/main/res/layout/act_layout.xml +++ b/app/src/main/res/layout/act_layout.xml @@ -1,140 +1,142 @@ - - - - + android:id="@+id/content" + > - - - - - - - - - - - - - - - - - - - + android:layout_height="match_parent" + android:orientation="vertical"> + + + + + + + + + + + + + + - - - + - - - - - + android:background="@android:color/background_dark" + android:orientation="vertical" + android:paddingBottom="10dp" + android:paddingLeft="10dp" + android:paddingRight="10dp" + android:paddingTop="10dp"> + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_map_mint4_me.xml b/app/src/main/res/layout/activity_map_mint4_me.xml index 74a2626..98de9ee 100644 --- a/app/src/main/res/layout/activity_map_mint4_me.xml +++ b/app/src/main/res/layout/activity_map_mint4_me.xml @@ -9,32 +9,7 @@ - + + + SatFinder + + + - \ No newline at end of file + + \ No newline at end of file diff --git a/app/src/main/assets/model.sfb b/app/src/main/assets/model.sfb index 52f776588665dc870f980ea8b30d31fc08495c69..3a532b2e2efbf464f5cb61ade2818d8a279c8b4f 100644 GIT binary patch delta 103 zcmV~$u@!|d2mruG;VvnG5G1|bp8|<2q8n)8vvLd9`*-&<-Ouzw$$`Gj=%k{qK`CrW t)n%+?ArX>MDfBF`luYg$Q)}LxW8gYGuySvrNn+g03?+5H;JW~h-ybK(9hU$A delta 103 zcmV~$yA{GP36qgAw+L)Nikwtz!Ft?#-P?6 diff --git a/app/src/main/assets/scripts/gene4.js b/app/src/main/assets/scripts/gene4.js new file mode 100644 index 0000000..fd624f1 --- /dev/null +++ b/app/src/main/assets/scripts/gene4.js @@ -0,0 +1,2461 @@ +var MM4ME_DEBUG=true; +var EDITION_TYPE_FILE=5; +var mainTable={}; +var mtable=null; +var refTables={}; +var tblId=null; +var allTables={}; +var editSchema={}; +var referenceIds={}; +var lastEdition={}; +var tblName=null; +var toRunOnLoad={}; +var valuesOnLoad=[]; +var definedSqlTypes=[]; +var changingFields=[]; +var lang=window.Android.getLang(); +var langUrl=null; +if(lang=="fr"){ + langUrl="file:///android_asset/localisation/French.json"; +} + + + + +function loadWelcome5(){ + window.Android.startWelcomeScreen("sat_finder"); +} + +/***************************************************************************** + * Update status icon color depending on network and GPS availability + *****************************************************************************/ +function updateStatus(gps,internet){ + if(internet){ + $('.glyphicon-signal').css('color','#00EE00'); + }else + $('.glyphicon-signal').css('color','#EE0000'); + $('#gpsMenu1').next().html(""); + if(gps.length==0){ + $('#gpsMenu1').css('color','#EE0000'); + $('#gpsMenu1').next().html('
  • '+window.Android.translate("no_gps")+'
  • '); + }else{ + $('#gpsMenu1').css('color','#00EE00'); + for(var i=0;i '+gps[i].source+''); + } +} + +/***************************************************************************** + * List all available table for a given theme + *****************************************************************************/ +function fetchTableForTheme(obj,id){ + var hasTable=false; + var tables=JSON.parse(window.Android.displayTable( + "SELECT mm4me_tables.id as tid,mm4me_views.id as id,"+ + "mm4me_tables.name as name, "+ + "mm4me_tables.description, "+ + "mm4me_views.name as title "+ + " from mm4me_tables,mm4me_views"+ + " where mm4me_tables.id=mm4me_views.ptid "+ + "and mm4me_views.visible "+ + "and mm4me_views.id in (select vid from mm4me_views_themes where tid="+id+")",[])); + for(var i=0;i0; +} + +/***************************************************************************** + * Create a JSON Object containing the themes hierarchy + *****************************************************************************/ +function fetchThemes(obj,id){ + var cthemes=JSON.parse(window.Android.displayTable("SELECT id,name FROM mm4me_themes where pid = "+id,[])); + for(var i=0;i'); + $('#tree').treeview({ + data: allThemes, + onNodeSelected: function(event, data) { + if(data["myId"]){ + try{ + func(data["myId"],data["myName"],data["myTitle"],true); + }catch(e){ + console.log(JSON.stringify(data)); + console.log(e); + window.Android.showToast(e); + //exit(); + } + } + else{ + $('#tree').treeview('toggleNodeExpanded',[$("#tree").treeview('getSelected')[0], { silent: true }]); + $('#tree').treeview('toggleNodeSelected',[$("#tree").treeview('getSelected')[0], { silent: true }]); + //console.log(JSON.stringify(data)); + //$(this).open(); + } + }, + showTags: true + }); + var list=JSON.parse(window.Android.displayTable("SELECT mm4me_tables.id as tid,mm4me_views.id as id,mm4me_tables.name as name,mm4me_tables.description,mm4me_views.name as title from mm4me_tables,mm4me_views where mm4me_tables.id=mm4me_views.ptid and mm4me_views.visible",[])); + var tableList=list; + var total=0; + contents=[]; + for(var i in list){ + mainTable[list[i]["id"]]=list[i]["tid"]; + } + } + catch(e){ + console.log(e); + displayNoListing(); + } +} + +/***************************************************************************** + * Update the breadcrumbs text to translated string + *****************************************************************************/ +function updateBreadcrumbs(breadcrumbs){ + //var breadcrumbs=["home","view"]; + var lcnt0=0; + $('.breadcrumb').find("a").each(function(){ + $(this).append(window.Android.translate(breadcrumbs[lcnt0])); + lcnt0+=1; + }); + $('.breadcrumb').find("li").last().each(function(){ + $(this).append(window.Android.translate(breadcrumbs[lcnt0])); + lcnt0+=1; + }); +} + +/***************************************************************************** + * Convert table names in a string from the PostgreSQL to our SQLite format. + * So, convert "AAAXXX.YYYBBB" to "AAAXXX_YYYBBB" + *****************************************************************************/ +function cleanupTableName(name){ + return name.replace(/(\w+)(\d*)\.(\d*)(\w+)/g,"$1$2_$3$4"); +} + +/***************************************************************************** + * Display an HTML part containing the image produced by Camera or picked from + * the photo library. + *****************************************************************************/ +function loadNewPicture(cid,id,picture){ + $(".tab-pane").each(function(){ + if($(this).is(":visible")) + $(this).find("#value_"+id) + .html('
    '+picture+'
    '); + }); +} + + +/***************************************************************************** + * Create a JSON Object representing the dependency values. + *****************************************************************************/ +function fetchDependencies(obj,cid,changingField){ + var list1=null; + //console.log(cid+" "+JSON.stringify(obj)); + + for(var key in changingField){ + for(var i=0;i'; + tmpStr+=res+ + '
    '; + console.log(tmpStr); + return tmpStr; + } + if(definedSqlTypes[i]["code"]=="geometry"){ + console.log("CID: "+cid+" select type from mm4me_gc where f_table_schema||'_'||f_table_name = (select name from mm4me_tables where id="+cid+") ") + var geoType=getGeometryType("(select replace(name,'.','_') from mm4me_tables where id="+cid+")"); + var viewb=''; + var res=' '; + if(geoType=='POINT' || geoType=='MULTIPOINT' ) + return res+''+ + '

    GPS Informations

    Type
    Coords
    '+ + ''; + else if(geoType=='LINESTRING' || geoType=='MULTILINESTRING' ) + return res+''+ + ''+ + '
    '+viewb+'
    '; + else if(geoType=='POLYGON' || geoType=='MULTIPOLYGON' ) + return res+''+ + ''+ + '
    '+viewb+'
    '; + + } + if(definedSqlTypes[i]["code"]=="tbl_linked"){ + var tmp=obj["value"].split(';'); + //console.log(cleanupTableName('SELECT '+tmp[1]+' FROM '+tmp[2]+' WHERE '+tmp[0]+'='+tblName+'.id')); + var refs=JSON.parse(window.Android.displayTable(cleanupTableName(tmp[3]),[])); + var tmpStr='"; + + } + if(definedSqlTypes[i]["code"]=="link"){ + var strReturn; + if(!View_template) + $.ajax({ + async: false, + method: "GET", + url: './content/view_template.html', + error: function(){ + console.log("Nothing to run after"); + }, + success: function(data){ + console.log("**** \n\n Load View Template"); + View_template=data; + var tmp=obj["value"].split(";"); + var refs=JSON.parse(window.Android.displayTable("SELECT mm4me_tables.id as tid,mm4me_views.id as id,mm4me_tables.name as name,mm4me_tables.description,mm4me_views.name as title from mm4me_tables,mm4me_views where mm4me_tables.name=\""+tmp[1]+"\" and mm4me_tables.id=mm4me_views.ptid",[])); + var reg=new RegExp("\\[id\\]","g"); + if(!valuesOnLoad[cid]) + valuesOnLoad[cid]=[]; + if(refs!=""){ + valuesOnLoad[cid].push(refs); + if(!toRunOnLoad[cid]) + toRunOnLoad[cid]=[]; + toRunOnLoad[cid].push(function(){ + prefix="_"+arguments[0]["0"]["id"]; + //if(!mainTable[arguments[0]["0"]["id"]]) + //mainTable[arguments[0]["0"]["id"]]=arguments[0]["0"]["tid"]; + //console.log(arguments[0]["0"]["tid"]+" "+arguments[0]["0"]["name"]+" "+arguments[0]["0"]["title"]); + listInnerTable(arguments[0]["0"]["tid"],arguments[0]["0"]["id"],arguments[0]["0"]["name"],arguments[0]["0"]["title"],false,prefix,tmp[0]+"="+arguments[1]["local_id"]); + }); + refTables[refs["0"]["tid"]]={"oid":cid,"col":tmp[0],"vid":refs["0"]["id"],"name":refs["0"]["table"],"name":refs["0"]["title"]}; + strReturn=data.replace(reg,refs["0"]["tid"]); + } + } + }); + else{ + var tmp=obj["value"].split(";"); + var refs=JSON.parse(window.Android.displayTable("SELECT mm4me_tables.id as tid,mm4me_views.id as id,mm4me_tables.name as name,mm4me_tables.description,mm4me_views.name as title from mm4me_tables,mm4me_views where mm4me_tables.name=\""+tmp[1]+"\" and mm4me_tables.id=mm4me_views.ptid",[])); + var reg=new RegExp("\\[id\\]","g"); + if(!valuesOnLoad[cid]) + valuesOnLoad[cid]=[]; + valuesOnLoad[cid].push(refs); + if(!toRunOnLoad[cid]) + toRunOnLoad[cid]=[]; + toRunOnLoad[cid].push(function(){ + prefix="_"+arguments[0]["0"]["id"]; + //if(!mainTable[arguments[0]["0"]["id"]]) + //mainTable[arguments[0]["0"]["id"]]=arguments[0]["0"]["tid"]; + //console.log(arguments[0]["0"]["tid"]+" "+arguments[0]["0"]["name"]+" "+arguments[0]["0"]["title"]); + listInnerTable(arguments[0]["0"]["tid"],arguments[0]["0"]["id"],arguments[0]["0"]["name"],arguments[0]["0"]["title"],false,prefix,tmp[0]+"="+arguments[1]["local_id"]); + }); + refTables[refs["0"]["tid"]]={"oid":cid,"col":tmp[0],"vid":refs["0"]["id"],"name":refs["0"]["table"],"name":refs["0"]["title"]}; + strReturn=View_template.replace(reg,refs["0"]["tid"]); + + } + //console.log(strReturn); + return strReturn; + } + if(definedSqlTypes[i]["code"]=="ref"){ + var req=obj["value"];//.replace(/^\((\w+)\)$/g,"$1"); + if(req[0]=="(") + req="SELECT * FROM "+req; + var refs=JSON.parse(window.Android.displayTable(cleanupTableName(req),[])); + var tmpStr='"; + + if(obj["dependencies"]) + try{ + var lobj={}; + lobj[obj["id"]]={"dep":JSON.parse(obj["dependencies"])}; + for(var jj=0;jj=0){ + return ''; + } + if(definedSqlTypes[i]["code"]=="html") + return ''; + if(definedSqlTypes[i]["code"]=="text") + return ''; + if(definedSqlTypes[i]["code"]=="boolean") + return ''; + if(definedSqlTypes[i]["code"]=="date" || definedSqlTypes[i]["code"]=="datetime") + return ''; + if(definedSqlTypes[i]["code"]=="float") + return ''; + return definedSqlTypes[i]["code"]; + } + } + return null; +} + +function printOptionalCheckbox(obj,cid){ + if(definedSqlTypes.length==0){ + definedSqlTypes=JSON.parse(window.Android.displayTable("select id,code from mm4me_ftypes where ftype='e' order by name",[])); + } + + var res=' '; + for(var i in definedSqlTypes){ + if(definedSqlTypes[i]["id"]==obj["ftype"]){ + if(definedSqlTypes[i]["code"]=="bytea"){ + var tmpStr=""; + return res; + } + else if(definedSqlTypes[i]["code"]=="geometry"){ + return res + } + } + } + return ''; +} + +var refTypeId=null; +var editPrintedOnce=[]; +/***************************************************************************** + * Create HTML part to display the line containing both the title and the + * corresponding input for a given table's field. + *****************************************************************************/ +function printEditionFields(obj,myRoot,cid,mid){ + var list1=window.Android.displayTable("select * from mm4me_edition_fields where mm4me_edition_fields.edition>0 and eid="+obj["id"]+" order by mm4me_edition_fields.id asc",[]); + if(!editSchema[mid]) + editSchema[mid]={}; + editSchema[mid][obj["id"]]=JSON.parse(list1); + list1=JSON.parse(list1); + myRoot.find(".tab-content").first().append('
    '+obj["description"]+'
    '); + for(var j in list1) + if(list1[j]["edition"]>0) { + myRoot.find(".tab-content").first().children().last().append( + '
    '+ + '
    '+ + ''+ + '
    '+ + '
    '+ + printCurrentType(list1[j],mid)+ + '
    '+ + '
    '); + if(list1[j]["dependencies"]){ + try{ + editPrintedOnce.push(list1[j]["name"]); + console.log("JSON PARSE") + console.log(list1[j]["dependencies"]); + var objJson=JSON.parse(list1[j]["dependencies"]); + console.log("JSON PARSE OK"); + if(!refTypeId) + refTypeId=JSON.parse(window.Android.displayTable("select id from mm4me_ftypes where ftype='e' and code='ref'",[]))[0]["id"]; + console.log(refTypeId); + for(i in objJson){ + if(objJson[i]["myself"]){ + console.log("IS MYSELF!!"); + for(k in objJson[i]["myself"]){ + for(l in objJson[i]["myself"][k]){ + + + if(objJson[i]["myself"][k][l]["dependents"]){ + for(m in objJson[i]["myself"][k][l]["dependents"]){ + for(n in objJson[i]["myself"][k][l]["dependents"][m]){ + console.log(objJson[i]["myself"][k][l]["dependents"][m][n]["sql_query"]); + console.log(objJson[i]["myself"][k][l]["dependents"][m][n]["label"]); + var lObj={"id": n, "ftype":refTypeId,"value":objJson[i]["myself"][k][l]["dependents"][m][n]["sql_query"]}; + //if(!myRoot.find('select[name="field_'+list1[j]["id"]+'"]').parent().find('select[name="field_'+n+'"]').length) + myRoot.find('select[name="field_'+list1[j]["id"]+'"]').last().parent().prepend( + '
    '+ + '
    '+ + ''+ + '
    '+ + '
    '+ + printCurrentType(lObj,mid)+ + '
    '+ + '
    '); + console.log(myRoot.find('select[name="field_'+n+'"]')); + console.log(printCurrentType(lObj,mid)); + (function(a,b){ + myRoot.find('select[name="field_'+n+'"]').off('change'); + myRoot.find('select[name="field_'+n+'"]').on('change',function(){ + console.log('select[name="field_'+n+'"]'); + var req=cleanupTableName(a["value"]); + if(a["value"].indexOf("WHERE")<0){ + req=req.replace(/order by/g,"where "+b["tfieldf"]+" "+b["operator"]+" "+(b["operator"]=="like"?"'":"")+$(this).val()+(b["operator"]=="like"?"'":"")+" order by") + } + console.log(req); + var res=JSON.parse(window.Android.displayTable(req,[])); + myRoot.find('select[name="field_'+a["id"]+'"]').html(""); + for(ij in res){ + var tmpStr=' '; + cnt+=1; + } + cnt0+=1; + $("select[name=field_"+changingField[j][ckey]["id"]+"]").append(cStr); + } + if(i==0) + $("select[name=field_"+changingField[j][ckey]["id"]+"]").html(''); + $("select[name=field_"+changingField[j][ckey]["id"]+"]").change(); + }else{ + console.log("DISPLAY ELEMENT IF CINDEX >=0 "); + //console.log(JSON.stringify(changingField[j][ckey])); + console.log('input[name="field_'+ckey+'"],select[name="field_'+ckey+'"],textarea[name="field_'+ckey+'"]'); + console.log($('input[name="field_'+changingField[j][ckey]["id"]+'"],select[name="field_'+changingField[j][ckey]["id"]+'"],textarea[name="field_'+changingField[j][ckey]["id"]+'"]').parent().parent().html()); + var mycKey=changingField[j][ckey]["id"]; + if(cIndex<0) + $('input[name="field_'+mycKey+'"],select[name="field_'+mycKey+'"],textarea[name="field_'+mycKey+'"]').parent().parent().hide(); + else + $('input[name="field_'+mycKey+'"],select[name="field_'+mycKey+'"],textarea[name="field_'+mycKey+'"]').parent().parent().show(); + } + } + } + }; + }; + $("select[name=field_"+key+"]").off('change'); + $("select[name=field_"+key+"]").change(localFunc(changingFields[i][key]["dep"])); + $("select[name=field_"+key+"]").change(); + } + } + }catch(e){ + console.log(e); + setTimeout(function() { updateChangingFields(changingFields) }, 500); + } +} + +/***************************************************************************** + * Display the content of a table referencing the current edited table. + *****************************************************************************/ +function listInnerTable(id,vid,name,title,init,prefix,clause,ref){ + var list=JSON.parse(window.Android.displayTable("select mm4me_tables.id as tid,mm4me_tables.name as tname,mm4me_editions.id,mm4me_editions.name from mm4me_editions,mm4me_tables where mm4me_editions.ptid=mm4me_tables.id and mm4me_tables.id="+id+" order by mm4me_editions.step asc",[])); + var cnt=0; + var detectInit=true; + var tid=0; + for(var i in list){ + lastEdition[list[i]["id"]]=list[i]; + tid=list[i]["tid"]; + if(!allTables[list[i]["tid"]]){ + allTables[list[i]["tid"]]={"id":list[i]["id"],"name":list[i]["tname"],"title":list[i]["name"]}; + detectInit=false; + $("#sub_tableContent_"+id+"").find(".mm4me_edition").find("ul").first().append(''); + printEditionFields(list[i],$("#sub_tableContent_"+id+"").find(".mm4me_edition"),list[i]["id"],list[i]["tid"]); + if(cnt==0){ + try{ + var cid=list[i]["id"]+"_0"; + $("#sub_tableContent_"+id+"").find("ul").first().append('
  • '+window.Android.translate('table')+'
  • '); + $("#sub_tableContent_"+id+"").find("ul").first().append(''); + $("#sub_tableContent_"+id+"").find("ul").first().append(''); + $("#sub_tableContent_"+id+" .require-select").hide(); + printEditionFields(list[i],$("#sub_tableContent_"+id+""),cid,list[i]["tid"]); + }catch(e){ + console.log("**** ERROR ***> "+e); + } + } + var myRoot=$("#sub_tableContent_"+id+""); + myRoot.find('ul').first().find('a').click(function (e) { + e.preventDefault(); + if($(this).parent().hasClass('require-select')) + myRoot.find('.mm4me_edition').show(); + else + myRoot.find('.mm4me_edition').hide(); + $(this).tab('show'); + }) + myRoot.find('ul').first().find('a').first().click(); + myRoot.find('.mm4me_edition').find('ul').find('a').first().click(); + myRoot.find('.swagEditor').summernote(); + myRoot.find(".mm-act-add").click(function(){ + runInsertQuery($(this).parent().parent(),tid,editTableReact); + }); + myRoot.find(".mm-act-save").click(function(){ + runUpdateQuery($(this).parent().parent(),tid,editTableReact); + }); + + } + cnt+=1; + } + + var list=JSON.parse(window.Android.displayTable("SELECT id,name,value,alias,width,class from mm4me_view_fields where vid="+vid+" order by id",[])); + var columns=[]; + var columnNames=[]; + var sqlColumns=""; + var orderColumn="id"; + var orderType="asc"; + for(var i=0;i0){ + orderColumn=list[i]["name"]; + if(list[i]["class"]==1) + orderType="desc"; + } + } + var ccol=getPKey(cleanupTableName(name)); + var req="SELECT "+ccol+" as ogc_fid FROM "+cleanupTableName(name)+" WHERE "+clause+" ORDER BY "+cleanupTableName(name)+"."+orderColumn+" "+orderType; + var list1=JSON.parse(window.Android.displayTable(req,[])); + req="SELECT "+sqlColumns+" FROM "+cleanupTableName(name)+" WHERE "+clause+" ORDER BY "+cleanupTableName(name)+"."+orderColumn+" "+orderType; + var list=JSON.parse(window.Android.displayTable(req,[])); + var dataSet = []; + for(var i=0;i'; + cnt+=1; + } + dataSet.push(tmpData); + } + var selected = []; + + //$("#edition_form_table").html('
    '); + var localName="exampleTable_"+id; + ((function(localName,sid){ + + if(!detectInit){ + + var options={ + data: dataSet, + columns: columns, + "scrollX": true, + scrollY: '50vh', + scrollCollapse: true, + select: true, + "rowCallback": function( row, data ) { + if ( $.inArray(data.DT_RowId, selected) !== -1 ) { + $(row).addClass('selected'); + } + } + }; + if(langUrl!=null) + options["language"]={ + url: langUrl + }; + + $('#'+localName).DataTable( options ); + $('#'+localName+' tbody').on('click', 'tr', function () { + var id = this.id; + var index = $.inArray(id, selected); + + if ( index === -1 ) { + selected.push( id ); + } else { + selected.splice( index, 1 ); + } + displayEditForm(sid,$(this).find("input[name=id]").first().val(),true); + var tmp=$(this).hasClass('selected'); + $('#'+localName+' tbody tr').removeClass('selected'); + $(this).toggleClass('selected'); + if(!tmp) + $(this).toggleClass('selected'); + } ); + + }else{ + $('#'+localName).dataTable().fnClearTable(); + if(dataSet.length>0) + $('#'+localName).dataTable().fnAddData(dataSet); + } + $("#sub_tableContent_"+id+"").find("ul").first().find('a').first().click(); + })(localName,tid)); + +} + +var onFormFirstLoad=null; +/***************************************************************************** + * Show the edit form + *****************************************************************************/ +function displayEditForm(cid,selectedId,basic){ + if(basic && !$("#exampleTable"+(cid==mtable?"":"_"+cid)).find(".selected").find('input[type=hidden]').first().val()){ + if(cid==mtable){ + $("#main_tableContent").find(".require-select").hide(); + $("#main_tableContent").find("ul").first().find("a").first().click(); + if($(".breadcrumb").children().length==4) + $(".breadcrumb").children().last().remove(); + + }else{ + $("#sub_tableContent_"+cid).find(".require-select").hide(); + $("#sub_tableContent_"+cid).find("ul").first().find("a").first().click(); + } + return; + } + var fields=[] + var sizedFields=[]; + var sizedFieldsAlias=[]; + var notSizedFields=[]; + for(var i in editSchema[cid]){ + for(var j in editSchema[cid][i]){ + //console.log(JSON.stringify(editSchema[cid][i][j])); + if(editSchema[cid][i][j]["ftype"]=="5"){ + sizedFields.push(editSchema[cid][i][j]["name"].replace(/wkb_geometry/g,"geometry")); + sizedFieldsAlias.push(editSchema[cid][i][j]["id"]); + } + else{ + notSizedFields.push(editSchema[cid][i][j]["name"].replace(/wkb_geometry/g,"geometry")+" AS \""+editSchema[cid][i][j]["id"]+"\""); + try{ + var tmp=JSON.parse(editSchema[cid][i][j]["dependencies"]); + var sqlReq=""; + var sqlClause=""; + var sqlParams=""; + var sqlParam=0; + var alphabet = 'abcdefghijklmnopqrstuvwxyz'.split(''); + var hasDep=false; + var previousElements=[]; + for(k in tmp) + for(l in tmp[k]){ + //console.log(JSON.stringify(tmp[k][l])); + if(l=="myself"){ + for(m in tmp[k][l]) + for(n in tmp[k][l][m]){ + //console.log(JSON.stringify(tmp[k][l][m][n])); + if(sqlReq!=""){ + sqlReq+=", "; + sqlParams+=", "; + //sqlClause+=" WHERE "+alphabet[sqlParam-1]+"."+tmp[k][l][m][n]["tfield"]+"="+alphabet[sqlParam]+"."+tmp[k][l][m][n]["tfield"]; + + } + sqlReq+="("+cleanupTableName(tmp[k][l][m][n]["sql_query"])+") as "+alphabet[sqlParam]; + sqlParams+=alphabet[sqlParam]+"."+tmp[k][l][m][n]["tfield"]; + sqlParam+=1; + if(tmp[k][l][m][n]["dependents"]){ + //console.log(JSON.stringify(tmp[k][l][m][n]["dependents"])); + for(var o in tmp[k][l][m][n]["dependents"]){ + //console.log(JSON.stringify(tmp[k][l][m][n]["dependents"][o])); + for(var p in tmp[k][l][m][n]["dependents"][o]){ + console.log(tmp[k][l][m][n]["dependents"][o][p]["sql_query"].replace(/from/,","+tmp[k][l][m][n]["dependents"][o][p]["tfield"]+" from")); + sqlReq+=", ("+cleanupTableName(tmp[k][l][m][n]["dependents"][o][p]["sql_query"]).replace(/from/,","+tmp[k][l][m][n]["dependents"][o][p]["tfield"]+" from")+") as b"; + sqlParams+=", b."+tmp[k][l][m][n]["dependents"][o][p]["tfieldf"]; + sqlParam+=2; + sqlReq+=", ("+cleanupTableName(editSchema[cid][i][j]["value"]).replace(/from/,","+tmp[k][l][m][n]["dependents"][o][p]["tfieldf"]+" from")+") as c"; + + sqlClause+=" WHERE c."+tmp[k][l][m][n]["dependents"][o][p]["tfieldf"]+"=b."+tmp[k][l][m][n]["dependents"][o][p]["tfieldf"]; + sqlClause+=" AND b."+tmp[k][l][m][n]["tfield"]+"=a."+tmp[k][l][m][n]["tfield"]; + sqlClause+=" AND c.id=(SELECT "+editSchema[cid][i][j]["name"]+" FROM "+cleanupTableName(allTables[cid].name)+" where id="+selectedId+")"; + hasDep=true; + } + } + } + } + if(!hasDep){ + var tmpReq=cleanupTableName(editSchema[cid][i][j]["value"]); + for(m in tmp[k][l]) + for(n in tmp[k][l][m]){ + tmpReq=tmpReq.replace(/from/,","+tmp[k][l][m][n]["tfield"]+" from"); + if(sqlClause=="") + sqlClause+=" WHERE "; + else + sqlClause+=" "+tmp[k][l][m][n]["cond_join"]+" "; + sqlClause+=alphabet[m]+"."+tmp[k][l][m][n]["tfield"]+"=a1."+tmp[k][l][m][n]["tfield"]; + sqlClause+=""; + } + sqlClause+=" AND a1.id=(SELECT "+editSchema[cid][i][j]["name"]+" FROM "+cleanupTableName(allTables[cid].name)+" where id="+selectedId+")"; + //sqlReq=(tmpReq); + console.log(tmpReq); + sqlReq+=", ("+tmpReq+") as a1"; + } + //console.log(JSON.stringify(tmp[k][l])); + console.log("SELECT "+sqlParams+" FROM "+sqlReq+" "+sqlClause); + var localQuery="SELECT "+sqlParams+" FROM "+sqlReq+" "+sqlClause; + var res0=JSON.parse(window.Android.displayTable(localQuery,[])); + console.log(JSON.stringify(res0)); + for(m in res0) + for(n in res0[m]){ + try{ + console.log("input[name=field_"+n+"],select[name=field_"+n+"],textarea[name=field_"+n+"]"); + /**/ + if($('.mm4me_edition').find("input[name=field_"+n+"],select[name=field_"+n+"],textarea[name=field_"+n+"]").first().length) + $('.mm4me_edition').find("input[name=field_"+n+"],select[name=field_"+n+"],textarea[name=field_"+n+"]").first().val(res0[m][n]).change(); + else{ + for(m0 in tmp[k][l]) + for(n0 in tmp[k][l][m0]){ + /*console.log(n0.indexOf(n)<0); + console.log(n); + console.log(n0); + console.log(res0[m][n]);*/ + if(n0.indexOf(n)>=0){ + console.log("input[name=field_"+n0+"],select[name=field_"+n0+"],textarea[name=field_"+n0+"]"); + if(!$('.mm4me_edition').find("input[name=field_"+n+"],select[name=field_"+n+"],textarea[name=field_"+n+"]").first().length) + $('.mm4me_edition').find("input[name=field_"+n0+"],select[name=field_"+n0+"],textarea[name=field_"+n0+"]").first().val(res0[m][n]).change(); + } + } + } + }catch(e){ + console.log(e); + } + + } + //if(tmp[k][l]["dependents"]) + }else{ + console.log(JSON.stringify(tmp[k][l])); + if(tmp[k][l]["tfield"]=="none"){ + var isNull=JSON.parse(window.Android.displayTable("SELECT CASE WHEN "+l+" is null THEN 1 ELSE 0 END as p FROM "+cleanupTableName(allTables[cid].name)+" where id="+selectedId,[])); + console.log(JSON.stringify(isNull)); + if(isNull[0]["p"]=="0"){ + console.log(JSON.stringify(isNull)); + console.log("input[name=field_"+editSchema[cid][i][j]["id"]+"],select[name=field_"+editSchema[cid][i][j]["id"]+"],textarea[name=field_"+editSchema[cid][i][j]["id"]+"]"); + console.log(k+""); + $('.mm4me_edition').find("input[name=field_"+editSchema[cid][i][j]["id"]+"],select[name=field_"+editSchema[cid][i][j]["id"]+"],textarea[name=field_"+editSchema[cid][i][j]["id"]+"]").first().val(k+"").change(); + } + } + + } + } + + //if(sqlReq) + }catch(e){ + console.log(e); + } + } + if(editSchema[cid][i][j]["name"].indexOf("unamed")<0) + fields.push(editSchema[cid][i][j]["name"].replace(/wkb_geometry/g,"geometry")+" AS \""+editSchema[cid][i][j]["id"]+"\""); + } + } + var ccol=getPKey(cleanupTableName(allTables[cid].name)); + /* $("#exampleTable"+(cid==mtable?"":"_"+cid)).find(".selected").find('input[type=hidden]').first().val() */ + var editValues; + if(sizedFields.length>0){ + for(var i=0;i 1000000 and "+ccol+"="+selectedId,[])); + //console.log(JSON.stringify(hasElement[0])); + if(hasElement[0]["cnt"]!="0"){ + var nbIteration=parseInt(hasElement[0]["len"])/1000000; + var zfields=[] + var len=0; + var query=""; + var len1=parseInt(hasElement[0]["len"]); + for(var j=0;j0){ + $("#field_"+j+"_map").show(); + $("#field_"+j+"_map").off('click'); + $("#field_"+j+"_map").on('click',function(){ + console.log("Display table with a selected feature"); + console.log($(this).prev().val()); + showElementOnMap($(this).prev().val()); + }); + } + if($(".btn_field_"+j+"_lat").length>0){ + //alert(editValues[i][j]); + $(".btn_field_"+j+"_lat").html(editValues[i][j]); + $(".btn_field_"+j+"_long").html(""); + $(".btn_field_"+j+"_source").html(""); + $("input[name='field_"+j+"']").val("POINT"+editValues[i][j]); + + } + } + //$(".swagEditor").summernote(); + } + } + + for(var i in editSchema[cid]){ + for(var j in editSchema[cid][i]){ + if(editSchema[cid][i][j]["name"].indexOf("unamed")>=0){ + if(editSchema[cid][i][j]["ftype"]==6){ + var tmp=editSchema[cid][i][j]["value"].split(';'); + var list=JSON.parse(window.Android.displayTable("select "+tmp[1]+" from "+cleanupTableName(tmp[2])+" where "+tmp[0]+"=(SELECT id from "+cleanupTableName(tblName)+" WHERE ogc_fid="+selectedId+")",[])); + editValues["0"][editSchema[cid][i][j]["id"]]=list; + for(var k in list){ + for(var l in list[k]){ + $('.mm4me_edition').find("select[name=field_"+editSchema[cid][i][j]["id"]+"]").find('option').each(function(){ + if($(this).val()==list[k][l]) + $(this).prop("selected",true); + }); + } + } + } + } + } + } + + if(cid==mtable){ + if($(".breadcrumb").children().length==4) + $(".breadcrumb").children().last().remove(); + $(".breadcrumb").append('
  • '+editValues["0"]["local_id"]+'
  • '); + } + ((cid==mtable)?$(".require-select"):$("#sub_tableContent_"+cid).find(".require-select")).first().show(); + ((cid==mtable)?$(".require-select"):$("#sub_tableContent_"+cid).find(".require-select")).first().find("a").first().click(); + + if(toRunOnLoad[cid]) + for(var i=0;i=0 order by mm4me_editions.step asc",[]); + if(MM4ME_DEBUG) + console.log(list); + list=JSON.parse(list); + if(MM4ME_DEBUG) + console.log((!allTables[tblId])); + if(!allTables[tblId]){ + allTables[tblId]={"id":id,"name":name,"title":title}; + } + mtable=tblId; + + $(".mm4me_edition").find("ul").first().html(""); + $(".mm4me_edition").find(".well").first().html(""); + var cnt=0; + for(var i in list){ + lastEdition[list[i]["id"]]=list[i]; + var cid=(i==0?list[i]["id"]+"_0":list[i]["id"]); + $(".mm4me_edition").find("ul").first().append(''); + printEditionFields(list[i],$("#edition_form_edit"),cid,mainTable[id]); + } + if(list.length==1) + $(".mm4me_edition").find("ul").first().hide(); + + var aCnt=0; + $('.mm4me_edition').find('ul').first().find('a').each(function () { + if(aCnt>0) + $(this).parent().addClass('require-select'); + aCnt+=1; + }); + $(".require-select").hide(); + $('.mm4me_listing').find('ul').first().find('a').first().click(); + $('.mm4me_edition').find('ul').find('a').first().click(); + $('.swagEditor').summernote(); + $(".mm-act-add").click(function(){ + runInsertQuery($(this).parent().parent(),mainTable[id],editOnlyTableReact); + }); + $(".mm-act-save").click(function(){ + runUpdateQuery($(this).parent().parent(),mainTable[id],editOnlyTableReact); + }); + $(".breadcrumb").children().last().remove(); + $(".breadcrumb").append('
  • '+window.Android.translate('edit')+'
  • '); + $(".breadcrumb").append('
  • '+tblTitle+'
  • '); + + setTimeout(function() { updateChangingFields(changingFields) }, 1500); + + /*for(var i=0;i'; + else + cStr+=changingField[j][ckey]["values"][cIndex][i][lkey]+''; + cnt+=1; + } + $("select[name=field_"+changingField[j][ckey]["id"]+"]").append(cStr); + } + if(i==0) + $("select[name=field_"+changingField[j][ckey]["id"]+"]").html(''); + } + } + }; + }; + $("select[name=field_"+key+"]").off('change'); + $("select[name=field_"+key+"]").change(localFunc(changingFields[i][key]["dep"])); + $("select[name=field_"+key+"]").change(); + } + }*/ + $('.mm4me_listing').show(); + $('.mm4me_content').hide(); + +} + +/***************************************************************************** + * In case no server is configured + *****************************************************************************/ +function displayNoListing(){ + $.ajax({ + method: "GET", + url: 'content/nolisting.html', + success: function(data){ + if(MM4ME_DEBUG) + console.log('Display warning message on the UI !'); + $(".mm4me_content").html(data); + $(".mm4me_content").find(".pannel-body").find("p").first().html(window.Android.translate("mm_no_db_found")); + }, + error: function(){ + window.Android.showToast("error !"); + } + }); +} + + +/***************************************************************************** + * Authenticate a user + *****************************************************************************/ +function authenticate(url,login,passwd,func,func1){ + var curl=url+"?service=WPS&request=Execute&version=1.0.0&Identifier=authenticate.clogIn&DataInputs=login="+login+";password="+passwd+"&RawDataOutput=Result"; + if(MM4ME_DEBUG) + console.log(curl); + $.ajax({ + method: "GET", + url: curl, + success: function(data){ + if(MM4ME_DEBUG) + console.log(data); + if(func){ + console.log("Call func!") + func(); + } + }, + error: function(){ + if(func1){ + func1(); + } + else{ + disconnect(url); + if(MM4ME_DEBUG) + console.log("unable to login!"); + var hasBeenShown=false; + var xml=arguments[0].responseText; + $(xml).find("ows\\:ExceptionText").each(function(){ + window.Android.showToast($(this).text()); + hasBeenShown=true; + }); + if(!hasBeenShown){ + window.Android.showToast(JSON.stringify(arguments)); + } + } + + } + }); +} + +/***************************************************************************** + * Disconnect a user + *****************************************************************************/ +function disconnect(url,func,func1){ + var curl=url+"?service=WPS&request=Execute&version=1.0.0&Identifier=authenticate.clogOut&DataInputs=&RawDataOutput=Result"; + if(MM4ME_DEBUG) + console.log(curl); + $.ajax({ + method: "GET", + url: curl, + success: function(data){ + if(MM4ME_DEBUG){ + console.log(data); + console.log("** Your are no more connected!"); + } + if(func){ + func(); + } + }, + error: function(){ + if(MM4ME_DEBUG){ + console.log(curl); + console.log("unable to disconnect!"); + } + if(func1){ + func1(); + } + } + }); +} + +var geometries={"line":{"geom":null,"constructor": "ol.geom.Linestring"},"polygon":{"geom":null,"constructor":"ol.geom.Polygon","cline":null}}; +var stopTracking=false; +var currentGeometry="line"; +var currentGeometryField="none"; +var map=null; +var vectorLayer,vectorLayer1; +var position; + +/***************************************************************************** + * Track modification of the GPS location + *****************************************************************************/ +function trackCurrentGPSPosition(){ + console.log("## trackCurrentGPSPosition"); + updateCurrentMapLocation(); + + var tmp0=geometries["origin"].getCoordinates(); + if(geometries[currentGeometry]["geom"]!=null){ + var tmp1=geometries[currentGeometry]["geom"].getCoordinates(); + tmp0=tmp1[tmp1.length-1]; + } + tmp=ol.proj.transform([position[bestIndex].lon,position[bestIndex].lat], 'EPSG:4326','EPSG:3857'); + console.log(JSON.stringify(tmp0)); + console.log(JSON.stringify(tmp)); + $("#currentPosition").html("Position: "+position[bestIndex].lon.toFixed(6)+","+position[bestIndex].lat.toFixed(6)+""); + if(tmp0[0]==tmp[0] && tmp0[1]==tmp[1]){ + console.log(" !!!!!!!! Same position!!!!!!!! "); + if(!stopTracking) + setTimeout(function() { trackCurrentGPSPosition();}, 1000); + return; + } + + console.log(JSON.stringify(tmp0)); + console.log(JSON.stringify(tmp)); + if(geometries[currentGeometry]["geom"]==null){ + //geometries[currentGeometry]=new geometries[currentGeometry]["constructor"]([tmp0,tmp]); + if(currentGeometry=="line") + geometries[currentGeometry]["geom"]=new ol.geom.LineString([tmp0,tmp]); + else + geometries[currentGeometry]["geom"]=new ol.geom.LineString([tmp0,tmp]); + }else{ + if(currentGeometry=="line"){ + tmpCoordinates=geometries[currentGeometry]["geom"].getCoordinates(); + tmpCoordinates.push(tmp); + geometries[currentGeometry]["geom"]=new ol.geom.LineString(tmpCoordinates); + } + else{ + if(geometries[currentGeometry]["cline"]==null) + tmpCoordinates=geometries[currentGeometry]["geom"].getCoordinates(); + else + tmpCoordinates=geometries[currentGeometry]["cline"].getCoordinates(); + console.log(JSON.stringify(tmpCoordinates)); + tmpCoordinates.push(tmp); + console.log(JSON.stringify(tmpCoordinates)); + if(tmpCoordinates.length>2){ + geometries[currentGeometry]["cline"]=new ol.geom.LineString(tmpCoordinates); + tmpCoordinates.push(tmpCoordinates[0]); + console.log(JSON.stringify(tmpCoordinates)); + geometries[currentGeometry]["geom"]=new ol.geom.Polygon(); + console.log(JSON.stringify(tmpCoordinates)); + geometries[currentGeometry]["geom"].appendLinearRing(new ol.geom.LinearRing(tmpCoordinates)); + console.log(JSON.stringify(tmpCoordinates)); + } + } + + } + console.log(JSON.stringify(tmp0)); + console.log(JSON.stringify(tmp)); + try{ + console.log(JSON.stringify(geometries[currentGeometry]["geom"].getCoordinates())); + var feature=new ol.Feature({geometry: geometries[currentGeometry]["geom"],"name":"myFeature"}); + vectorLayer1.getSource().clear(); + vectorLayer1.getSource().addFeatures([feature]); + var wkt=new ol.format.WKT(); + var tmpGeometry=geometries[currentGeometry]["geom"].clone().transform('EPSG:3857', 'EPSG:4326'); + console.log(wkt.writeGeometry(tmpGeometry)); + $("input[name='"+currentGeometryField+"']").val(wkt.writeGeometry(tmpGeometry)); + }catch(e){ + console.log(e); + } + console.log(JSON.stringify(tmp0)); + console.log(JSON.stringify(tmp)); + if(!stopTracking) + setTimeout(function() { trackCurrentGPSPosition();}, 1000); + console.log(JSON.stringify(tmp0)); + console.log(JSON.stringify(tmp)); +} + +var modalCallback=null; + +function trackStepCurrentGPSPosition(){ + console.log("## trackStepCurrentGPSPosition"); + updateCurrentMapLocation(); + $("#currentPosition").html("Position: "+position[bestIndex].lon.toFixed(6)+","+position[bestIndex].lat.toFixed(6)+""); + if(!stopTracking) + setTimeout(function() { trackStepCurrentGPSPosition();}, 1000); +} + +function addCurrentLocation(){ + console.log("addCurrentLocation!!!!"); + if(geometries["origin"]==null) + geometries["origin"]=new ol.geom.Point(ol.proj.transform([position[bestIndex].lon,position[bestIndex].lat], 'EPSG:4326','EPSG:3857')); + else{ + var tmp0=geometries["origin"].getCoordinates(); + if(geometries[currentGeometry]["geom"]!=null){ + var tmp1=geometries[currentGeometry]["geom"].getCoordinates(); + tmp0=tmp1[tmp1.length-1]; + } + tmp=ol.proj.transform([position[bestIndex].lon,position[bestIndex].lat], 'EPSG:4326','EPSG:3857'); + console.log(JSON.stringify(tmp0)); + console.log(JSON.stringify(tmp)); + $("#currentPosition").html("Position: "+position[bestIndex].lon.toFixed(6)+","+position[bestIndex].lat.toFixed(6)+""); + if(geometries[currentGeometry]["geom"]==null){ + //geometries[currentGeometry]=new geometries[currentGeometry]["constructor"]([tmp0,tmp]); + if(currentGeometry=="line") + geometries[currentGeometry]["geom"]=new ol.geom.LineString([tmp0,tmp]); + else + geometries[currentGeometry]["geom"]=new ol.geom.LineString([tmp0,tmp]); + }else{ + if(currentGeometry=="line"){ + tmpCoordinates=geometries[currentGeometry]["geom"].getCoordinates(); + tmpCoordinates.push(tmp); + geometries[currentGeometry]["geom"]=new ol.geom.LineString(tmpCoordinates); + } + else{ + if(geometries[currentGeometry]["cline"]==null) + tmpCoordinates=geometries[currentGeometry]["geom"].getCoordinates(); + else + tmpCoordinates=geometries[currentGeometry]["cline"].getCoordinates(); + console.log(JSON.stringify(tmpCoordinates)); + tmpCoordinates.push(tmp); + console.log(JSON.stringify(tmpCoordinates)); + if(tmpCoordinates.length>2){ + geometries[currentGeometry]["cline"]=new ol.geom.LineString(tmpCoordinates); + tmpCoordinates.push(tmpCoordinates[0]); + console.log(JSON.stringify(tmpCoordinates)); + geometries[currentGeometry]["geom"]=new ol.geom.Polygon(); + console.log(JSON.stringify(tmpCoordinates)); + geometries[currentGeometry]["geom"].appendLinearRing(new ol.geom.LinearRing(tmpCoordinates)); + console.log(JSON.stringify(tmpCoordinates)); + } + } + + } + try{ + console.log(JSON.stringify(geometries[currentGeometry]["geom"].getCoordinates())); + var feature=new ol.Feature({geometry: geometries[currentGeometry]["geom"],"name":"myFeature"}); + vectorLayer1.getSource().clear(); + vectorLayer1.getSource().addFeatures([feature]); + var wkt=new ol.format.WKT(); + var tmpGeometry=geometries[currentGeometry]["geom"].clone().transform('EPSG:3857', 'EPSG:4326'); + console.log(wkt.writeGeometry(tmpGeometry)); + $("input[name='"+currentGeometryField+"']").val(wkt.writeGeometry(tmpGeometry)); + }catch(e){ + console.log(e); + } + + } +} + +function trackStepByStepPosition(elem,ltype){ + modalCallback=function(){ + //$("#myModal").find(".glyphicon-ok").parent().show(); + if(myVectorLayer) + myVectorLayer.getSource().clear(); + geometries["origin"]=null; + setTimeout(function() { trackStepCurrentGPSPosition();}, 1000); + $("#myModal").find("h4").html(' '+window.Android.translate("gps_track")); + $("#myModal").find(".modal-footer").html( + ''+ + ''+ + '' + ); + }; + currentGeometry=ltype; + currentGeometryField=elem; + geometries[ltype]["geom"]=null; + geometries[ltype]["cline"]=null; + if(!$("#map").length) + $.ajax({ + method: "GET", + url: './map-modal.html', + error: function(){ + }, + success: function(data){ + console.log(data); + $("body").append(data); + $("#map").css("height",($(window).height()-220)+"px"); + $('#myModal').modal(); + addOptionalLocalTiles(true); + $('#myModal').on('shown.bs.modal', function () { + console.log($("#mmm4me_ls").length); + initMapToLocation(); + localTileIndex=map.getLayers().getLength(); + /*geometries["origin"]=new ol.geom.Point(ol.proj.transform([position[bestIndex].lon,position[bestIndex].lat], 'EPSG:4326','EPSG:3857')); + setTimeout(function() { trackCurrentGPSPosition();}, 1000);*/ + modalCallback(); + }); + $('#myModal').on('hide.bs.modal', function () { + console.log("HIDE"); + stopTracking=true; + }); + } + }); + else{ + stopTracking=false; + $('#myModal').modal(); + vectorLayer1.getSource().clear(); + updateCurrentMapLocation(); + geometries["origin"]=null; + setTimeout(function() { trackStepCurrentGPSPosition();}, 1000); + } +} +/***************************************************************************** + * Start tracking GPS location + *****************************************************************************/ +function trackGPSPosition(elem,ltype){ + modalCallback=function(){ + //$("#myModal").find(".glyphicon-ok").parent().show(); + if(myVectorLayer) + myVectorLayer.getSource().clear(); + geometries["origin"]=new ol.geom.Point(ol.proj.transform([position[bestIndex].lon,position[bestIndex].lat], 'EPSG:4326','EPSG:3857')); + setTimeout(function() { trackCurrentGPSPosition();}, 1000); + $("#myModal").find("h4").html(' '+window.Android.translate("gps_track")); + $("#myModal").find(".modal-footer").html( + ''+ + '' + ); + }; + currentGeometry=ltype; + currentGeometryField=elem; + geometries[ltype]["geom"]=null; + geometries[ltype]["cline"]=null; + if(!$("#map").length) + $.ajax({ + method: "GET", + url: './map-modal.html', + error: function(){ + }, + success: function(data){ + console.log(data); + $("body").append(data); + $("#map").css("height",($(window).height()-220)+"px"); + $('#myModal').modal(); + addOptionalLocalTiles(true); + $('#myModal').on('shown.bs.modal', function () { + console.log($("#mmm4me_ls").length); + initMapToLocation(); + localTileIndex=map.getLayers().getLength(); + /*geometries["origin"]=new ol.geom.Point(ol.proj.transform([position[bestIndex].lon,position[bestIndex].lat], 'EPSG:4326','EPSG:3857')); + setTimeout(function() { trackCurrentGPSPosition();}, 1000);*/ + modalCallback(); + }); + $('#myModal').on('hide.bs.modal', function () { + console.log("HIDE"); + stopTracking=true; + }); + } + }); + else{ + stopTracking=false; + $('#myModal').modal(); + vectorLayer1.getSource().clear(); + updateCurrentMapLocation(); + geometries["origin"]=new ol.geom.Point(ol.proj.transform([position[bestIndex].lon,position[bestIndex].lat], 'EPSG:4326','EPSG:3857')); + setTimeout(function() { trackCurrentGPSPosition();}, 1000); + } +} + +var hasAddedElement=0; +var myVectorLayer=null; +function addSelectedElement(wktString){ + var features=[]; + try{ + var format = new ol.format.WKT(); + features.push( + format.readFeature(wktString, { + dataProjection: 'EPSG:4326', + featureProjection: 'EPSG:3857' + }) + ); + }catch(e){ + console.log("Unable to parse WKT ?!"+e) + } + if(!vectorLayer1){ + myVectorLayer = new ol.layer.Vector({ + source: new ol.source.Vector({ + features: features + }), + style: new ol.style.Style({ + stroke: new ol.style.Stroke({ + color: "#3333aa", + width: 1.4 + }) + }) + }); + map.addLayer(myVectorLayer); + } + else{ + vectorLayer1.getSource().clear(); + vectorLayer1.getSource().addFeatures(features); + } + console.log(vectorLayer1.getSource().getExtent()); + map.updateSize(); + map.getView().fit(vectorLayer1.getSource().getExtent(),map.getSize()); + hasAddedElement=1; +} + +/***************************************************************************** + * Show selected feature on map + *****************************************************************************/ + var addSelectedIndex=0; + var mySelectedElement=null; +function showElementOnMap(wktString){ + mySelectedElement=wktString; + modalCallback=function(){ + //$("#myModal").find(".glyphicon-ok").parent().hide(); + if(addSelectedIndex==0){ + updateCurrentMapLocation(); + $("#currentPosition").html("Position: "+position[bestIndex].lon.toFixed(6)+","+position[bestIndex].lat.toFixed(6)+""); + } + addSelectedElement(mySelectedElement); + $("#myModal").find("h4").html(' '+window.Android.translate("view_feature")); + addSelectedIndex++; + }; + //$("#currentPosition").html("Position: "+position[bestIndex].lon.toFixed(6)+","+position[bestIndex].lat.toFixed(6)+""); + if(!$("#map").length) + $.ajax({ + method: "GET", + url: './map-modal.html', + error: function(){ + }, + success: function(data){ + console.log(data); + $("body").append(data); + $("#map").css("height",($(window).height()-220)+"px"); + $('#myModal').modal(); + addOptionalLocalTiles(true); + $('#myModal').on('shown.bs.modal', function () { + console.log($("#mmm4me_ls").length); + initMapToLocation(); + localTileIndex=map.getLayers().getLength(); + modalCallback(); + }); + $('#myModal').on('hide.bs.modal', function () { + console.log("HIDE"); + stopTracking=true; + }); + } + }); + else{ + console.log("OK "); + $('#myModal').modal(); + console.log("OK "); + vectorLayer1.getSource().clear(); + console.log("OK "); + updateCurrentMapLocation(); + console.log("OK "); + addSelectedElement(wktString); + console.log("OK "); + } + + $("#currentPosition").html("Position: "+position[bestIndex].lon.toFixed(6)+","+position[bestIndex].lat.toFixed(6)+""); +} + + +/***************************************************************************** + * Store the current GPS location in the edit form + *****************************************************************************/ +function requireGPSPosition(elem){ + var position=JSON.parse(window.Android.getGPS()); + //elem=$("#"+elem); + if(position.lat){ + $(".btn_"+elem+"_lat").html(position.lat); + $(".btn_"+elem+"_long").html(position.lon); + $(".btn_"+elem+"_source").html(position.source); + $("input[name='"+elem+"']").val("POINT("+position.lon+" "+position.lat+")"); + }else{ + + } + console.log(JSON.stringify(position)); +} + +/***************************************************************************** + * Ajax setup to ensure seting "withCredentials" to true + *****************************************************************************/ +function ajaxSetup(){ + $.ajaxSetup({ + xhrFields: { + withCredentials: true + } + }); +} + +/***************************************************************************** + * Get the current Network and GPS availability + *****************************************************************************/ +function getCurrentStatus(){ + var tmp=JSON.parse(window.Android.getGNStatus()); + tmp["gps"]=JSON.parse(tmp["gps"]); + updateStatus(tmp['gps'],tmp['net']); +} + +/***************************************************************************** + * Display the status icons + *****************************************************************************/ +function addStatusControl(){ + $('.breadcrumb').append(' /'+ + ''+ + ''+ + ''+ + ''+ + ''); +} + +/***************************************************************************** + * Initialize the map and show the current GPS location + *****************************************************************************/ +var localTiles,localTiles0; +var tileLayers=[]; +function initMapToLocation(){ + if(map) + return; + var osmSource = new ol.source.OSM(); + + var otherLayers=[]; + var otherLayersSwitcher=""; + try{ + var BasesLayersStr=window.Android.getBaseLayers(); + console.log(BasesLayersStr); + var BasesLayers=JSON.parse(BasesLayersStr); + for(var i=0;i=4) + map.getLayers().item(otherLayers[i]["index"]).setVisible(false); + }else{ + $(this).parent().find("i").removeClass("glyphicon-eye-close").addClass("glyphicon-eye-open"); + $("#dmopacity_"+i).show(); + console.log(JSON.stringify(map.getLayers().getLength())); + if(!otherLayers[i]["index"]){ + var myTileLayer=new ol.layer.Tile({source: otherLayers[i]["olLayer"]}); + var layers = map.getLayers(); + layers.insertAt(tileLayers.length+1,myTileLayer); + otherLayers[i]["index"]=tileLayers.length+1; + tileLayers.push(myTileLayer); + if(localTileIndex>0) + localTileIndex+=1; + } + else + map.getLayers().item(otherLayers[i]["index"]).setVisible(true); + } + }); + $(".map").parent().find("input[type=range]").last().on('change',function(){ + console.log("change to "+$(this).val()); + map.getLayers().item(otherLayers[i]["index"]).setOpacity($(this).val()/100); + }); + + })(i); + $(".map").parent().find("input[type=checkbox]").parent().off('click'); + $(".map").parent().find("input[type=checkbox]").parent().on('click',function(){ + var tmp=$(this).find("input[type=checkbox]"); + if(tmp.is(":checked")){ + $(this).find("input[type=checkbox]").prop("checked",false).change(); + $(this).addClass("select"); + } + else{ + $(this).find("input[type=checkbox]").prop("checked",true).change(); + $(this).removeClass("select"); + } + }); + + },1); + })(i) + } + } + map = new ol.Map({ + layers: layers, + target: 'map', + controls: ol.control.defaults({ + attributionOptions: /** @type {olx.control.AttributionOptions} */ ({ + collapsible: true + }) + }), + view: new ol.View({ + center: ol.proj.transform([0,0], 'EPSG:4326', 'EPSG:3857'), + zoom: 14, + maxZoom: 22, + minZoom: 1 + }) + }); + + var bstyle = function (feature, resolution) { + + /*var iconFont = 'glyphicon'; + var iconFontText = '\e062';*/ + //var iconFont = 'glyphicon'; + var iconFont = 'Glyphicons Halflings'; + var iconFontText = '\ue062'; + var iconSize = 24; + var opacity=0.4; + var col = 'rgba(0,255,0,0.6)'; + if(feature.get("source")=="GPS") + col = 'rgba(41,136,54,0.5)';//#298836 + else if(feature.get("source")=="Network"){ + col = 'rgba(91,176,75,0.4)';//#5bb04b + iconSize = 32; + opacity=0.2; + } + else if(feature.get("source")=="other"){ + col='rgba(129,208,113,0.5)';//#81d071 + iconSize = 36; + opacity=0.2; + } + else + col='rgba(166,63,39,0.5)'; //#a63f27 //"#EE0000"; + var styles = []; + + var styleIcon = new ol.style.Style({ + text: new ol.style.Text({ + font: 'Normal ' + iconSize + 'px ' + iconFont, + text: iconFontText, + fill: new ol.style.Fill({ color: col }) + }) + }); + styles.push(styleIcon); + + //console.log(feature.get("type")); + return styles; + /*return function (feature, resolution) { + styles.styleIcon.getText().setText(feature.get("iconCode")); + return styles; + };*/ + }; + position=JSON.parse(window.Android.getFullGPS()); + //console.log(JSON.stringify(position)); + if(position.length==0){ + position=[{lon:3.5,lat:43.5,source:"none"}]; + } + var iconFeatures = []; + for(var i=0;i=4) + map.getLayers().item(localTileIndex).setVisible(false); + }else{ + $(this).parent().find("i").removeClass("glyphicon-eye-close").addClass("glyphicon-eye-open"); + $("#dmopacity").show(); + console.log(JSON.stringify(map.getLayers().getLength())); + if(map.getLayers().getLength()0.05){ + map.getView().setRotation(-direction); + oldBearer=direction; + } + }else{ + window.Android.stopReportDirection(); + } +} \ No newline at end of file diff --git a/app/src/main/java/Satellite/DatabaseHandler.java b/app/src/main/java/Satellite/DatabaseHandler.java new file mode 100644 index 0000000..fb05987 --- /dev/null +++ b/app/src/main/java/Satellite/DatabaseHandler.java @@ -0,0 +1,1087 @@ + +package Satellite; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.location.Location; +import android.util.Log; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.TimeZone; + + +class DatabaseHandler extends SQLiteOpenHelper { + + // All Static variables + // Database Version + private static final int DATABASE_VERSION = 2; // Updated to 2 in v2.1.3 (code 14) + private static final int LOCATION_TYPE_LOCATION = 1; + private static final int LOCATION_TYPE_PLACEMARK = 2; + + // Database Name + private static final String DATABASE_NAME = "GPS"; + + // -------------------------------------------------------------------------------- Table names + private static final String TABLE_LOCATIONS = "locations"; + private static final String TABLE_TRACKS = "tracks"; + private static final String TABLE_PLACEMARKS = "placemarks"; + + // ----------------------------------------------------------------------- Common Columns names + private static final String KEY_ID = "id"; + private static final String KEY_TRACK_ID = "track_id"; + + // --------------------------------------------------------------- Location Table Columns names + private static final String KEY_LOCATION_NUMBER = "nr"; + private static final String KEY_LOCATION_LATITUDE = "latitude"; + private static final String KEY_LOCATION_LONGITUDE = "longitude"; + private static final String KEY_LOCATION_ALTITUDE = "altitude"; + private static final String KEY_LOCATION_SPEED = "speed"; + private static final String KEY_LOCATION_ACCURACY = "accuracy"; + private static final String KEY_LOCATION_BEARING = "bearing"; + private static final String KEY_LOCATION_TIME = "time"; + private static final String KEY_LOCATION_NUMBEROFSATELLITES = "number_of_satellites"; + private static final String KEY_LOCATION_TYPE = "type"; + private static final String KEY_LOCATION_NUMBEROFSATELLITESUSEDINFIX = "number_of_satellites_used_in_fix"; + + // ---------------------------------------------------------------------------- Placemarks adds + private static final String KEY_LOCATION_NAME = "name"; + + // ------------------------------------------------------------------ Track Table Columns names + private static final String KEY_TRACK_NAME = "name"; + private static final String KEY_TRACK_FROM = "location_from"; + private static final String KEY_TRACK_TO = "location_to"; + + + private static final String KEY_TRACK_START_LATITUDE = "start_latitude"; + private static final String KEY_TRACK_START_LONGITUDE = "start_longitude"; + private static final String KEY_TRACK_START_ALTITUDE = "start_altitude"; + private static final String KEY_TRACK_START_ACCURACY = "start_accuracy"; + private static final String KEY_TRACK_START_SPEED = "start_speed"; + private static final String KEY_TRACK_START_TIME = "start_time"; + + private static final String KEY_TRACK_LASTFIX_TIME = "lastfix_time"; + + private static final String KEY_TRACK_END_LATITUDE = "end_latitude"; + private static final String KEY_TRACK_END_LONGITUDE = "end_longitude"; + private static final String KEY_TRACK_END_ALTITUDE = "end_altitude"; + private static final String KEY_TRACK_END_ACCURACY = "end_accuracy"; + private static final String KEY_TRACK_END_SPEED = "end_speed"; + private static final String KEY_TRACK_END_TIME = "end_time"; + + private static final String KEY_TRACK_LASTSTEPDST_LATITUDE = "laststepdst_latitude"; + private static final String KEY_TRACK_LASTSTEPDST_LONGITUDE = "laststepdst_longitude"; + private static final String KEY_TRACK_LASTSTEPDST_ACCURACY = "laststepdst_accuracy"; + + private static final String KEY_TRACK_LASTSTEPALT_ALTITUDE = "laststepalt_altitude"; + private static final String KEY_TRACK_LASTSTEPALT_ACCURACY = "laststepalt_accuracy"; + + private static final String KEY_TRACK_MIN_LATITUDE = "min_latitude"; + private static final String KEY_TRACK_MIN_LONGITUDE = "min_longitude"; + + private static final String KEY_TRACK_MAX_LATITUDE = "max_latitude"; + private static final String KEY_TRACK_MAX_LONGITUDE = "max_longitude"; + + private static final String KEY_TRACK_DURATION = "duration"; + private static final String KEY_TRACK_DURATION_MOVING = "duration_moving"; + + private static final String KEY_TRACK_DISTANCE = "distance"; + private static final String KEY_TRACK_DISTANCE_INPROGRESS = "distance_in_progress"; + private static final String KEY_TRACK_DISTANCE_LASTALTITUDE = "distance_last_altitude"; + + private static final String KEY_TRACK_ALTITUDE_UP = "altitude_up"; + private static final String KEY_TRACK_ALTITUDE_DOWN = "altitude_down"; + private static final String KEY_TRACK_ALTITUDE_INPROGRESS = "altitude_in_progress"; + + private static final String KEY_TRACK_SPEED_MAX = "speed_max"; + private static final String KEY_TRACK_SPEED_AVERAGE = "speed_average"; + private static final String KEY_TRACK_SPEED_AVERAGEMOVING = "speed_average_moving"; + + private static final String KEY_TRACK_NUMBEROFLOCATIONS = "number_of_locations"; + private static final String KEY_TRACK_NUMBEROFPLACEMARKS = "number_of_placemarks"; + private static final String KEY_TRACK_TYPE = "type"; + + private static final String KEY_TRACK_VALIDMAP = "validmap"; + + + + public DatabaseHandler(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + // Creating Tables + @Override + public void onCreate(SQLiteDatabase db) { + String CREATE_TRACKS_TABLE = "CREATE TABLE " + TABLE_TRACKS + "(" + + KEY_ID + " INTEGER PRIMARY KEY AUTOINCREMENT," // 0 + + KEY_TRACK_NAME + " TEXT," // 1 + + KEY_TRACK_FROM + " TEXT," // 2 + + KEY_TRACK_TO + " TEXT," // 3 + + KEY_TRACK_START_LATITUDE + " REAL," // 4 + + KEY_TRACK_START_LONGITUDE + " REAL," // 5 + + KEY_TRACK_START_ALTITUDE + " REAL," // 6 + + KEY_TRACK_START_ACCURACY + " REAL," // 7 + + KEY_TRACK_START_SPEED + " REAL," // 8 + + KEY_TRACK_START_TIME + " REAL," // 9 + + KEY_TRACK_LASTFIX_TIME + " REAL," // 10 + + KEY_TRACK_END_LATITUDE + " REAL," // 11 + + KEY_TRACK_END_LONGITUDE + " REAL," // 12 + + KEY_TRACK_END_ALTITUDE + " REAL," // 13 + + KEY_TRACK_END_ACCURACY + " REAL," // 14 + + KEY_TRACK_END_SPEED + " REAL," // 15 + + KEY_TRACK_END_TIME + " REAL," // 16 + + KEY_TRACK_LASTSTEPDST_LATITUDE + " REAL," // 17 + + KEY_TRACK_LASTSTEPDST_LONGITUDE + " REAL," // 18 + + KEY_TRACK_LASTSTEPDST_ACCURACY + " REAL," // 19 + + KEY_TRACK_LASTSTEPALT_ALTITUDE + " REAL," // 20 + + KEY_TRACK_LASTSTEPALT_ACCURACY + " REAL," // 21 + + KEY_TRACK_MIN_LATITUDE + " REAL," // 22 + + KEY_TRACK_MIN_LONGITUDE + " REAL," // 23 + + KEY_TRACK_MAX_LATITUDE + " REAL," // 24 + + KEY_TRACK_MAX_LONGITUDE + " REAL," // 25 + + KEY_TRACK_DURATION + " REAL," // 26 + + KEY_TRACK_DURATION_MOVING + " REAL," // 27 + + KEY_TRACK_DISTANCE + " REAL," // 28 + + KEY_TRACK_DISTANCE_INPROGRESS + " REAL," // 29 + + KEY_TRACK_DISTANCE_LASTALTITUDE + " REAL," // 30 + + KEY_TRACK_ALTITUDE_UP + " REAL," // 31 + + KEY_TRACK_ALTITUDE_DOWN + " REAL," // 32 + + KEY_TRACK_ALTITUDE_INPROGRESS + " REAL," // 33 + + KEY_TRACK_SPEED_MAX + " REAL," // 34 + + KEY_TRACK_SPEED_AVERAGE + " REAL," // 35 + + KEY_TRACK_SPEED_AVERAGEMOVING + " REAL," // 36 + + KEY_TRACK_NUMBEROFLOCATIONS + " INTEGER," // 37 + + KEY_TRACK_NUMBEROFPLACEMARKS + " INTEGER," // 38 + + KEY_TRACK_VALIDMAP + " INTEGER," // 39 + + KEY_TRACK_TYPE + " INTEGER " + ")"; // 40 + db.execSQL(CREATE_TRACKS_TABLE); + + String CREATE_LOCATIONS_TABLE = "CREATE TABLE " + TABLE_LOCATIONS + "(" + + KEY_ID + " INTEGER PRIMARY KEY AUTOINCREMENT," // 0 + + KEY_TRACK_ID + " INTEGER," // 1 + + KEY_LOCATION_NUMBER + " INTEGER," // 2 + + KEY_LOCATION_LATITUDE + " REAL," // 3 + + KEY_LOCATION_LONGITUDE + " REAL," // 4 + + KEY_LOCATION_ALTITUDE + " REAL," // 5 + + KEY_LOCATION_SPEED + " REAL," // 6 + + KEY_LOCATION_ACCURACY + " REAL," // 7 + + KEY_LOCATION_BEARING + " REAL," // 8 + + KEY_LOCATION_TIME + " REAL," // 9 + + KEY_LOCATION_NUMBEROFSATELLITES + " INTEGER," // 10 + + KEY_LOCATION_TYPE + " INTEGER," // 11 + + KEY_LOCATION_NUMBEROFSATELLITESUSEDINFIX + " INTEGER" + ")"; // 12 + db.execSQL(CREATE_LOCATIONS_TABLE); + + String CREATE_PLACEMARKS_TABLE = "CREATE TABLE " + TABLE_PLACEMARKS + "(" + + KEY_ID + " INTEGER PRIMARY KEY AUTOINCREMENT," // 0 + + KEY_TRACK_ID + " INTEGER," // 1 + + KEY_LOCATION_NUMBER + " INTEGER," // 2 + + KEY_LOCATION_LATITUDE + " REAL," // 3 + + KEY_LOCATION_LONGITUDE + " REAL," // 4 + + KEY_LOCATION_ALTITUDE + " REAL," // 5 + + KEY_LOCATION_SPEED + " REAL," // 6 + + KEY_LOCATION_ACCURACY + " REAL," // 7 + + KEY_LOCATION_BEARING + " REAL," // 8 + + KEY_LOCATION_TIME + " REAL," // 9 + + KEY_LOCATION_NUMBEROFSATELLITES + " INTEGER," // 10 + + KEY_LOCATION_TYPE + " INTEGER," // 11 + + KEY_LOCATION_NAME + " TEXT," // 12 + + KEY_LOCATION_NUMBEROFSATELLITESUSEDINFIX + " INTEGER" + ")"; // 13 + db.execSQL(CREATE_PLACEMARKS_TABLE); + } + + + private static final int NOT_AVAILABLE = -100000; + + private static final String DATABASE_ALTER_TABLE_LOCATIONS_TO_V2 = "ALTER TABLE " + + TABLE_LOCATIONS + " ADD COLUMN " + KEY_LOCATION_NUMBEROFSATELLITESUSEDINFIX + " INTEGER DEFAULT " + NOT_AVAILABLE + ";"; + private static final String DATABASE_ALTER_TABLE_PLACEMARKS_TO_V2 = "ALTER TABLE " + + TABLE_PLACEMARKS + " ADD COLUMN " + KEY_LOCATION_NUMBEROFSATELLITESUSEDINFIX + " INTEGER DEFAULT " + NOT_AVAILABLE + ";"; + + // Upgrading database + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + + // Use this function in case of DB version upgrade. + + // Drop older table if existed + //db.execSQL("DROP TABLE IF EXISTS " + TABLE_LOCATIONS); + //db.execSQL("DROP TABLE IF EXISTS " + TABLE_PLACEMARKS); + //db.execSQL("DROP TABLE IF EXISTS " + TABLE_TRACKS); + + // Create tables again + //onCreate(db); + + switch (oldVersion) + { + case 1: + //upgrade from version 1 to 2 + //Log.w("myApp", "[#] DatabaseHandler.java - onUpgrade: from version 1 to 2 ..."); + db.execSQL(DATABASE_ALTER_TABLE_LOCATIONS_TO_V2); + db.execSQL(DATABASE_ALTER_TABLE_PLACEMARKS_TO_V2); + //case 2: + //upgrade from version 2 to 3 + // db.execSQL(DATABASE_ALTER_TEAM_TO_V3); + + //and so on.. do not add breaks so that switch will + //start at oldVersion, and run straight through to the latest + } + //Log.w("myApp", "[#] DatabaseHandler.java - onUpgrade: DB upgraded to version " + newVersion); + } + +// ----------------------------------------------------------------------- LOCATIONS AND PLACEMARKS + + // Add new Location and update the corresponding track + public void addLocationToTrack(LocationExtended location, Track track) { + SQLiteDatabase db = this.getWritableDatabase(); + + Location loc = location.getLocation(); + + ContentValues locvalues = new ContentValues(); + locvalues.put(KEY_TRACK_ID, track.getId()); + locvalues.put(KEY_LOCATION_NUMBER, track.getNumberOfLocations()); + locvalues.put(KEY_LOCATION_LATITUDE, loc.getLatitude()); + locvalues.put(KEY_LOCATION_LONGITUDE, loc.getLongitude()); + locvalues.put(KEY_LOCATION_ALTITUDE, loc.hasAltitude() ? loc.getAltitude() : NOT_AVAILABLE); + locvalues.put(KEY_LOCATION_SPEED, loc.hasSpeed() ? loc.getSpeed() : NOT_AVAILABLE); + locvalues.put(KEY_LOCATION_ACCURACY, loc.hasAccuracy() ? loc.getAccuracy() : NOT_AVAILABLE); + locvalues.put(KEY_LOCATION_BEARING, loc.hasBearing() ? loc.getBearing() : NOT_AVAILABLE); + locvalues.put(KEY_LOCATION_TIME, loc.getTime()); + locvalues.put(KEY_LOCATION_NUMBEROFSATELLITES, location.getNumberOfSatellites()); + locvalues.put(KEY_LOCATION_TYPE, LOCATION_TYPE_LOCATION); + locvalues.put(KEY_LOCATION_NUMBEROFSATELLITESUSEDINFIX, location.getNumberOfSatellitesUsedInFix()); + + ContentValues trkvalues = new ContentValues(); + trkvalues.put(KEY_TRACK_NAME, track.getName()); + trkvalues.put(KEY_TRACK_FROM, ""); + trkvalues.put(KEY_TRACK_TO, ""); + + trkvalues.put(KEY_TRACK_START_LATITUDE, track.getStart_Latitude()); + trkvalues.put(KEY_TRACK_START_LONGITUDE, track.getStart_Longitude()); + trkvalues.put(KEY_TRACK_START_ALTITUDE, track.getStart_Altitude()); + trkvalues.put(KEY_TRACK_START_ACCURACY, track.getStart_Accuracy()); + trkvalues.put(KEY_TRACK_START_SPEED, track.getStart_Speed()); + trkvalues.put(KEY_TRACK_START_TIME, track.getStart_Time()); + + trkvalues.put(KEY_TRACK_LASTFIX_TIME, track.getLastFix_Time()); + + trkvalues.put(KEY_TRACK_END_LATITUDE, track.getEnd_Latitude()); + trkvalues.put(KEY_TRACK_END_LONGITUDE, track.getEnd_Longitude()); + trkvalues.put(KEY_TRACK_END_ALTITUDE, track.getEnd_Altitude()); + trkvalues.put(KEY_TRACK_END_ACCURACY, track.getEnd_Accuracy()); + trkvalues.put(KEY_TRACK_END_SPEED, track.getEnd_Speed()); + trkvalues.put(KEY_TRACK_END_TIME, track.getEnd_Time()); + + trkvalues.put(KEY_TRACK_LASTSTEPDST_LATITUDE, track.getLastStepDistance_Latitude()); + trkvalues.put(KEY_TRACK_LASTSTEPDST_LONGITUDE, track.getLastStepDistance_Longitude()); + trkvalues.put(KEY_TRACK_LASTSTEPDST_ACCURACY, track.getLastStepDistance_Accuracy()); + + trkvalues.put(KEY_TRACK_LASTSTEPALT_ALTITUDE, track.getLastStepAltitude_Altitude()); + trkvalues.put(KEY_TRACK_LASTSTEPALT_ACCURACY, track.getLastStepAltitude_Accuracy()); + + trkvalues.put(KEY_TRACK_MIN_LATITUDE, track.getMin_Latitude()); + trkvalues.put(KEY_TRACK_MIN_LONGITUDE, track.getMin_Longitude()); + + trkvalues.put(KEY_TRACK_MAX_LATITUDE, track.getMax_Latitude()); + trkvalues.put(KEY_TRACK_MAX_LONGITUDE, track.getMax_Longitude()); + + trkvalues.put(KEY_TRACK_DURATION, track.getDuration()); + trkvalues.put(KEY_TRACK_DURATION_MOVING, track.getDuration_Moving()); + + trkvalues.put(KEY_TRACK_DISTANCE, track.getDistance()); + trkvalues.put(KEY_TRACK_DISTANCE_INPROGRESS, track.getDistanceInProgress()); + trkvalues.put(KEY_TRACK_DISTANCE_LASTALTITUDE, track.getDistanceLastAltitude()); + + trkvalues.put(KEY_TRACK_ALTITUDE_UP, track.getAltitude_Up()); + trkvalues.put(KEY_TRACK_ALTITUDE_DOWN, track.getAltitude_Down()); + trkvalues.put(KEY_TRACK_ALTITUDE_INPROGRESS, track.getAltitude_InProgress()); + + trkvalues.put(KEY_TRACK_SPEED_MAX, track.getSpeedMax()); + trkvalues.put(KEY_TRACK_SPEED_AVERAGE, track.getSpeedAverage()); + trkvalues.put(KEY_TRACK_SPEED_AVERAGEMOVING, track.getSpeedAverageMoving()); + + trkvalues.put(KEY_TRACK_NUMBEROFLOCATIONS, track.getNumberOfLocations()); + trkvalues.put(KEY_TRACK_NUMBEROFPLACEMARKS, track.getNumberOfPlacemarks()); + trkvalues.put(KEY_TRACK_TYPE, track.getType()); + + trkvalues.put(KEY_TRACK_VALIDMAP, track.getValidMap()); + + db.beginTransaction(); + db.insert(TABLE_LOCATIONS, null, locvalues); // Insert the new Location + db.update(TABLE_TRACKS, trkvalues, KEY_ID + " = ?", + new String[] { String.valueOf(track.getId()) }); // Update the corresponding Track + db.setTransactionSuccessful(); + db.endTransaction(); + + //Log.w("myApp", "[#] DatabaseHandler.java - addLocation: Location " + track.getNumberOfLocations() + " added into track " + track.getID()); + } + + + // Add new Placemark and update the corresponding track + public void addPlacemarkToTrack(LocationExtended placemark, Track track) { + SQLiteDatabase db = this.getWritableDatabase(); + + Location loc = placemark.getLocation(); + + ContentValues locvalues = new ContentValues(); + locvalues.put(KEY_TRACK_ID, track.getId()); + locvalues.put(KEY_LOCATION_NUMBER, track.getNumberOfPlacemarks()); + locvalues.put(KEY_LOCATION_LATITUDE, loc.getLatitude()); + locvalues.put(KEY_LOCATION_LONGITUDE, loc.getLongitude()); + locvalues.put(KEY_LOCATION_ALTITUDE, loc.hasAltitude() ? loc.getAltitude() : NOT_AVAILABLE); + locvalues.put(KEY_LOCATION_SPEED, loc.hasSpeed() ? loc.getSpeed() : NOT_AVAILABLE); + locvalues.put(KEY_LOCATION_ACCURACY, loc.hasAccuracy() ? loc.getAccuracy() : NOT_AVAILABLE); + locvalues.put(KEY_LOCATION_BEARING, loc.hasBearing() ? loc.getBearing() : NOT_AVAILABLE); + locvalues.put(KEY_LOCATION_TIME, loc.getTime()); + locvalues.put(KEY_LOCATION_NUMBEROFSATELLITES, placemark.getNumberOfSatellites()); + locvalues.put(KEY_LOCATION_TYPE, LOCATION_TYPE_PLACEMARK); + locvalues.put(KEY_LOCATION_NAME, placemark.getDescription()); + locvalues.put(KEY_LOCATION_NUMBEROFSATELLITESUSEDINFIX, placemark.getNumberOfSatellitesUsedInFix()); + + ContentValues trkvalues = new ContentValues(); + trkvalues.put(KEY_TRACK_NAME, track.getName()); + trkvalues.put(KEY_TRACK_FROM, ""); + trkvalues.put(KEY_TRACK_TO, ""); + + trkvalues.put(KEY_TRACK_START_LATITUDE, track.getStart_Latitude()); + trkvalues.put(KEY_TRACK_START_LONGITUDE, track.getStart_Longitude()); + trkvalues.put(KEY_TRACK_START_ALTITUDE, track.getStart_Altitude()); + trkvalues.put(KEY_TRACK_START_ACCURACY, track.getStart_Accuracy()); + trkvalues.put(KEY_TRACK_START_SPEED, track.getStart_Speed()); + trkvalues.put(KEY_TRACK_START_TIME, track.getStart_Time()); + + trkvalues.put(KEY_TRACK_LASTFIX_TIME, track.getLastFix_Time()); + + trkvalues.put(KEY_TRACK_END_LATITUDE, track.getEnd_Latitude()); + trkvalues.put(KEY_TRACK_END_LONGITUDE, track.getEnd_Longitude()); + trkvalues.put(KEY_TRACK_END_ALTITUDE, track.getEnd_Altitude()); + trkvalues.put(KEY_TRACK_END_ACCURACY, track.getEnd_Accuracy()); + trkvalues.put(KEY_TRACK_END_SPEED, track.getEnd_Speed()); + trkvalues.put(KEY_TRACK_END_TIME, track.getEnd_Time()); + + trkvalues.put(KEY_TRACK_LASTSTEPDST_LATITUDE, track.getLastStepDistance_Latitude()); + trkvalues.put(KEY_TRACK_LASTSTEPDST_LONGITUDE, track.getLastStepDistance_Longitude()); + trkvalues.put(KEY_TRACK_LASTSTEPDST_ACCURACY, track.getLastStepDistance_Accuracy()); + + trkvalues.put(KEY_TRACK_LASTSTEPALT_ALTITUDE, track.getLastStepAltitude_Altitude()); + trkvalues.put(KEY_TRACK_LASTSTEPALT_ACCURACY, track.getLastStepAltitude_Accuracy()); + + trkvalues.put(KEY_TRACK_MIN_LATITUDE, track.getMin_Latitude()); + trkvalues.put(KEY_TRACK_MIN_LONGITUDE, track.getMin_Longitude()); + + trkvalues.put(KEY_TRACK_MAX_LATITUDE, track.getMax_Latitude()); + trkvalues.put(KEY_TRACK_MAX_LONGITUDE, track.getMax_Longitude()); + + trkvalues.put(KEY_TRACK_DURATION, track.getDuration()); + trkvalues.put(KEY_TRACK_DURATION_MOVING, track.getDuration_Moving()); + + trkvalues.put(KEY_TRACK_DISTANCE, track.getDistance()); + trkvalues.put(KEY_TRACK_DISTANCE_INPROGRESS, track.getDistanceInProgress()); + trkvalues.put(KEY_TRACK_DISTANCE_LASTALTITUDE, track.getDistanceLastAltitude()); + + trkvalues.put(KEY_TRACK_ALTITUDE_UP, track.getAltitude_Up()); + trkvalues.put(KEY_TRACK_ALTITUDE_DOWN, track.getAltitude_Down()); + trkvalues.put(KEY_TRACK_ALTITUDE_INPROGRESS, track.getAltitude_InProgress()); + + trkvalues.put(KEY_TRACK_SPEED_MAX, track.getSpeedMax()); + trkvalues.put(KEY_TRACK_SPEED_AVERAGE, track.getSpeedAverage()); + trkvalues.put(KEY_TRACK_SPEED_AVERAGEMOVING, track.getSpeedAverageMoving()); + + trkvalues.put(KEY_TRACK_NUMBEROFLOCATIONS, track.getNumberOfLocations()); + trkvalues.put(KEY_TRACK_NUMBEROFPLACEMARKS, track.getNumberOfPlacemarks()); + trkvalues.put(KEY_TRACK_TYPE, track.getType()); + + trkvalues.put(KEY_TRACK_VALIDMAP, track.getValidMap()); + + db.beginTransaction(); + db.insert(TABLE_PLACEMARKS, null, locvalues); // Insert the new Location + db.update(TABLE_TRACKS, trkvalues, KEY_ID + " = ?", + new String[] { String.valueOf(track.getId()) }); // Update the corresponding Track + db.setTransactionSuccessful(); + db.endTransaction(); + + //Log.w("myApp", "[#] DatabaseHandler.java - addLocation: Location " + track.getNumberOfLocations() + " added into track " + track.getID()); + } + + + // Get single Location + public LocationExtended getLocation(long id) { + SQLiteDatabase db = this.getWritableDatabase(); + LocationExtended extdloc = null; + double lcdata_double; + float lcdata_float; + + Cursor cursor = db.query(TABLE_LOCATIONS, new String[] {KEY_ID, + KEY_LOCATION_LATITUDE, + KEY_LOCATION_LONGITUDE, + KEY_LOCATION_ALTITUDE, + KEY_LOCATION_SPEED, + KEY_LOCATION_ACCURACY, + KEY_LOCATION_BEARING, + KEY_LOCATION_TIME, + KEY_LOCATION_NUMBEROFSATELLITES, + KEY_LOCATION_NUMBEROFSATELLITESUSEDINFIX}, KEY_ID + "=?", + new String[] { String.valueOf(id) }, null, null, null, null); + if (cursor != null) { + cursor.moveToFirst(); + + Location lc = new Location("DB"); + lc.setLatitude(cursor.getDouble(1)); + lc.setLongitude(cursor.getDouble(2)); + + lcdata_double = cursor.getDouble(3); + if (lcdata_double != NOT_AVAILABLE) lc.setAltitude(lcdata_double); + //else lc.removeAltitude(); + + lcdata_float = cursor.getFloat(4); + if (lcdata_float != NOT_AVAILABLE) lc.setSpeed(lcdata_float); + //else lc.removeSpeed(); + + lcdata_float = cursor.getFloat(5); + if (lcdata_float != NOT_AVAILABLE) lc.setAccuracy(lcdata_float); + //else lc.removeAccuracy(); + + lcdata_float = cursor.getFloat(6); + if (lcdata_float != NOT_AVAILABLE) lc.setBearing(lcdata_float); + //else lc.removeBearing(); + + lc.setTime(cursor.getLong(7)); + + + extdloc = new LocationExtended(lc); + extdloc.setNumberOfSatellites(cursor.getInt(8)); + extdloc.setNumberOfSatellitesUsedInFix(cursor.getInt(9)); + + cursor.close(); + } + return extdloc != null ? extdloc : null; + } + + + + // Getting a list of Locations associated to a specified track, with number between startNumber and endNumber + // Please note that limits both are inclusive! + public List getLocationsList(long TrackID, long startNumber, long endNumber) { + + List locationList = new ArrayList<>(); + + String selectQuery = "SELECT * FROM " + TABLE_LOCATIONS + " WHERE " + + KEY_TRACK_ID + " = " + TrackID + " AND " + + KEY_LOCATION_NUMBER + " BETWEEN " + startNumber + " AND " + endNumber + + " ORDER BY " + KEY_LOCATION_NUMBER; + + //Log.w("myApp", "[#] DatabaseHandler.java - getLocationList(" + TrackID + ", " + startNumber + ", " +endNumber + ") ==> " + selectQuery); + + SQLiteDatabase db = this.getWritableDatabase(); + Cursor cursor = db.rawQuery(selectQuery, null); + double lcdata_double; + float lcdata_float; + + if (cursor != null) { + // looping through all rows and adding to list + if (cursor.moveToFirst()) { + do { + Location lc = new Location("DB"); + lc.setLatitude(cursor.getDouble(3)); + lc.setLongitude(cursor.getDouble(4)); + + lcdata_double = cursor.getDouble(5); + if (lcdata_double != NOT_AVAILABLE) lc.setAltitude(lcdata_double); + //else lc.removeAltitude(); + + lcdata_float = cursor.getFloat(6); + if (lcdata_float != NOT_AVAILABLE) lc.setSpeed(lcdata_float); + //else lc.removeSpeed(); + + lcdata_float = cursor.getFloat(7); + if (lcdata_float != NOT_AVAILABLE) lc.setAccuracy(lcdata_float); + //else lc.removeAccuracy(); + + lcdata_float = cursor.getFloat(8); + if (lcdata_float != NOT_AVAILABLE) lc.setBearing(lcdata_float); + //else lc.removeBearing(); + + lc.setTime(cursor.getLong(9)); + + LocationExtended extdloc = new LocationExtended(lc); + extdloc.setNumberOfSatellites(cursor.getInt(10)); + extdloc.setNumberOfSatellitesUsedInFix(cursor.getInt(12)); + + locationList.add(extdloc); // Add Location to list + } while (cursor.moveToNext()); + } + cursor.close(); + } + return locationList; + } + + // Getting a list of Locations associated to a specified track, with number between startNumber and endNumber + // Please note that limits both are inclusive! + public List getPlacemarksList(long TrackID, long startNumber, long endNumber) { + + List placemarkList = new ArrayList<>(); + + String selectQuery = "SELECT * FROM " + TABLE_PLACEMARKS + " WHERE " + + KEY_TRACK_ID + " = " + TrackID + " AND " + + KEY_LOCATION_NUMBER + " BETWEEN " + startNumber + " AND " + endNumber + + " ORDER BY " + KEY_LOCATION_NUMBER; + + //Log.w("myApp", "[#] DatabaseHandler.java - getLocationList(" + TrackID + ", " + startNumber + ", " +endNumber + ") ==> " + selectQuery); + + SQLiteDatabase db = this.getWritableDatabase(); + Cursor cursor = db.rawQuery(selectQuery, null); + double lcdata_double; + float lcdata_float; + + if (cursor != null) { + // looping through all rows and adding to list + if (cursor.moveToFirst()) { + do { + Location lc = new Location("DB"); + lc.setLatitude(cursor.getDouble(3)); + lc.setLongitude(cursor.getDouble(4)); + + lcdata_double = cursor.getDouble(5); + if (lcdata_double != NOT_AVAILABLE) lc.setAltitude(lcdata_double); + //else lc.removeAltitude(); + + lcdata_float = cursor.getFloat(6); + if (lcdata_float != NOT_AVAILABLE) lc.setSpeed(lcdata_float); + //else lc.removeSpeed(); + + lcdata_float = cursor.getFloat(7); + if (lcdata_float != NOT_AVAILABLE) lc.setAccuracy(lcdata_float); + //else lc.removeAccuracy(); + + lcdata_float = cursor.getFloat(8); + if (lcdata_float != NOT_AVAILABLE) lc.setBearing(lcdata_float); + //else lc.removeBearing(); + + lc.setTime(cursor.getLong(9)); + + LocationExtended extdloc = new LocationExtended(lc); + extdloc.setNumberOfSatellites(cursor.getInt(10)); + extdloc.setNumberOfSatellitesUsedInFix(cursor.getInt(13)); + extdloc.setDescription(cursor.getString(12)); + + placemarkList.add(extdloc); // Add Location to list + } while (cursor.moveToNext()); + } + cursor.close(); + } + return placemarkList; + } + + + // Getting a list of Locations associated to a specified track, with number between startNumber and endNumber + // Please note that limits both are inclusive! + public List getLatLngList(long TrackID, long startNumber, long endNumber) { + + List latlngList = new ArrayList<>(); + + String selectQuery = "SELECT " + KEY_TRACK_ID + "," + KEY_LOCATION_LATITUDE + "," + KEY_LOCATION_LONGITUDE + "," + KEY_LOCATION_NUMBER + + " FROM " + TABLE_LOCATIONS + " WHERE " + + KEY_TRACK_ID + " = " + TrackID + " AND " + + KEY_LOCATION_NUMBER + " BETWEEN " + startNumber + " AND " + endNumber + + " ORDER BY " + KEY_LOCATION_NUMBER; + + //Log.w("myApp", "[#] DatabaseHandler.java - getLocationList(" + TrackID + ", " + startNumber + ", " +endNumber + ") ==> " + selectQuery); + + SQLiteDatabase db = this.getWritableDatabase(); + Cursor cursor = db.rawQuery(selectQuery, null); + + if (cursor != null) { + // looping through all rows and adding to list + if (cursor.moveToFirst()) { + do { + LatLng latlng = new LatLng(); + latlng.Latitude = cursor.getDouble(1); + latlng.Longitude = cursor.getDouble(2); + + latlngList.add(latlng); // Add Location to list + } while (cursor.moveToNext()); + } + cursor.close(); + } + return latlngList; + } + + + // Get the total amount of Locations stored in the DB + public long getLocationsTotalCount() { + String countQuery = "SELECT * FROM " + TABLE_LOCATIONS; + SQLiteDatabase db = this.getWritableDatabase(); + Cursor cursor = db.rawQuery(countQuery, null); + long result = cursor.getCount(); + cursor.close(); + return result; + } + + + // Get the number of Locations of a Track + public long getLocationsCount(long TrackID) { + String countQuery = "SELECT * FROM " + TABLE_LOCATIONS + " WHERE " + KEY_TRACK_ID + " = " + TrackID; + SQLiteDatabase db = this.getWritableDatabase(); + Cursor cursor = db.rawQuery(countQuery, null); + long result = cursor.getCount(); + cursor.close(); + return result; + } + + + // Get last Location ID + public long getLastLocationID(long TrackID) { + SQLiteDatabase db = this.getWritableDatabase(); + long result = 0; + + String query = "SELECT " + KEY_ID + " FROM " + TABLE_LOCATIONS + + " WHERE " + KEY_TRACK_ID + " = " + TrackID + " ORDER BY " + KEY_ID + " DESC LIMIT 1"; + + //Log.w("myApp", "[#] DatabaseHandler.java - getLastLocationID(): " + query); + + Cursor cursor = db.rawQuery(query, null); + + if (cursor != null) { + //Log.w("myApp", "[#] !null"); + if (cursor.moveToFirst()) { + result = cursor.getLong(0); + } //else Log.w("myApp", "[#] DatabaseHandler.java - Location table is empty"); + cursor.close(); + } + //Log.w("myApp", "[#] RETURN getLastTrack()"); + return result; + } + + + + +// ----------------------------------------------------------------------------------------- TRACKS + + // Delete the track with the specified ID; + // The method deletes also Placemarks and Locations associated to the specified track + public void DeleteTrack(long TrackID) { + SQLiteDatabase db = this.getWritableDatabase(); + db.beginTransaction(); + db.delete(TABLE_PLACEMARKS, KEY_TRACK_ID + " = ?", + new String[] { String.valueOf(TrackID) }); // Delete track's Placemarks + db.delete(TABLE_LOCATIONS, KEY_TRACK_ID + " = ?", + new String[] { String.valueOf(TrackID) }); // Delete track's Locations + db.delete(TABLE_TRACKS, KEY_ID + " = ?", + new String[] { String.valueOf(TrackID) }); // Delete track + db.setTransactionSuccessful(); + db.endTransaction(); + + //Log.w("myApp", "[#] DatabaseHandler.java - addLocation: Location " + track.getNumberOfLocations() + " added into track " + track.getID()); + } + + + // Add a new Track, returns the TrackID + public long addTrack(Track track) { + + SQLiteDatabase db = this.getWritableDatabase(); + + ContentValues trkvalues = new ContentValues(); + trkvalues.put(KEY_TRACK_NAME, track.getName()); + trkvalues.put(KEY_TRACK_FROM, ""); + trkvalues.put(KEY_TRACK_TO, ""); + + trkvalues.put(KEY_TRACK_START_LATITUDE, track.getStart_Latitude()); + trkvalues.put(KEY_TRACK_START_LONGITUDE, track.getStart_Longitude()); + trkvalues.put(KEY_TRACK_START_ALTITUDE, track.getStart_Altitude()); + trkvalues.put(KEY_TRACK_START_ACCURACY, track.getStart_Accuracy()); + trkvalues.put(KEY_TRACK_START_SPEED, track.getStart_Speed()); + trkvalues.put(KEY_TRACK_START_TIME, track.getStart_Time()); + + trkvalues.put(KEY_TRACK_LASTFIX_TIME, track.getLastFix_Time()); + + trkvalues.put(KEY_TRACK_END_LATITUDE, track.getEnd_Latitude()); + trkvalues.put(KEY_TRACK_END_LONGITUDE, track.getEnd_Longitude()); + trkvalues.put(KEY_TRACK_END_ALTITUDE, track.getEnd_Altitude()); + trkvalues.put(KEY_TRACK_END_ACCURACY, track.getEnd_Accuracy()); + trkvalues.put(KEY_TRACK_END_SPEED, track.getEnd_Speed()); + trkvalues.put(KEY_TRACK_END_TIME, track.getEnd_Time()); + + trkvalues.put(KEY_TRACK_LASTSTEPDST_LATITUDE, track.getLastStepDistance_Latitude()); + trkvalues.put(KEY_TRACK_LASTSTEPDST_LONGITUDE, track.getLastStepDistance_Longitude()); + trkvalues.put(KEY_TRACK_LASTSTEPDST_ACCURACY, track.getLastStepDistance_Accuracy()); + + trkvalues.put(KEY_TRACK_LASTSTEPALT_ALTITUDE, track.getLastStepAltitude_Altitude()); + trkvalues.put(KEY_TRACK_LASTSTEPALT_ACCURACY, track.getLastStepAltitude_Accuracy()); + + trkvalues.put(KEY_TRACK_MIN_LATITUDE, track.getMin_Latitude()); + trkvalues.put(KEY_TRACK_MIN_LONGITUDE, track.getMin_Longitude()); + + trkvalues.put(KEY_TRACK_MAX_LATITUDE, track.getMax_Latitude()); + trkvalues.put(KEY_TRACK_MAX_LONGITUDE, track.getMax_Longitude()); + + trkvalues.put(KEY_TRACK_DURATION, track.getDuration()); + trkvalues.put(KEY_TRACK_DURATION_MOVING, track.getDuration_Moving()); + + trkvalues.put(KEY_TRACK_DISTANCE, track.getDistance()); + trkvalues.put(KEY_TRACK_DISTANCE_INPROGRESS, track.getDistanceInProgress()); + trkvalues.put(KEY_TRACK_DISTANCE_LASTALTITUDE, track.getDistanceLastAltitude()); + + trkvalues.put(KEY_TRACK_ALTITUDE_UP, track.getAltitude_Up()); + trkvalues.put(KEY_TRACK_ALTITUDE_DOWN, track.getAltitude_Down()); + trkvalues.put(KEY_TRACK_ALTITUDE_INPROGRESS, track.getAltitude_InProgress()); + + trkvalues.put(KEY_TRACK_SPEED_MAX, track.getSpeedMax()); + trkvalues.put(KEY_TRACK_SPEED_AVERAGE, track.getSpeedAverage()); + trkvalues.put(KEY_TRACK_SPEED_AVERAGEMOVING, track.getSpeedAverageMoving()); + + trkvalues.put(KEY_TRACK_NUMBEROFLOCATIONS, track.getNumberOfLocations()); + trkvalues.put(KEY_TRACK_NUMBEROFPLACEMARKS, track.getNumberOfPlacemarks()); + trkvalues.put(KEY_TRACK_TYPE, track.getType()); + + trkvalues.put(KEY_TRACK_VALIDMAP, track.getValidMap()); + + long TrackID; + // Inserting Row + TrackID = (db.insert(TABLE_TRACKS, null, trkvalues)); + + //Log.w("myApp", "[#] DatabaseHandler.java - addTrack " + TrackID); + + return TrackID; // Insert this in the track ID !!! + } + + + // Get Track + public Track getTrack(long TrackID) { + + Track track = null; + + String selectQuery = "SELECT * FROM " + TABLE_TRACKS + " WHERE " + + KEY_ID + " = " + TrackID; + + //Log.w("myApp", "[#] DatabaseHandler.java - getTrackList(" + startNumber + ", " +endNumber + ") ==> " + selectQuery); + + SQLiteDatabase db = this.getWritableDatabase(); + Cursor cursor = db.rawQuery(selectQuery, null); + + //Log.w("myApp", "[#] DatabaseHandler.java - getTrack(" + TrackID + ")"); + + if (cursor != null) { + if (cursor.moveToFirst()) { + track = new Track(); + track.FromDB(cursor.getLong(0), + cursor.getString(1), + cursor.getString(2), + cursor.getString(3), + + cursor.getDouble(4), + cursor.getDouble(5), + cursor.getDouble(6), + cursor.getFloat(7), + cursor.getFloat(8), + cursor.getLong(9), + + cursor.getLong(10), + + cursor.getDouble(11), + cursor.getDouble(12), + cursor.getDouble(13), + cursor.getFloat(14), + cursor.getFloat(15), + cursor.getLong(16), + + cursor.getDouble(17), + cursor.getDouble(18), + cursor.getFloat(19), + + cursor.getDouble(20), + cursor.getFloat(21), + + cursor.getDouble(22), + cursor.getDouble(23), + cursor.getDouble(24), + cursor.getDouble(25), + + cursor.getLong(26), + cursor.getLong(27), + + cursor.getFloat(28), + cursor.getFloat(29), + cursor.getLong(30), + + cursor.getDouble(31), + cursor.getDouble(32), + cursor.getDouble(33), + + cursor.getFloat(34), + cursor.getFloat(35), + cursor.getFloat(36), + + cursor.getLong(37), + cursor.getLong(38), + + cursor.getInt(39), + cursor.getInt(40)); + } + cursor.close(); + } + return track; + } + + + // Get last TrackID + public long getLastTrackID() { + SQLiteDatabase db = this.getWritableDatabase(); + long result = 0; + + String query = "SELECT " + KEY_ID + " FROM " + TABLE_TRACKS + " ORDER BY " + KEY_ID + " DESC LIMIT 1"; + + //Log.w("myApp", "[#] DatabaseHandler.java - getLastTrackID(): " + query); + + Cursor cursor = db.rawQuery(query, null); + + if (cursor != null) { + //Log.w("myApp", "[#] !null"); + if (cursor.moveToFirst()) { + result = cursor.getLong(0); + } //else Log.w("myApp", "[#] Tracks table is empty"); + cursor.close(); + } + //Log.w("myApp", "[#] RETURN getLastTrack()"); + //Log.w("myApp", "[#] DatabaseHandler.java - LastTrackID = " + result); + return result; + } + + + // Get last TrackID + public Track getLastTrack() { + return getTrack(getLastTrackID()); + } + + + // Getting a list of Tracks, with number between startNumber and endNumber + // Please note that limits both are inclusive! + public List getTracksList(long startNumber, long endNumber) { + + List trackList = new ArrayList<>(); + + String selectQuery = "SELECT * FROM " + TABLE_TRACKS + " WHERE " + + KEY_ID + " BETWEEN " + startNumber + " AND " + endNumber + + " ORDER BY " + KEY_ID + " DESC"; + + //Log.w("myApp", "[#] DatabaseHandler.java - getTrackList(" + startNumber + ", " +endNumber + ") ==> " + selectQuery); + + SQLiteDatabase db = this.getWritableDatabase(); + Cursor cursor = db.rawQuery(selectQuery, null); + + if (cursor != null) { + // looping through all rows and adding to list + if (cursor.moveToFirst()) { + do { + Track trk = new Track(); + trk.FromDB(cursor.getLong(0), + cursor.getString(1), + cursor.getString(2), + cursor.getString(3), + + cursor.getDouble(4), + cursor.getDouble(5), + cursor.getDouble(6), + cursor.getFloat(7), + cursor.getFloat(8), + cursor.getLong(9), + + cursor.getLong(10), + + cursor.getDouble(11), + cursor.getDouble(12), + cursor.getDouble(13), + cursor.getFloat(14), + cursor.getFloat(15), + cursor.getLong(16), + + cursor.getDouble(17), + cursor.getDouble(18), + cursor.getFloat(19), + + cursor.getDouble(20), + cursor.getFloat(21), + + cursor.getDouble(22), + cursor.getDouble(23), + cursor.getDouble(24), + cursor.getDouble(25), + + cursor.getLong(26), + cursor.getLong(27), + + cursor.getFloat(28), + cursor.getFloat(29), + cursor.getLong(30), + + cursor.getDouble(31), + cursor.getDouble(32), + cursor.getDouble(33), + + cursor.getFloat(34), + cursor.getFloat(35), + cursor.getFloat(36), + + cursor.getLong(37), + cursor.getLong(38), + + cursor.getInt(39), + cursor.getInt(40)); + + trackList.add(trk); // Add Track to list + } while (cursor.moveToNext()); + } + cursor.close(); + } + return trackList; + } + + + public void CorrectGPSWeekRollover() { + String CorrectLocationsQuery = "UPDATE " + TABLE_LOCATIONS + " SET " + KEY_LOCATION_TIME + " = " + KEY_LOCATION_TIME + " + 619315200000 WHERE " + + KEY_LOCATION_TIME + " <= 1388534400000 "; // 01/01/2014 00:00:00.000 + String CorrectPlacemarksQuery = "UPDATE " + TABLE_PLACEMARKS + " SET " + KEY_LOCATION_TIME + " = " + KEY_LOCATION_TIME + " + 619315200000 WHERE " + + KEY_LOCATION_TIME + " <= 1388534400000 "; // 01/01/2014 00:00:00.000 + String CorrectNamesQuery = "SELECT " + KEY_ID + "," + KEY_TRACK_NAME + " FROM " + TABLE_TRACKS + " WHERE " + + KEY_TRACK_NAME + " LIKE '199%'"; + + class IdAndName { + long id; + String Name; + } + + ArrayList Names = new ArrayList<>(); + + //Log.w("myApp", "[#] DatabaseHandler.java - getTrackList(" + startNumber + ", " +endNumber + ") ==> " + selectQuery); + + SQLiteDatabase db = this.getWritableDatabase(); + + // Correction of Locations + db.execSQL(CorrectLocationsQuery); + + // Correction of Placemarks + db.execSQL(CorrectPlacemarksQuery); + + + // Correction of Track Names + Cursor cursor = db.rawQuery(CorrectNamesQuery, null); + + if (cursor != null) { + int i = 0; + // looping through all rows and adding to list + if (cursor.moveToFirst()) { + do { + SimpleDateFormat SDF = new SimpleDateFormat("yyyyMMdd-HHmmss"); // date and time formatter + SDF.setTimeZone(TimeZone.getTimeZone("GMT")); + try { + Date d = SDF.parse(cursor.getString(1)); + long timeInMilliseconds = d.getTime(); + long timeInMilliseconds_Corrected = timeInMilliseconds + 619315200000L; + String Name_Corrected = SDF.format(timeInMilliseconds_Corrected); + //Log.w("myApp", "[#] GPSApplication.java - NAME CORRECTED FROM " + cursor.getString(0) + " TO " + Name_Corrected); + IdAndName IN = new IdAndName(); + IN.id = cursor.getLong(0); + IN.Name = Name_Corrected; + Names.add(IN); + } catch (ParseException ex) { + Log.v("Exception", ex.getLocalizedMessage()); + } + i++; + } while (cursor.moveToNext()); + } + Log.w("myApp", "[#] DatabaseHandler.java - CorrectGPSWeekRollover NAMES = " + i); + cursor.close(); + } + + for (IdAndName N : Names) { + Log.w("myApp", "[#] GPSApplication.java - CORRECTING TRACK " + N.id + " = " + N.Name); + db.execSQL("UPDATE " + TABLE_TRACKS + " SET " + KEY_TRACK_NAME + " = \"" + N.Name + "\" WHERE " + KEY_ID + " = " + N.id); + } + } + + + // Getting the list of all Tracks in the DB + /* NOT USED, COMMENTED OUT !! + public List getTracksList() { + + List trackList = new ArrayList(); + + String selectQuery = "SELECT * FROM " + TABLE_TRACKS + + " ORDER BY " + KEY_ID + " DESC"; + + //Log.w("myApp", "[#] DatabaseHandler.java - getTrackList() ==> " + selectQuery); + + SQLiteDatabase db = this.getWritableDatabase(); + Cursor cursor = db.rawQuery(selectQuery, null); + + if (cursor != null) { + // looping through all rows and adding to list + if (cursor.moveToFirst()) { + do { + Track trk = new Track(); + trk.FromDB(cursor.getLong(0), + cursor.getString(1), + cursor.getString(2), + cursor.getString(3), + + cursor.getDouble(4), + cursor.getDouble(5), + cursor.getDouble(6), + cursor.getFloat(7), + cursor.getFloat(8), + cursor.getLong(9), + + cursor.getLong(10), + + cursor.getDouble(11), + cursor.getDouble(12), + cursor.getDouble(13), + cursor.getFloat(14), + cursor.getFloat(15), + cursor.getLong(16), + + cursor.getDouble(17), + cursor.getDouble(18), + cursor.getFloat(19), + + cursor.getDouble(20), + cursor.getFloat(21), + + cursor.getDouble(22), + cursor.getDouble(23), + cursor.getDouble(24), + cursor.getDouble(25), + + cursor.getLong(26), + cursor.getLong(27), + + cursor.getFloat(28), + cursor.getFloat(29), + cursor.getLong(30), + + cursor.getDouble(31), + cursor.getDouble(32), + cursor.getDouble(33), + + cursor.getFloat(34), + cursor.getFloat(35), + cursor.getFloat(36), + + cursor.getLong(37), + cursor.getLong(38), + + cursor.getInt(39), + cursor.getInt(40)); + trackList.add(trk); // Add Track to list + } while (cursor.moveToNext()); + } + cursor.close(); + } + return trackList; + } */ +} \ No newline at end of file diff --git a/app/src/main/java/Satellite/EGM96.java b/app/src/main/java/Satellite/EGM96.java new file mode 100644 index 0000000..e1d9964 --- /dev/null +++ b/app/src/main/java/Satellite/EGM96.java @@ -0,0 +1,276 @@ + +package Satellite; + + +import android.util.Log; + +import org.greenrobot.eventbus.EventBus; + +import java.io.BufferedInputStream; +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +class EGM96 { + + // ---------------------------------------------------------------------------- Singleton Class + private static EGM96 instance = new EGM96(); + + private EGM96(){} + + public static EGM96 getInstance(){ + return instance; + } + + + + private int BOUNDARY = 3; // The grid extensions (in each of the 4 sides) of the real 721 x 1440 grid + private short[][] EGMGrid = new short[BOUNDARY + 1440 + BOUNDARY][BOUNDARY + 721 + BOUNDARY]; + private boolean isEGMGridLoaded = false; + private boolean isEGMGridLoading = false; + private boolean isEGMFileCopying = false; + private String EGMFileName; + private String EGMFileNameLocalCopy; + + public double EGM96_VALUE_INVALID = -100000; + + public void LoadGridFromFile(String FileName, String FileNameLocalCopy) { + if (!isEGMGridLoaded && !isEGMGridLoading) { + isEGMGridLoading = true; + EGMFileName = FileName; + EGMFileNameLocalCopy = FileNameLocalCopy; + new Thread(new LoadEGM96Grid()).start(); + } else { + if (isEGMGridLoading) Log.w("myApp", "[#] EGM96.java - Grid is already loading, please wait"); + if (isEGMGridLoaded) Log.w("myApp", "[#] EGM96.java - Grid already loaded"); + } + } + + /* public void UnloadGrid() { + isEGMGridLoaded = false; + isEGMGridLoading = false; + //listener.onEGMGridLoaded(isEGMGridLoaded); + EventBus.getDefault().post(EventBusMSG.UPDATE_FIX); + EventBus.getDefault().post(EventBusMSG.UPDATE_TRACK); + EventBus.getDefault().post(EventBusMSG.UPDATE_TRACKLIST); + } +*/ + public boolean isEGMGridLoaded() { + return isEGMGridLoaded; + } + + public boolean isEGMGridLoading() { + return isEGMGridLoading; + } + + public double getEGMCorrection(double Latitude, double Longitude) { + // This function calculates and return the EGM96 altitude correction value of the input coordinates, in m; + // Input coordinates are: -90 < Latitude < 90; -180 < Longitude < 360 (android range -180 < Longitude < 180); + + if (isEGMGridLoaded) { + double Lat = 90.0 - Latitude; + double Lon = Longitude; + if (Lon < 0) Lon += 360.0; + + int ilon = (int) (Lon / 0.25) + BOUNDARY; + int ilat = (int) (Lat / 0.25) + BOUNDARY; + + try { + // Creating points for interpolation + short hc11 = EGMGrid[ilon][ilat]; + short hc12 = EGMGrid[ilon][ilat + 1]; + short hc21 = EGMGrid[ilon + 1][ilat]; + short hc22 = EGMGrid[ilon + 1][ilat + 1]; + + // Interpolation: + // Latitude + double hc1 = hc11 + (hc12 - hc11) * (Lat % 0.25) / 0.25; + double hc2 = hc21 + (hc22 - hc21) * (Lat % 0.25) / 0.25; + // Longitude + //double hc = (hc1 + (hc2 - hc1) * (Lon % 0.25) / 0.25) / 100; + //Log.w("myApp", "[#] EGM96.java - getEGMCorrection(" + Latitude + ", " + Longitude + ") = " + hc); + + return ((hc1 + (hc2 - hc1) * (Lon % 0.25) / 0.25) / 100); + } catch (ArrayIndexOutOfBoundsException e) { + return EGM96_VALUE_INVALID; + } + } + else return EGM96_VALUE_INVALID; + } + + private void copyFile(InputStream in, OutputStream out) throws IOException { + byte[] buffer = new byte[1024]; + int read; + while ((read = in.read(buffer)) != -1) { + out.write(buffer, 0, read); + } + } + + private void DeleteFile(String filename) { + File file = new File(filename); + if (file.exists ()) file.delete(); + } + + + // The Thread that loads the grid in background ------------------------------------------------ + + private class LoadEGM96Grid implements Runnable { + // Thread: Load EGM grid + + @Override + public void run() { + Thread.currentThread().setPriority(Thread.MIN_PRIORITY); + Log.w("myApp", "[#] EGM96.java - Start loading grid"); + + boolean islocalcopypresent = false; + boolean issharedcopypresent = false; + + File localfile = new File(EGMFileNameLocalCopy); + if (localfile.exists() && (localfile.length() == 2076480)) islocalcopypresent = true; + + File sharedfile = new File(EGMFileName); + if (sharedfile.exists() && (sharedfile.length() == 2076480)) issharedcopypresent = true; + + File file = new File(islocalcopypresent ? EGMFileNameLocalCopy : EGMFileName); + if (islocalcopypresent || issharedcopypresent) { + Log.w("myApp", "[#] EGM96.java - From file: " + file.getAbsolutePath()); + + FileInputStream fin; + try { + fin = new FileInputStream(file); + } catch (FileNotFoundException e) { + isEGMGridLoaded = false; + isEGMGridLoading = false; + Log.w("myApp", "[#] EGM96.java - FileNotFoundException"); + //Toast.makeText(getApplicationContext(), "Oops", Toast.LENGTH_SHORT).show(); + //e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates. + return; + } + BufferedInputStream bin = new BufferedInputStream(fin); + DataInputStream din = new DataInputStream(bin); + int i; + int i_lon = BOUNDARY; + int i_lat = BOUNDARY; + int count = (int) ((file.length() / 2)); + + for (i = 0; (i < count); i++) { + try { + EGMGrid[i_lon][i_lat] = din.readShort(); + i_lon++; + if (i_lon >= (1440 + BOUNDARY)) { + i_lat++; + i_lon = BOUNDARY; + } + } catch (IOException e) { + isEGMGridLoaded = false; + isEGMGridLoading = false; + Log.w("myApp", "[#] EGM96.java - IOException"); + return; + //Toast.makeText(getApplicationContext(), "Oops", Toast.LENGTH_SHORT).show(); + //e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates. + } + } + + if (BOUNDARY > 0) { + // Fill boundaries with correct data, in order to speed up retrieving for interpolation; + // fill left + right boundaries + //Log.w("myApp", "[#] EGM96.java - LR BOUNDARIES"); + for (int ix = 0; (ix < BOUNDARY); ix++) { + for (int iy = BOUNDARY; (iy < BOUNDARY + 721); iy++) { + EGMGrid[ix][iy] = EGMGrid[ix + 1440][iy]; + EGMGrid[BOUNDARY + ix + 1440][iy] = EGMGrid[BOUNDARY + ix][iy]; + } + } + // fill top + bottom boundaries + //Log.w("myApp", "[#] EGM96.java - TOP DOWN BOUNDARIES"); + for (int iy = 0; (iy < BOUNDARY); iy++) { + for (int ix = 0; (ix < BOUNDARY + 1440 + BOUNDARY); ix++) { + if (ix > 720) { + EGMGrid[ix][iy] = EGMGrid[ix - 720][BOUNDARY + BOUNDARY - iy]; + EGMGrid[ix][BOUNDARY + iy + 721] = EGMGrid[ix - 720][BOUNDARY + 721-2 - iy]; + } + else { + EGMGrid[ix][iy] = EGMGrid[ix + 720][BOUNDARY + BOUNDARY - iy]; + EGMGrid[ix][BOUNDARY + iy + 721] = EGMGrid[ix + 720][BOUNDARY + 721-2 - iy]; + } + } + } + } + + isEGMGridLoading = false; + isEGMGridLoaded = true; + Log.w("myApp", "[#] EGM96.java - Grid Successfully Loaded: " + file.getAbsolutePath()); + //Toast.makeText(getApplicationContext(), "EGM96 correction grid loaded", Toast.LENGTH_SHORT).show(); + + if (issharedcopypresent) { + if (!islocalcopypresent) new Thread(new CopyEGM96Grid()).start(); + else { + DeleteFile(EGMFileName); // Delete the EGM file from the shared folder + Log.w("myApp", "[#] EGM96.java - EGM File already present into FilesDir. File deleted from shared folder"); + } + } + + } else { + isEGMGridLoading = false; + isEGMGridLoaded = false; + if (!file.exists()) { Log.w("myApp", "[#] EGM96.java - File not found"); } + if (!file.canRead()) { Log.w("myApp", "[#] EGM96.java - Cannot read file"); } + if (file.length() != 2076480) { + Log.w("myApp", "[#] EGM96.java - File has invalid length: " + file.length());} + //Toast.makeText(getApplicationContext(), "EGM96 correction not available", Toast.LENGTH_SHORT).show(); + } + EventBus.getDefault().post(EventBusMSG.UPDATE_FIX); + EventBus.getDefault().post(EventBusMSG.UPDATE_TRACK); + EventBus.getDefault().post(EventBusMSG.UPDATE_TRACKLIST); + //listener.onEGMGridLoaded(isEGMGridLoaded); + } + } + + // The Thread that copies the EGM grid in FilesDir (in background) ----------------------------- + + private class CopyEGM96Grid implements Runnable { + // Thread: Copy the EGM grid in FilesDir + + @Override + public void run() { + Thread.currentThread().setPriority(Thread.MIN_PRIORITY); + Log.w("myApp", "[#] EGM96.java - Copy EGM96 Grid into FilesDir"); + + if (isEGMFileCopying) return; + + isEGMFileCopying = true; + + File sd_cpy = new File(EGMFileNameLocalCopy); + if (sd_cpy.exists()) sd_cpy.delete(); + + File sd_old = new File(EGMFileName); + if (sd_old.exists()) { + InputStream in = null; + OutputStream out = null; + try { + in = new FileInputStream(EGMFileName); + out = new FileOutputStream(EGMFileNameLocalCopy); + copyFile(in, out); + in.close(); + in = null; + out.flush(); + out.close(); + out = null; + Log.w("myApp", "[#] EGM96.java - EGM File copy completed"); + DeleteFile(EGMFileName); // Delete the EGM file from the shared folder + Log.w("myApp", "[#] EGM96.java - EGM File deleted from shared folder"); + + } catch(Exception e) { + Log.w("MyApp", "[#] EGM96.java - Unable to make local copy of EGM file: " + e.getMessage()); + } + } + + isEGMFileCopying = false; + } + } +} diff --git a/app/src/main/java/Satellite/EventBusMSG.java b/app/src/main/java/Satellite/EventBusMSG.java new file mode 100644 index 0000000..7eeacfd --- /dev/null +++ b/app/src/main/java/Satellite/EventBusMSG.java @@ -0,0 +1,32 @@ + +package Satellite; + +public class EventBusMSG { + + static final short APP_RESUME = 1; // Sent to components on app resume + static final short APP_PAUSE = 2; // Sent to components on app pause + static final short NEW_TRACK = 3; // Request to create a new track + static final short UPDATE_FIX = 4; // Notify that a new fix is available + static final short UPDATE_TRACK = 5; // Notify that the current track stats are updated + static final short UPDATE_TRACKLIST = 6; // Notify that the tracklist is changed + static final short UPDATE_SETTINGS = 7; // Tell that settings are changed + static final short REQUEST_ADD_PLACEMARK = 8; // The user ask to add a placemark + static final short ADD_PLACEMARK = 9; // The placemark is available + static final short APPLY_SETTINGS = 10; // The new settings must be applied + static final short TOAST_TRACK_EXPORTED = 11; // The exporter has finished to export the track, shows toast + static final short TOAST_STORAGE_PERMISSION_REQUIRED= 12; // The Storage permission is required + static final short UPDATE_JOB_PROGRESS = 13; // Update the progress of the current Job + static final short NOTIFY_TRACKS_DELETED = 14; // Notify that some tracks are deleted + static final short UPDATE_ACTIONBAR = 15; // Notify that the actionbar must be updated + static final short REFRESH_TRACKLIST = 16; // Refresh the tracklist, without update it from DB + + static final short TRACKLIST_DESELECT = 24; // The user deselect (into the tracklist) the track with a given id + static final short TRACKLIST_SELECT = 25; // The user select (into the tracklist) the track with a given id + static final short INTENT_SEND = 26; // Request to share + static final short TOAST_UNABLE_TO_WRITE_THE_FILE = 27; // Exporter fails to export the Track (given id) + + static final short ACTION_BULK_DELETE_TRACKS = 40; // Delete the selected tracks + static final short ACTION_BULK_EXPORT_TRACKS = 41; // Export the selected tracks + static final short ACTION_BULK_VIEW_TRACKS = 42; // View the selected tracks + static final short ACTION_BULK_SHARE_TRACKS = 43; // Share the selected tracks +} diff --git a/app/src/main/java/Satellite/ExportingTask.java b/app/src/main/java/Satellite/ExportingTask.java new file mode 100644 index 0000000..ace345f --- /dev/null +++ b/app/src/main/java/Satellite/ExportingTask.java @@ -0,0 +1,58 @@ + +package Satellite; + +public class ExportingTask { + + static final short STATUS_PENDING = 0; // Task not yet started + static final short STATUS_RUNNING = 1; // Task is running... + static final short STATUS_ENDED_SUCCESS = 2; // Task ended with success + static final short STATUS_ENDED_FAILED = 3; // Task failed to export + + private long id = 0; + private long NumberOfPoints_Total = 0; + private long NumberOfPoints_Processed = 0; + private short Status = STATUS_PENDING; + private String Name = ""; + + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public long getNumberOfPoints_Total() { + return NumberOfPoints_Total; + } + + public void setNumberOfPoints_Total(long numberOfPoints_Total) { + NumberOfPoints_Total = numberOfPoints_Total; + } + + public long getNumberOfPoints_Processed() { + return NumberOfPoints_Processed; + } + + public void setNumberOfPoints_Processed(long numberOfPoints_Processed) { + NumberOfPoints_Processed = numberOfPoints_Processed; + } + + public short getStatus() { + return Status; + } + + public void setStatus(short status) { + Status = status; + } + + public String getName() { + return Name; + } + + public void setName(String name) { + Name = name; + } +} + diff --git a/app/src/main/java/Satellite/FragmentGPSFix.java b/app/src/main/java/Satellite/FragmentGPSFix.java new file mode 100644 index 0000000..82f3445 --- /dev/null +++ b/app/src/main/java/Satellite/FragmentGPSFix.java @@ -0,0 +1,290 @@ + +package Satellite; + +import android.content.Intent; +import android.content.res.Configuration; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.text.method.ScrollingMovementMethod; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.TableLayout; +import android.widget.TextView; + +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import fr.geolabs.dev.mapmint4me.R; + +public class FragmentGPSFix extends Fragment { + + private static final int NOT_AVAILABLE = -100000; + + private final int GPS_DISABLED = 0; + private final int GPS_OUTOFSERVICE = 1; + private final int GPS_TEMPORARYUNAVAILABLE = 2; + private final int GPS_SEARCHING = 3; + private final int GPS_STABILIZING = 4; + private final int GPS_OK = 5; + + private PhysicalDataFormatter phdformatter = new PhysicalDataFormatter(); + + private FrameLayout FLGPSFix; + + private TextView TVLatitude; + private TextView TVLongitude; + private TextView TVLatitudeUM; + private TextView TVLongitudeUM; + private TextView TVAltitude; + private TextView TVAltitudeUM; + private TextView TVSpeed; + private TextView TVSpeedUM; + private TextView TVBearing; + private TextView TVAccuracy; + private TextView TVAccuracyUM; + private TextView TVGPSFixStatus; + private TextView TVDirectionUM; + private TextView TVTime; + public TextView TVSatellites; + public TextView TVSatellites1; + private TableLayout TLCoordinates; + private TableLayout TLAltitude; + private TableLayout TLSpeed; + private TableLayout TLBearing; + private TableLayout TLAccuracy; + private TableLayout TLTime; + public TableLayout TLSatellites; + + private LinearLayout LLTimeSatellites; + + private PhysicalData phdLatitude; + private PhysicalData phdLongitude; + private PhysicalData phdAltitude; + private PhysicalData phdSpeed; + private PhysicalData phdBearing; + private PhysicalData phdAccuracy; + private PhysicalData phdTime; + + final GPSApplication gpsApplication = GPSApplication.getInstance(); + + public FragmentGPSFix() { + // Required empty public constructor + } + + @Subscribe (threadMode = ThreadMode.MAIN) + public void onEvent(Short msg) { + if (msg == EventBusMSG.UPDATE_FIX) { + Update(); + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + // Inflate the layout for this fragment + View view = inflater.inflate(R.layout.fragment_gpsfix, container, false); + + // FrameLayouts + FLGPSFix = view.findViewById(R.id.id_fragmentgpsfixFrameLayout); + + // TextViews + TVLatitude = view.findViewById(R.id.id_textView_Latitude); + TVLongitude = view.findViewById(R.id.id_textView_Longitude); + TVLatitudeUM = view.findViewById(R.id.id_textView_LatitudeUM); + TVLongitudeUM = view.findViewById(R.id.id_textView_LongitudeUM); + TVAltitude = view.findViewById(R.id.id_textView_Altitude); + TVAltitudeUM = view.findViewById(R.id.id_textView_AltitudeUM); + TVSpeed = view.findViewById(R.id.id_textView_Speed); + TVSpeedUM = view.findViewById(R.id.id_textView_SpeedUM); + TVBearing = view.findViewById(R.id.id_textView_Bearing); + TVAccuracy = view.findViewById(R.id.id_textView_Accuracy); + TVAccuracyUM = view.findViewById(R.id.id_textView_AccuracyUM); + TVGPSFixStatus = view.findViewById(R.id.id_textView_GPSFixStatus); + TVDirectionUM = view.findViewById(R.id.id_textView_BearingUM); + TVTime = view.findViewById(R.id.id_textView_Time); + TVSatellites = view.findViewById(R.id.id_textView_satellite1); + TVSatellites1 = view.findViewById(R.id.id_textView_satellite2); + TVSatellites1.setMovementMethod(new ScrollingMovementMethod()); + + + // TableLayouts + TLCoordinates = view.findViewById(R.id.id_TableLayout_Coordinates) ; + TLAltitude = view.findViewById(R.id.id_TableLayout_Altitude); + TLSpeed = view.findViewById(R.id.id_TableLayout_Speed); + TLBearing = view.findViewById(R.id.id_TableLayout_Bearing); + TLAccuracy = view.findViewById(R.id.id_TableLayout_Accuracy); + TLTime = view.findViewById(R.id.id_TableLayout_Time); + TLSatellites = view.findViewById(R.id.id_TableLayout_Satellites); + + // LinearLayouts + LLTimeSatellites = view.findViewById(R.id.id_linearLayout_Time_Satellites); + + TVGPSFixStatus.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (GPSStatus == GPS_DISABLED) { + if (GPSApplication.getInstance().getLocationSettingsFlag()) { + // This is the second click + GPSApplication.getInstance().setLocationSettingsFlag(false); + // Go to Settings screen + Intent callGPSSettingIntent = new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS); + if (callGPSSettingIntent != null) startActivityForResult(callGPSSettingIntent, 0); + } else { + GPSApplication.getInstance().setLocationSettingsFlag(true); // Start the timer + } + } + } + }); + + return view; + } + + @Override + public void onResume() { + super.onResume(); + + // Workaround for Nokia Devices, Android 9 + // https://github.com/BasicAirData/GPSLogger/issues/77 + if (EventBus.getDefault().isRegistered(this)) { + //Log.w("myApp", "[#] FragmentGPSFix.java - EventBus: FragmentGPSFix already registered"); + EventBus.getDefault().unregister(this); + } + + EventBus.getDefault().register(this); + Update(); + } + + @Override + public void onPause() { + EventBus.getDefault().unregister(this); + super.onPause(); + } + + + private LocationExtended location; + private double AltitudeManualCorrection; + private int prefDirections; + private int GPSStatus = GPS_DISABLED; + private boolean EGMAltitudeCorrection; + private boolean isValidAltitude; + + public void Update() { + //Log.w("myApp", "[#] FragmentGPSFix.java - Update(Location location)"); + location = gpsApplication.getCurrentLocationExtended(); + AltitudeManualCorrection = gpsApplication.getPrefAltitudeCorrection(); + prefDirections = gpsApplication.getPrefShowDirections(); + GPSStatus = gpsApplication.getGPSStatus(); + EGMAltitudeCorrection = gpsApplication.getPrefEGM96AltitudeCorrection(); + if (isAdded()) { + if ((location != null) && (GPSStatus == GPS_OK)) { + + phdLatitude = phdformatter.format(location.getLatitude(), PhysicalDataFormatter.FORMAT_LATITUDE); + phdLongitude = phdformatter.format(location.getLongitude(), PhysicalDataFormatter.FORMAT_LONGITUDE); + phdAltitude = phdformatter.format(location.getAltitudeCorrected(AltitudeManualCorrection, EGMAltitudeCorrection), PhysicalDataFormatter.FORMAT_ALTITUDE); + phdSpeed = phdformatter.format(location.getSpeed(), PhysicalDataFormatter.FORMAT_SPEED); + phdBearing = phdformatter.format(location.getBearing(), PhysicalDataFormatter.FORMAT_BEARING); + phdAccuracy = phdformatter.format(location.getAccuracy(), PhysicalDataFormatter.FORMAT_ACCURACY); + phdTime = phdformatter.format(location.getTime(), PhysicalDataFormatter.FORMAT_TIME); + + TVLatitude.setText(phdLatitude.Value); + TVLongitude.setText(phdLongitude.Value); + TVLatitudeUM.setText(phdLatitude.UM); + TVLongitudeUM.setText(phdLongitude.UM); + //TVAltitude.setText(phdAltitude.Value);TVAltitudeUM.setText(phdAltitude.UM); + //TVSpeed.setText(phdSpeed.Value); + //TVSpeedUM.setText(phdSpeed.UM); + //TVBearing.setText(phdBearing.Value); + //TVAccuracy.setText(phdAccuracy.Value); + //TVAccuracyUM.setText(phdAccuracy.UM); + // TVTime.setText(phdTime.Value); + TVSatellites.setText(location.getNumberOfSatellitesUsedInFix() != NOT_AVAILABLE ? location.getNumberOfSatellitesUsedInFix() + "/" + location.getNumberOfSatellites() : ""); + int i =0; + while(i<10) { + + TVSatellites1.setText(location.getSatellite_info()); + i = i + 1; + } + // Colorize the Altitude textview depending on the altitude EGM Correction + isValidAltitude = EGMAltitudeCorrection && (location.getAltitudeEGM96Correction() != NOT_AVAILABLE); + TVAltitude.setTextColor(isValidAltitude ? getResources().getColor(R.color.textColorPrimary) : getResources().getColor(R.color.textColorSecondary)); + TVAltitudeUM.setTextColor(isValidAltitude ? getResources().getColor(R.color.textColorPrimary) : getResources().getColor(R.color.textColorSecondary)); + + TVGPSFixStatus.setVisibility(View.GONE); + + TVDirectionUM.setVisibility(prefDirections == 0 ? View.GONE : View.VISIBLE); + + TLCoordinates.setVisibility(phdLatitude.Value.equals("") ? View.INVISIBLE : View.VISIBLE); + TLAltitude.setVisibility(phdAltitude.Value.equals("") ? View.INVISIBLE : View.VISIBLE); + TLSpeed.setVisibility(phdSpeed.Value.equals("") ? View.INVISIBLE : View.VISIBLE); + TLBearing.setVisibility(phdBearing.Value.equals("") ? View.INVISIBLE : View.VISIBLE); + TLAccuracy.setVisibility(phdAccuracy.Value.equals("") ? View.INVISIBLE : View.VISIBLE); + TLTime.setVisibility(View.VISIBLE); + TLSatellites.setVisibility(location.getNumberOfSatellitesUsedInFix() == NOT_AVAILABLE ? View.INVISIBLE : View.VISIBLE); + + FLGPSFix.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, + int oldLeft, int oldTop, int oldRight, int oldBottom) { + FLGPSFix.removeOnLayoutChangeListener(this); + + int ViewHeight = TLTime.getMeasuredHeight() + (int)(6*getResources().getDisplayMetrics().density); + int LayoutHeight = FLGPSFix.getHeight() - (int)(6*getResources().getDisplayMetrics().density); + //Log.w("myApp", "[#]"); + //Log.w("myApp", "[#] -----------------------------------"); + boolean isTimeAndSatellitesVisible; + if(getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT){ + isTimeAndSatellitesVisible = LayoutHeight >= 6*ViewHeight; + //Log.w("myApp", "[#] " + LayoutHeight + " / " + 6*ViewHeight + " -> " + isTimeAndSatellitesVisible); + } else { + isTimeAndSatellitesVisible = LayoutHeight >= 4*ViewHeight; + //Log.w("myApp", "[#] " + LayoutHeight + " / " + 4*ViewHeight + " -> " + isTimeAndSatellitesVisible); + } + LLTimeSatellites.setVisibility(isTimeAndSatellitesVisible ? View.VISIBLE : View.GONE); + + //Log.w("myApp", "[#] -----------------------------------"); + //Log.w("myApp", "[#] Available Height = " + LayoutHeight + " px"); + //Log.w("myApp", "[#] Density = " + getResources().getDisplayMetrics().density); + //Log.w("myApp", "[#] Tile Height = " + ViewHeight + " px"); + } + }); + + } else { + TLCoordinates.setVisibility(View.INVISIBLE); + TLAltitude.setVisibility(View.INVISIBLE); + TLSpeed.setVisibility(View.INVISIBLE); + TLBearing.setVisibility(View.INVISIBLE); + TLAccuracy.setVisibility(View.INVISIBLE); + TLTime.setVisibility(View.INVISIBLE); + TLSatellites.setVisibility(View.INVISIBLE); + + TVGPSFixStatus.setVisibility(View.VISIBLE); + switch (GPSStatus) { + case GPS_DISABLED: + TVGPSFixStatus.setText(R.string.gps_disabled_with_hint); + break; + case GPS_OUTOFSERVICE: + TVGPSFixStatus.setText(R.string.gps_out_of_service); + break; + case GPS_TEMPORARYUNAVAILABLE: + //TVGPSFixStatus.setText(R.string.gps_temporary_unavailable); + //break; + case GPS_SEARCHING: + TVGPSFixStatus.setText(R.string.gps_searching); + break; + case GPS_STABILIZING: + TVGPSFixStatus.setText(R.string.gps_stabilizing); + break; + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/Satellite/GPSActivity.java b/app/src/main/java/Satellite/GPSActivity.java new file mode 100644 index 0000000..f778f23 --- /dev/null +++ b/app/src/main/java/Satellite/GPSActivity.java @@ -0,0 +1,583 @@ + +package Satellite; + +import android.Manifest; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.provider.Settings; +import android.support.annotation.NonNull; +import android.support.design.widget.BottomSheetBehavior; +import android.support.design.widget.TabLayout; +import android.support.v4.app.ActivityCompat; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentPagerAdapter; +import android.support.v4.content.ContextCompat; +import android.support.v4.view.ViewPager; +import android.support.v7.app.AlertDialog; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.preference.PreferenceManager; +import android.support.v7.view.ActionMode; +import android.support.v7.view.ContextThemeWrapper; +import android.support.v7.widget.Toolbar; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.WindowManager; +import android.widget.Toast; + +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import fr.geolabs.dev.mapmint4me.R; + +public class GPSActivity extends AppCompatActivity { + + private static final int REQUEST_ID_MULTIPLE_PERMISSIONS = 1; + + private final GPSApplication GPSApp = GPSApplication.getInstance(); + + private Toolbar toolbar; + private TabLayout tabLayout; + private ViewPager viewPager; + private ActionMode actionMode; + private View bottomSheet; + private MenuItem menutrackfinished = null; + private int activeTab = 1; + final Context context = this; + + private boolean prefKeepScreenOn = true; + private boolean show_toast_grant_storage_permission = false; + private int theme; + + private BottomSheetBehavior mBottomSheetBehavior; + + Toast ToastClickAgain; + + + @Override + public void onRestart(){ + Log.w("myApp", "[#] " + this + " - onRestart()"); + + if (Integer.valueOf(PreferenceManager.getDefaultSharedPreferences(getApplicationContext()).getString("prefColorTheme", "2")) != theme) { + Log.w("myApp", "[#] GPSActivity.java - it needs to be recreated (Theme changed)"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + // Normal behaviour for Android 5 + + this.recreate(); + } else { + // Workaround to a bug on Android 4.4.X platform (google won't fix because Android 4.4 is obsolete) + // Android 4.4.X: taskAffinity & launchmode='singleTask' violating Activity's lifecycle + // https://issuetracker.google.com/issues/36998700 + finish(); + startActivity(new Intent(this, getClass())); + } + overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out); + } + super.onRestart(); + } + + + @Override + protected void onCreate(Bundle savedInstanceState) { + Log.w("myApp", "[#] " + this + " - onCreate()"); + + setTheme(R.style.MyMaterialTheme); + theme = Integer.valueOf(PreferenceManager.getDefaultSharedPreferences(getApplicationContext()).getString("prefColorTheme", "2")); + + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_gps); + toolbar = findViewById(R.id.id_toolbar); + setSupportActionBar(toolbar); + getSupportActionBar().setDisplayHomeAsUpEnabled(false); + viewPager = findViewById(R.id.id_viewpager); + viewPager.setOffscreenPageLimit(3); + + setupViewPager(viewPager); + + tabLayout = findViewById(R.id.id_tablayout); + tabLayout.setTabMode(TabLayout.MODE_FIXED); + tabLayout.setupWithViewPager(viewPager); + + tabLayout.addOnTabSelectedListener( + new TabLayout.ViewPagerOnTabSelectedListener(viewPager) { + @Override + public void onTabSelected(TabLayout.Tab tab) { + super.onTabSelected(tab); + activeTab = tab.getPosition(); + GPSApp.setGPSActivity_activeTab(activeTab); + updateBottomSheetPosition(); + ActivateActionModeIfNeeded(); + } + }); + + bottomSheet = findViewById( R.id.id_bottomsheet ); + + mBottomSheetBehavior = BottomSheetBehavior.from(bottomSheet); + mBottomSheetBehavior.setHideable (false); + + ToastClickAgain = Toast.makeText(this, getString(R.string.toast_track_finished_click_again), Toast.LENGTH_SHORT); + } + + + @Override + public void onStart() { + Log.w("myApp", "[#] " + this + " - onStart()"); + super.onStart(); + activeTab = tabLayout.getSelectedTabPosition(); + System.out.println(activeTab); + GPSApp.setGPSActivity_activeTab(activeTab); + } + + + @Override + public void onStop() { + Log.w("myApp", "[#] " + this + " - onStop()"); + super.onStop(); + } + + + @Override + public void onResume() { + Log.w("myApp", "[#] " + this + " - onResume()"); + super.onResume(); + + // Workaround for Nokia Devices, Android 9 + // https://github.com/BasicAirData/GPSLogger/issues/77 + if (EventBus.getDefault().isRegistered(this)) { + //Log.w("myApp", "[#] GPSActivity.java - EventBus: GPSActivity already registered"); + EventBus.getDefault().unregister(this); + } + + EventBus.getDefault().register(this); + LoadPreferences(); + EventBus.getDefault().post(EventBusMSG.APP_RESUME); + if (menutrackfinished != null) menutrackfinished.setVisible(!GPSApp.getCurrentTrack().getName().equals("")); + + // Check for Location runtime Permissions (for Android 23+) + if (!GPSApp.isLocationPermissionChecked()) { + CheckLocationPermission(); + GPSApp.setLocationPermissionChecked(true); + } + + ActivateActionModeIfNeeded(); + + if (GPSApp.FlagExists(GPSApp.FLAG_RECORDING) && !GPSApp.getRecording()) { + // The app is crashed in background + Log.w("myApp", "[#] GPSActivity.java - THE APP HAS BEEN KILLED IN BACKGROUND DURING A RECORDING !!!"); + GPSApp.FlagRemove(GPSApp.FLAG_RECORDING); + + AlertDialog.Builder builder = new AlertDialog.Builder(new ContextThemeWrapper(this, R.style.StyledDialog)); + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + builder.setMessage(getResources().getString(R.string.dlg_app_killed) + "\n\n" + getResources().getString(R.string.dlg_app_killed_description)); + builder.setNeutralButton(R.string.open_android_app_settings, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + Intent intent = new Intent(); + intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + Uri uri = Uri.fromParts("package", getPackageName(), null); + intent.setData(uri); + try { + startActivity(intent); + } catch (Exception e) { + //Log.w("myApp", "[#] GPSActivity.java - Unable to open the settings screen"); + } + dialog.dismiss(); + } + }); + } + else builder.setMessage(getResources().getString(R.string.dlg_app_killed)); + builder.setIcon(android.R.drawable.ic_menu_info_details); + builder.setPositiveButton(R.string.about_ok, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dialog.dismiss(); + } + }); + + AlertDialog dialog = builder.create(); + dialog.show(); + } + + if (GPSApp.isJustStarted() && (GPSApp.getCurrentTrack().getNumberOfLocations() + GPSApp.getCurrentTrack().getNumberOfPlacemarks() > 0)) { + Toast.makeText(this.context, getString(R.string.toast_active_track_not_empty), Toast.LENGTH_LONG).show(); + GPSApp.setJustStarted(false); + } else GPSApp.setJustStarted(false); + + if (show_toast_grant_storage_permission) { + Toast.makeText(this.context, getString(R.string.please_grant_storage_permission), Toast.LENGTH_LONG).show(); + show_toast_grant_storage_permission = false; + } + } + + + @Override + public void onPause() { + Log.w("myApp", "[#] " + this + " - onPause()"); + EventBus.getDefault().post(EventBusMSG.APP_PAUSE); + EventBus.getDefault().unregister(this); + super.onPause(); + } + + + @Override + public void onBackPressed() { + ShutdownApp(); + } + + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + updateBottomSheetPosition(); + } + + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.main_menu, menu); + return true; + } + + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + menutrackfinished = menu.findItem(R.id.action_track_finished); + menutrackfinished.setVisible(!GPSApp.getCurrentTrack().getName().equals("")); + return true; + } + + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + + /* if (id == R.id.action_settings) { + GPSApp.setHandlerTimer(60000); + // Intent intent = new Intent(this, SettingsActivity.class); + // startActivity(intent); + return true; + } */ + if (id == R.id.action_track_finished) { + if (GPSApp.getNewTrackFlag()) { + // This is the second click + GPSApp.setNewTrackFlag(false); + GPSApp.setRecording(false); + EventBus.getDefault().post(EventBusMSG.NEW_TRACK); + ToastClickAgain.cancel(); + Toast.makeText(this, getString(R.string.toast_track_saved_into_tracklist), Toast.LENGTH_SHORT).show(); + } else { + // This is the first click + GPSApp.setNewTrackFlag(true); // Start the timer + ToastClickAgain.show(); + } + return true; + } + /* if (id == R.id.action_about) { + // Show About Dialog + FragmentManager fm = getSupportFragmentManager(); + FragmentAboutDialog aboutDialog = new FragmentAboutDialog(); + aboutDialog.show(fm, ""); + return true; + } */ + /* if (id == R.id.action_shutdown) { + ShutdownApp(); + return true; + }*/ + return super.onOptionsItemSelected(item); + } + + + private void updateBottomSheetPosition() { + activeTab = tabLayout.getSelectedTabPosition(); + if (activeTab != 2) { + mBottomSheetBehavior.setPeekHeight(1); + mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); + //Log.w("myApp", "[#] GPSActivity.java - mBottomSheetBehavior.setPeekHeight(" + bottomSheet.getHeight() +");"); + mBottomSheetBehavior.setPeekHeight(bottomSheet.getHeight()); + } else { + mBottomSheetBehavior.setPeekHeight(1); + mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED) ; + } + } + + + private void setupViewPager(ViewPager viewPager) { + ViewPagerAdapter adapter = new ViewPagerAdapter(getSupportFragmentManager()); + adapter.addFragment(new FragmentGPSFix(), getString(R.string.tab_gpsfix1)); + //adapter.addFragment(new FragmentTrack(), getString(R.string.tab_track)); + //adapter.addFragment(new FragmentTracklist(), getString(R.string.tab_tracklist)); + viewPager.setAdapter(adapter); + } + + class ViewPagerAdapter extends FragmentPagerAdapter { + private final List mFragmentList = new ArrayList<>(); + private final List mFragmentTitleList = new ArrayList<>(); + + public ViewPagerAdapter(FragmentManager manager) { + super(manager); + } + + @Override + public Fragment getItem(int position) { + return mFragmentList.get(position); + } + + @Override + public int getCount() { + return mFragmentList.size(); + } + + public void addFragment(Fragment fragment, String title) { + mFragmentList.add(fragment); + mFragmentTitleList.add(title); + } + + @Override + public CharSequence getPageTitle(int position) { + return mFragmentTitleList.get(position); + } + } + /* + @Subscribe + public void onEvent(EventBusMSGNormal msg) { + switch (msg.MSGType) { + case EventBusMSG.TRACKLIST_SELECT: + case EventBusMSG.TRACKLIST_DESELECT: + ActivateActionModeIfNeeded(); + } + } + */ + @Subscribe + public void onEvent(Short msg) { + switch (msg) { + case EventBusMSG.REQUEST_ADD_PLACEMARK: + // Show Placemark Dialog + FragmentManager fm = getSupportFragmentManager(); + //FragmentPlacemarkDialog placemarkDialog = new FragmentPlacemarkDialog(); + //placemarkDialog.show(fm, ""); + break; + case EventBusMSG.UPDATE_TRACKLIST: + case EventBusMSG.NOTIFY_TRACKS_DELETED: + ActivateActionModeIfNeeded(); + break; + case EventBusMSG.APPLY_SETTINGS: + LoadPreferences(); + break; + + case EventBusMSG.UPDATE_TRACK: + runOnUiThread(new Runnable() { + @Override + public void run() { + if (menutrackfinished != null) + menutrackfinished.setVisible(!GPSApp.getCurrentTrack().getName().equals("")); + } + }); + break; + case EventBusMSG.TOAST_TRACK_EXPORTED: + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(context, getString(R.string.toast_track_exported), Toast.LENGTH_LONG).show(); + } + }); + break; + case EventBusMSG.TOAST_STORAGE_PERMISSION_REQUIRED: + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(context, getString(R.string.please_grant_storage_permission), Toast.LENGTH_LONG).show(); + } + }); + break; + case EventBusMSG.TOAST_UNABLE_TO_WRITE_THE_FILE: + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(context, getString(R.string.export_unable_to_write_file), Toast.LENGTH_LONG).show(); + } + }); + } + } + + private void LoadPreferences() { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); + prefKeepScreenOn = preferences.getBoolean("prefKeepScreenOn", true); + if (prefKeepScreenOn) getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + else getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + private void ShutdownApp() + { + if ((GPSApp.getCurrentTrack().getNumberOfLocations() > 0) + || (GPSApp.getCurrentTrack().getNumberOfPlacemarks() > 0) + || (GPSApp.getRecording()) + || (GPSApp.getPlacemarkRequest())) { + + AlertDialog.Builder builder = new AlertDialog.Builder(new ContextThemeWrapper(this, R.style.StyledDialog)); + builder.setMessage(getResources().getString(R.string.message_exit_finalizing)); + builder.setIcon(android.R.drawable.ic_menu_info_details); + builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + GPSApp.setRecording(false); + GPSApp.setPlacemarkRequest(false); + EventBus.getDefault().post(EventBusMSG.NEW_TRACK); + GPSApp.StopAndUnbindGPSService(); + GPSApp.setLocationPermissionChecked(false); + + dialog.dismiss(); + GPSApp.setJustStarted(true); + finish(); + } + }); + builder.setNeutralButton(R.string.cancel, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dialog.dismiss(); + } + }); + builder.setNegativeButton(R.string.no, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + GPSApp.setRecording(false); + GPSApp.setPlacemarkRequest(false); + GPSApp.StopAndUnbindGPSService(); + GPSApp.setLocationPermissionChecked(false); + + dialog.dismiss(); + GPSApp.setJustStarted(true); + finish(); + } + }); + AlertDialog dialog = builder.create(); + dialog.show(); + } else { + GPSApp.setRecording(false); + GPSApp.setPlacemarkRequest(false); + GPSApp.StopAndUnbindGPSService(); + GPSApp.setLocationPermissionChecked(false); + + finish(); + } + } + + private void ActivateActionModeIfNeeded() { + runOnUiThread(new Runnable() { + @Override + public void run() { + if ((GPSApp.getNumberOfSelectedTracks() > 0) && (activeTab == 2)) { + if (actionMode == null) + // actionMode = (startSupportActionMode(new ToolbarActionMode())); + actionMode.setTitle(GPSApp.getNumberOfSelectedTracks() > 1 ? String.valueOf(GPSApp.getNumberOfSelectedTracks()) : ""); + } else if (actionMode != null) { + actionMode.finish(); + actionMode = null; + } + } + }); + } + + + public boolean CheckLocationPermission() { + Log.w("myApp", "[#] GPSActivity.java - Check Location Permission..."); + if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { + Log.w("myApp", "[#] GPSActivity.java - Location Permission granted"); + return true; // Permission Granted + } else { + Log.w("myApp", "[#] GPSActivity.java - Location Permission denied"); + boolean showRationale = ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.ACCESS_FINE_LOCATION); + if (showRationale || !GPSApp.isLocationPermissionChecked()) { + Log.w("myApp", "[#] GPSActivity.java - Location Permission denied, need new check"); + List listPermissionsNeeded = new ArrayList<>(); + listPermissionsNeeded.add(Manifest.permission.ACCESS_FINE_LOCATION); + ActivityCompat.requestPermissions(this, listPermissionsNeeded.toArray(new String[listPermissionsNeeded.size()]) , REQUEST_ID_MULTIPLE_PERMISSIONS); + } + return false; + } + } + + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) { + switch (requestCode) { + case REQUEST_ID_MULTIPLE_PERMISSIONS: { + Map perms = new HashMap<>(); + + if (grantResults.length > 0) { + // Fill with actual results from user + for (int i = 0; i < permissions.length; i++) perms.put(permissions[i], grantResults[i]); + // Check for permissions + if (perms.containsKey(Manifest.permission.ACCESS_FINE_LOCATION)) { + if (perms.get(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { + Log.w("myApp", "[#] GPSActivity.java - ACCESS_FINE_LOCATION = PERMISSION_GRANTED; setGPSLocationUpdates!"); + GPSApp.setGPSLocationUpdates(false); + GPSApp.setGPSLocationUpdates(true); + GPSApp.updateGPSLocationFrequency(); + } else { + Log.w("myApp", "[#] GPSActivity.java - ACCESS_FINE_LOCATION = PERMISSION_DENIED"); + } + } + + if (perms.containsKey(Manifest.permission.INTERNET)) { + if (perms.get(Manifest.permission.INTERNET) == PackageManager.PERMISSION_GRANTED) { + Log.w("myApp", "[#] GPSActivity.java - INTERNET = PERMISSION_GRANTED"); + } else { + Log.w("myApp", "[#] GPSActivity.java - INTERNET = PERMISSION_DENIED"); + } + } + + if (perms.containsKey(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + if (perms.get(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { + Log.w("myApp", "[#] GPSActivity.java - WRITE_EXTERNAL_STORAGE = PERMISSION_GRANTED"); + // ---------------------------------------------------- Create the Directories if not exist + File sd = new File(Environment.getExternalStorageDirectory() + "/GPSLogger"); + if (!sd.exists()) { + sd.mkdir(); + } + sd = new File(Environment.getExternalStorageDirectory() + "/GPSLogger/AppData"); + if (!sd.exists()) { + sd.mkdir(); + } + sd = new File(getApplicationContext().getFilesDir() + "/Thumbnails"); + if (!sd.exists()) { + sd.mkdir(); + } + EGM96 egm96 = EGM96.getInstance(); + if (egm96 != null) { + if (!egm96.isEGMGridLoaded()) { + //Log.w("myApp", "[#] GPSApplication.java - Loading EGM Grid..."); + egm96.LoadGridFromFile(Environment.getExternalStorageDirectory() + "/GPSLogger/AppData/WW15MGH.DAC", getApplicationContext().getFilesDir() + "/WW15MGH.DAC"); + } + } + + if (GPSApp.getJobsPending() > 0) GPSApp.ExecuteJob(); + + } else { + Log.w("myApp", "[#] GPSActivity.java - WRITE_EXTERNAL_STORAGE = PERMISSION_DENIED"); + if (GPSApp.getJobsPending() > 0) { + // Shows toast "Unable to write the file" + show_toast_grant_storage_permission = true; + EventBus.getDefault().post(EventBusMSG.TOAST_STORAGE_PERMISSION_REQUIRED); + GPSApp.setJobsPending(0); + } + } + } + } + break; + } + default: { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/Satellite/GPSActivity_sat.java b/app/src/main/java/Satellite/GPSActivity_sat.java new file mode 100644 index 0000000..6a29428 --- /dev/null +++ b/app/src/main/java/Satellite/GPSActivity_sat.java @@ -0,0 +1,133 @@ +package Satellite; + + +import android.Manifest; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.location.Address; +import android.location.Geocoder; +import android.location.Location; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.ActivityCompat; +import android.support.v7.app.AppCompatActivity; +import android.text.Html; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; + +import com.google.android.gms.location.FusedLocationProviderClient; +import com.google.android.gms.location.LocationServices; +import com.google.android.gms.tasks.OnCompleteListener; +import com.google.android.gms.tasks.Task; + +import java.io.IOException; +import java.util.List; +import java.util.Locale; + +import fr.geolabs.dev.mapmint4me.R; + +public class GPSActivity_sat extends AppCompatActivity { + + Button btLocation,btLocation1; + TextView textView1, textView2, textView3, textView4, textView5; + FusedLocationProviderClient fusedLocationProviderClient; + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.layout); + + btLocation = findViewById(R.id.bt_location); + btLocation1 = findViewById(R.id.bt_location1); + textView1 = findViewById(R.id.text_view1); + textView2 = findViewById(R.id.text_view2); + textView3 = findViewById(R.id.text_view3); + textView4 = findViewById(R.id.text_view4); + textView5 = findViewById(R.id.text_view5); + + fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(this); + + btLocation.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + + if (ActivityCompat.checkSelfPermission(GPSActivity_sat.this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { + getLocation(); + } else { + ActivityCompat.requestPermissions(GPSActivity_sat.this, + new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, 44); + } + + } + }); + + btLocation1.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(GPSActivity_sat.this, GPSActivity.class); + startActivity(intent); + } + }); + + } + + private void getLocation() { + + if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { + // TODO: Consider calling + // ActivityCompat#requestPermissions + // here to request the missing permissions, and then overriding + // public void onRequestPermissionsResult(int requestCode, String[] permissions, + // int[] grantResults) + // to handle the case where the user grants the permission. See the documentation + // for ActivityCompat#requestPermissions for more details. + return; + } + fusedLocationProviderClient.getLastLocation().addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + Location location = task.getResult(); + if (location != null) { + try { + Geocoder geocoder = new Geocoder(GPSActivity_sat.this, + Locale.getDefault()); + + List
    addresses = geocoder.getFromLocation( + location.getLatitude(), location.getLongitude(), 1 + ); + textView1.setText(Html.fromHtml( + "lattitude:
    " + + addresses.get(0).getLatitude() + )); + + textView2.setText(Html.fromHtml( + "longitude:
    " + + addresses.get(0).getLongitude() + )); + + textView3.setText(Html.fromHtml( + "Country Name:
    " + + addresses.get(0).getCountryName() + )); + + textView4.setText(Html.fromHtml( + "Locality:
    " + + addresses.get(0).getLocality() + )); + + textView5.setText(Html.fromHtml( + "Address:
    " + + addresses.get(0).getAddressLine(0) + )); + + + } catch (IOException e) { + } + } + } + }); + + } +} \ No newline at end of file diff --git a/app/src/main/java/Satellite/GPSApplication.java b/app/src/main/java/Satellite/GPSApplication.java new file mode 100644 index 0000000..75bd16e --- /dev/null +++ b/app/src/main/java/Satellite/GPSApplication.java @@ -0,0 +1,1592 @@ + +package Satellite; + +import android.Manifest; +import android.app.Application; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.content.ActivityNotFoundException; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.location.GpsSatellite; +import android.location.GpsStatus; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.location.LocationProvider; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.os.IBinder; +import android.os.StrictMode; +import android.support.v4.content.ContextCompat; +import android.support.v7.app.AppCompatDelegate; +import android.support.v7.preference.PreferenceManager; +import android.util.Log; + +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; + +import java.io.File; +import java.io.FileOutputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.StringTokenizer; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +import fr.geolabs.dev.mapmint4me.R; + + + +public class GPSApplication extends Application implements GpsStatus.Listener, LocationListener { + + //private static final float M_TO_FT = 3.280839895f; + private static final int NOT_AVAILABLE = -100000; + + //private static final int UM_METRIC_MS = 0; + private static final int UM_METRIC_KMH = 1; + //private static final int UM_IMPERIAL_FPS = 8; + //private static final int UM_IMPERIAL_MPH = 9; + + private static final int STABILIZERVALUE = 3000; // The application discards fixes for 3000 ms (minimum) + private static final int DEFAULTHANDLERTIMER = 5000; // The timer for turning off GPS on exit + private static final int GPSUNAVAILABLEHANDLERTIMER = 7000; // The "GPS temporary unavailable" timer + private int StabilizingSamples = 3; + + private static final int GPS_DISABLED = 0; + private static final int GPS_OUTOFSERVICE = 1; + private static final int GPS_TEMPORARYUNAVAILABLE = 2; + private static final int GPS_SEARCHING = 3; + private static final int GPS_STABILIZING = 4; + private static final int GPS_OK = 5; + + public static final int APP_ORIGIN_NOT_SPECIFIED = 0; + public static final int APP_ORIGIN_GOOGLE_PLAY_STORE = 1; // The app is installed via the Google Play Store + + public static final int JOB_TYPE_NONE = 0; // No operation + public static final int JOB_TYPE_EXPORT = 1; // Bulk Exportation + public static final int JOB_TYPE_VIEW = 2; // Bulk View + public static final int JOB_TYPE_SHARE = 3; // Bulk Share + public static final int JOB_TYPE_DELETE = 4; // Bulk Delete + + public static final String FLAG_RECORDING = "flagRecording"; // The persistent Flag is set when the app is recording, in order to detect Background Crashes + + + // Preferences Variables + // private boolean prefKeepScreenOn = true; // DONE in GPSActivity + private boolean prefShowDecimalCoordinates = false; + private int prefViewTracksWith = 0; + private int prefUM = UM_METRIC_KMH; + private float prefGPSdistance = 0f; + private long prefGPSupdatefrequency = 1000L; + private boolean prefEGM96AltitudeCorrection = false; + private double prefAltitudeCorrection = 0d; + private boolean prefExportKML = true; + private boolean prefExportGPX = true; + private int prefGPXVersion = 100; // the version of the GPX schema + private boolean prefExportTXT = false; + private int prefKMLAltitudeMode = 0; + private int prefShowTrackStatsType = 0; + private int prefShowDirections = 0; + private boolean prefGPSWeekRolloverCorrected= false; + + private boolean LocationPermissionChecked = false; // If the flag is false the GPSActivity will check for Location Permission + private boolean isFirstRun = false; // True if it is the first run of the app (the DB is empty) + private boolean isJustStarted = true; // True if the application has just been started + private boolean isMockProvider = false; // True if the location is from mock provider + + private LocationExtended PrevFix = null; + private boolean isPrevFixRecorded = false; + + private LocationExtended PrevRecordedFix = null; + + private boolean MustUpdatePrefs = true; // True if preferences needs to be updated + + private boolean isCurrentTrackVisible = false; + private boolean isContextMenuShareVisible = false; // True if "Share with ..." menu is visible + private boolean isContextMenuViewVisible = false; // True if "View in *" menu is visible + private String ViewInApp = ""; // The string of default app name for "View" + private String _satelliteList = ""; + // "" in case of selector + + // Singleton instance + private static GPSApplication singleton; + public static GPSApplication getInstance(){ + return singleton; + } + + + DatabaseHandler GPSDataBase; + private String PlacemarkDescription = ""; + private boolean Recording = false; + private boolean PlacemarkRequest = false; + private boolean isGPSLocationUpdatesActive = false; + private int GPSStatus = GPS_SEARCHING; + + private int AppOrigin = APP_ORIGIN_NOT_SPECIFIED; // Which package manager is used to install this app + + private boolean NewTrackFlag = false; // The variable that handle the double-click on "Track Finished" + final Handler newtrackhandler = new Handler(); + Runnable newtrackr = new Runnable() { + @Override + public void run() { + NewTrackFlag = false; + } + }; + + private boolean LocationSettingsFlag = false; // The variable that handle the double-click on "Open Location Settings" + final Handler locationsettingshandler = new Handler(); + Runnable locationsettingsr = new Runnable() { + @Override + public void run() { + LocationSettingsFlag = false; + } + }; + + private LocationManager mlocManager = null; // GPS LocationManager + private int _NumberOfSatellites = 0; + private int _NumberOfSatellitesUsedInFix = 0; + + private int GPSActivity_activeTab = 0; // The active tab on GPSActivity + private int JobProgress = 0; + private int JobsPending = 0; // The number of jobs to be done + public int JobType = JOB_TYPE_NONE; // The type of job that is pending + private boolean DeleteAlsoExportedFiles = false; // When true, the deletion of some tracks will delete also the exported files of the tracks + + public int GPSActivityColorTheme; + + private int _Stabilizer = StabilizingSamples; + private int HandlerTimer = DEFAULTHANDLERTIMER; + + private LocationExtended _currentLocationExtended = null; + private LocationExtended _currentPlacemark = null; + private Track _currentTrack = null; + private List _ArrayListTracks = Collections.synchronizedList(new ArrayList()); + + Thumbnailer Th; + //Exporter Ex; + private AsyncUpdateThreadClass asyncUpdateThread = new AsyncUpdateThreadClass(); + + // The handler that switches off the location updates after a time delay: + final Handler handler = new Handler(); + Runnable r = new Runnable() { + + @Override + public void run() { + setGPSLocationUpdates(false); + } + }; + + final Handler gpsunavailablehandler = new Handler(); + Runnable unavailr = new Runnable() { + + @Override + public void run() { + if ((GPSStatus == GPS_OK) || (GPSStatus == GPS_STABILIZING)) { + GPSStatus = GPS_TEMPORARYUNAVAILABLE; + EventBus.getDefault().post(EventBusMSG.UPDATE_FIX); + } + } + }; + + private final int MAX_ACTIVE_EXPORTER_THREADS = 3; // The maximum number of Exporter threads to run simultaneously + + private List ExportingTaskList = new ArrayList<>(); + + + // The handler that checks the progress of an exportation: + private final int ExportingStatusCheckInterval = 16; // The app updates the progress of exportation every 16 milliseconds + final Handler ExportingStatusCheckHandler = new Handler(); + + Runnable ExportingStatusChecker = new Runnable() { + @Override + public void run() { + long Total = 0; + long Progress = 0; + int Exporters_Total = ExportingTaskList.size(); // The total amount of exportation into the current job + int Exporters_Pending = 0; + int Exporters_Running = 0; // The amount of exportation in progress + int Exporters_Success = 0; // The amount of exportation finished with success + int Exporters_Failed = 0; // The amount of exportation failed + + + // Check Progress + for (ExportingTask ET : ExportingTaskList) { + Total += ET.getNumberOfPoints_Total(); + Progress += ET.getNumberOfPoints_Processed(); + if (ET.getStatus() == ExportingTask.STATUS_PENDING) Exporters_Pending++; + if (ET.getStatus() == ExportingTask.STATUS_RUNNING) Exporters_Running++; + if (ET.getStatus() == ExportingTask.STATUS_ENDED_SUCCESS) Exporters_Success++; + if (ET.getStatus() == ExportingTask.STATUS_ENDED_FAILED) Exporters_Failed++; + } + + // Update job progress + if (Total != 0) { + if (JobProgress != (int) Math.round(1000L * Progress / Total)) { // The ProgressBar on FragmentJobProgress has android:max="1000" + JobProgress = (int) Math.round(1000L * Progress / Total); + EventBus.getDefault().post(EventBusMSG.UPDATE_JOB_PROGRESS); + } + } else { + if (JobProgress != 0) { + JobProgress = 0; + EventBus.getDefault().post(EventBusMSG.UPDATE_JOB_PROGRESS); + } + } + + //Log.w("myApp", "[#] GPSApplication.java - ExportingStatusChecker running: " + 100*Progress/Total + "% - P " + // + Exporters_Pending + " - R " + Exporters_Running + " - S " + Exporters_Success + " - F " + Exporters_Failed); + + // Exportation Failed + if (Exporters_Failed != 0) { + EventBus.getDefault().post(EventBusMSG.TOAST_UNABLE_TO_WRITE_THE_FILE); + JobProgress = 0; + JobsPending = 0; + EventBus.getDefault().post(EventBusMSG.UPDATE_JOB_PROGRESS); + return; + } + + // Exportation Finished + if (Exporters_Success == Exporters_Total) { + if (JobType == JOB_TYPE_VIEW) { + if (!ExportingTaskList.isEmpty()) ViewTrack(ExportingTaskList.get(0)); + } else if (JobType == JOB_TYPE_SHARE) { + EventBus.getDefault().post(EventBusMSG.INTENT_SEND); + } else { + EventBus.getDefault().post(EventBusMSG.TOAST_TRACK_EXPORTED); + } + JobProgress = 0; + JobsPending = 0; + EventBus.getDefault().post(EventBusMSG.UPDATE_JOB_PROGRESS); + return; + } + + // If needed, run another Exportation Thread + if ((Exporters_Running < MAX_ACTIVE_EXPORTER_THREADS) && (Exporters_Pending > 0)) { + for (ExportingTask ET : ExportingTaskList) { + if (ET.getStatus() == ExportingTask.STATUS_PENDING) { + //Log.w("myApp", "[#] GPSApplication.java - Run the export thread nr." + Exporters_Running + ": " + ET.getId()); + ET.setStatus(ExportingTask.STATUS_RUNNING); + //ExecuteExportingTask(ET); + break; + } + } + } + + ExportingStatusCheckHandler.postDelayed(ExportingStatusChecker, ExportingStatusCheckInterval); + } + }; + + void startExportingStatusChecker() { + ExportingStatusChecker.run(); + } + + void stopExportingStatusChecker() { + ExportingStatusCheckHandler.removeCallbacks(ExportingStatusChecker); + } + + + // ------------------------------------------------------------------------------------ Service + Intent GPSServiceIntent; + GPSService GPSLoggerService; + boolean isGPSServiceBound = false; + + private ServiceConnection GPSServiceConnection = new ServiceConnection() { + + @Override + public void onServiceConnected(ComponentName className, + IBinder service) { + GPSService.LocalBinder binder = (GPSService.LocalBinder) service; + GPSLoggerService = binder.getServiceInstance(); //Get instance of your service! + Log.w("myApp", "[#] GPSApplication.java - GPSSERVICE CONNECTED - onServiceConnected event"); + isGPSServiceBound = true; + } + + @Override + public void onServiceDisconnected(ComponentName arg0) { + Log.w("myApp", "[#] GPSApplication.java - GPSSERVICE DISCONNECTED - onServiceDisconnected event"); + isGPSServiceBound = false; + } + }; + + private void StartAndBindGPSService() { + GPSServiceIntent = new Intent(GPSApplication.this, GPSService.class); + //Start the service + startService(GPSServiceIntent); + //Bind to the service + if (Build.VERSION.SDK_INT >= 14) + bindService(GPSServiceIntent, GPSServiceConnection, Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT); + else + bindService(GPSServiceIntent, GPSServiceConnection, Context.BIND_AUTO_CREATE); + Log.w("myApp", "[#] GPSApplication.java - StartAndBindGPSService"); + } + + + /* private void UnbindGPSService() { //UNUSED + try { + unbindService(GPSServiceConnection); //Unbind to the service + Log.w("myApp", "[#] GPSApplication.java - Service unbound"); + } catch (Exception e) { + Log.w("myApp", "[#] GPSApplication.java - Unable to unbind the GPSService"); + } + } */ + + public void StopAndUnbindGPSService() { + try { + unbindService(GPSServiceConnection); //Unbind to the service + Log.w("myApp", "[#] GPSApplication.java - Service unbound"); + } catch (Exception e) { + Log.w("myApp", "[#] GPSApplication.java - Unable to unbind the GPSService"); + } + try { + stopService(GPSServiceIntent); //Stop the service + Log.w("myApp", "[#] GPSApplication.java - Service stopped"); + } catch (Exception e) { + Log.w("myApp", "[#] GPSApplication.java - Unable to stop GPSService"); + } + } + + + // ------------------------------------------------------------------------ Getters and Setters + public boolean getNewTrackFlag() { + return NewTrackFlag; + } + + public void setNewTrackFlag(boolean newTrackFlag) { + if (newTrackFlag) { + NewTrackFlag = true; + newtrackhandler.removeCallbacks(newtrackr); // Cancel the previous newtrackr handler + newtrackhandler.postDelayed(newtrackr, 1500); // starts the new handler + } else { + NewTrackFlag = false; + newtrackhandler.removeCallbacks(newtrackr); // Cancel the previous newtrackr handler + } + } + + public boolean getLocationSettingsFlag() { + return LocationSettingsFlag; + } + + public void setLocationSettingsFlag(boolean locationSettingsFlag) { + if (locationSettingsFlag) { + LocationSettingsFlag = true; + locationsettingshandler.removeCallbacks(locationsettingsr); // Cancel the previous locationsettingsr handler + locationsettingshandler.postDelayed(locationsettingsr, 1000); // starts the new handler + } else { + LocationSettingsFlag = false; + locationsettingshandler.removeCallbacks(locationsettingsr); // Cancel the previous locationsettingsr handler + } + } + + public boolean isLocationPermissionChecked() { + return LocationPermissionChecked; + } + + public void setLocationPermissionChecked(boolean locationPermissionChecked) { + LocationPermissionChecked = locationPermissionChecked; + } + + public void setHandlerTimer(int handlerTimer) { + HandlerTimer = handlerTimer; + } + + public int getHandlerTimer() { + return HandlerTimer; + } + + public int getGPSStatus() { + return GPSStatus; + } + + + + public double getPrefAltitudeCorrection() { + return prefAltitudeCorrection; + } + + public boolean getPrefEGM96AltitudeCorrection() { + return prefEGM96AltitudeCorrection; + } + + public boolean getPrefShowDecimalCoordinates() { + return prefShowDecimalCoordinates; + } + + + public int getPrefUM() { + return prefUM; + } + + public int getPrefShowTrackStatsType() { + return prefShowTrackStatsType; + } + + public int getPrefShowDirections() { + return prefShowDirections; + } + + public LocationExtended getCurrentLocationExtended() { + return _currentLocationExtended; + } + + public void setPlacemarkDescription(String Description) { + this.PlacemarkDescription = Description; + } + + public Track getCurrentTrack() { + return _currentTrack; + } + + public int getNumberOfSatellites() { + return _NumberOfSatellites; + } + + public String get_satelliteList() { + return _satelliteList; + } + + public int getNumberOfSatellitesUsedInFix() { + return _NumberOfSatellitesUsedInFix; + } + + public boolean getRecording() { + return Recording; + } + + public void setRecording(boolean recordingState) { + PrevRecordedFix = null; + Recording = recordingState; + if (Recording) FlagAdd(FLAG_RECORDING); + else FlagRemove(FLAG_RECORDING); + } + + public boolean getPlacemarkRequest() { return PlacemarkRequest; } + + public void setPlacemarkRequest(boolean placemarkRequest) { PlacemarkRequest = placemarkRequest; } + + public List getTrackList() { + return _ArrayListTracks; + } + + public boolean isCurrentTrackVisible() { + return isCurrentTrackVisible; + } + + public void setisCurrentTrackVisible(boolean currentTrackVisible) { + isCurrentTrackVisible = currentTrackVisible; + } + + public int getAppOrigin() { + return AppOrigin; + } + + public int getJobProgress() { + return JobProgress; + } + + public int getJobsPending() { + return JobsPending; + } + + public void setJobsPending(int jobsPending) { + JobsPending = jobsPending; + } + + public int getGPSActivity_activeTab() { + return GPSActivity_activeTab; + } + + public void setGPSActivity_activeTab(int GPSActivity_activeTab) { + System.out.println(GPSActivity_activeTab); + this.GPSActivity_activeTab = 1; + } + + public List getExportingTaskList() { + return ExportingTaskList; + } + + public void setDeleteAlsoExportedFiles(boolean deleteAlsoExportedFiles) { + DeleteAlsoExportedFiles = deleteAlsoExportedFiles; + } + + public boolean isJustStarted() { + return isJustStarted; + } + + public void setJustStarted(boolean justStarted) { + isJustStarted = justStarted; + } + + // ------------------------------------------------------------------------ Utility + + private void DeleteFile(String filename) { + File file = new File(filename); + boolean deleted; + if (file.exists ()) { + deleted = file.delete(); + if (deleted) Log.w("myApp", "[#] GPSApplication.java - DeleteFile: " + filename + " deleted"); + else Log.w("myApp", "[#] GPSApplication.java - DeleteFile: " + filename + " unable to delete the File"); + } + else Log.w("myApp", "[#] GPSApplication.java - DeleteFile: " + filename + " doesn't exists"); + } + + + /* NOT USED, Commented out + private boolean FileExists(String filename) { + File file = new File(filename); + return file.exists (); + } */ + + + // Flags are Boolean SharedPreferences that are excluded by automatic Backups + + public void FlagAdd (String flag) { + SharedPreferences preferences_nobackup = getSharedPreferences("prefs_nobackup",Context.MODE_PRIVATE); + SharedPreferences.Editor editor = preferences_nobackup.edit(); + editor.putBoolean(flag, true); + editor.commit(); + } + + + public void FlagRemove (String flag) { + SharedPreferences preferences_nobackup = getSharedPreferences("prefs_nobackup",Context.MODE_PRIVATE); + SharedPreferences.Editor editor = preferences_nobackup.edit(); + editor.remove(flag); + editor.commit(); + } + + + public boolean FlagExists (String flag) { + SharedPreferences preferences_nobackup = getSharedPreferences("prefs_nobackup",Context.MODE_PRIVATE); + return preferences_nobackup.getBoolean(flag, false); + } + + + // -------------------------------------------------------------------------------------------- + + @Override + public void onCreate() { + + AppCompatDelegate.setDefaultNightMode(Integer.valueOf(PreferenceManager.getDefaultSharedPreferences(getApplicationContext()).getString("prefColorTheme", "2"))); + + super.onCreate(); + + singleton = this; + + // work around the android.os.FileUriExposedException + StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder(); + StrictMode.setVmPolicy(builder.build()); + + final String CHANNEL_ID = "GPSLoggerServiceChannel"; + + // Create notification channel for Android >= O + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel( + CHANNEL_ID, + getString(R.string.app_name), + NotificationManager.IMPORTANCE_LOW + ); + channel.setSound(null, null); + channel.enableLights(false); + channel.enableVibration(false); + channel.setSound(null,null); + + NotificationManager manager = getSystemService(NotificationManager.class); + manager.createNotificationChannel(channel); + } + + // ----------------------- + // TODO: Uncomment it to run the Week Rollover Tests (For Test Purpose) + // SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()).edit(); + // editor.putBoolean("prefGPSWeekRolloverCorrected", false); + // editor.commit(); + // ----------------------- + + // ----------------------- + // TODO: Uncomment it to reload the EGM Grid File (For Test Purpose) + //File file = new File(getApplicationContext().getFilesDir() + "/WW15MGH.DAC"); + //if (file.exists ()) file.delete(); + // ----------------------- + + EventBus.getDefault().register(this); + + mlocManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE); // Location Manager + + File sd = new File(Environment.getExternalStorageDirectory() + "/GPSLogger"); // Create the Directories if not exist + if (!sd.exists()) { + sd.mkdir(); + Log.w("myApp", "[#] GPSApplication.java - Folder created: " + sd.getAbsolutePath()); + } + sd = new File(Environment.getExternalStorageDirectory() + "/GPSLogger/AppData"); + if (!sd.exists()) { + sd.mkdir(); + Log.w("myApp", "[#] GPSApplication.java - Folder created: " + sd.getAbsolutePath()); + } + + sd = new File(getApplicationContext().getFilesDir() + "/Thumbnails"); + if (!sd.exists()) { + sd.mkdir(); + Log.w("myApp", "[#] GPSApplication.java - Folder created: " + sd.getAbsolutePath()); + } + + EGM96 egm96 = EGM96.getInstance(); // Load EGM Grid + if (egm96 != null) { + if (!egm96.isEGMGridLoaded()) { + egm96.LoadGridFromFile(Environment.getExternalStorageDirectory() + "/GPSLogger/AppData/WW15MGH.DAC", getApplicationContext().getFilesDir() + "/WW15MGH.DAC"); + } + } + + try { // Determine the app installation source + String installer; + installer = getApplicationContext().getPackageManager().getInstallerPackageName(getApplicationContext().getPackageName()); + if (installer.equals("com.android.vending") || installer.equals("com.google.android.feedback")) + AppOrigin = APP_ORIGIN_GOOGLE_PLAY_STORE; // App installed from Google Play Store + else AppOrigin = APP_ORIGIN_NOT_SPECIFIED; // Otherwise + } catch (Throwable e) { + Log.w("myApp", "[#] GPSApplication.java - Exception trying to determine the package installer"); + AppOrigin = APP_ORIGIN_NOT_SPECIFIED; + } + + GPSDataBase = new DatabaseHandler(this); // Initialize the Database + + // Prepare the current track + if (GPSDataBase.getLastTrackID() == 0) { + GPSDataBase.addTrack(new Track()); // Creation of the first track if the DB is empty + isFirstRun = true; + } + _currentTrack = GPSDataBase.getLastTrack(); // Get the last track + + LoadPreferences(); // Load Settings + + // ---------------------------------------------------------------------------------------- + + asyncUpdateThread.start(); + + // Get max available VM memory, exceeding this amount will throw an + // OutOfMemory exception. Stored in kilobytes as LruCache takes an + // int in its constructor. + //Log.w("myApp", "[#] GPSApplication.java - Max available VM memory = " + (int) (Runtime.getRuntime().maxMemory() / 1024) + " kbytes"); + } + + + @Override + public void onTerminate() { + Log.w("myApp", "[#] GPSApplication.java - onTerminate"); + EventBus.getDefault().unregister(this); + StopAndUnbindGPSService(); + super.onTerminate(); + } + + + @Subscribe + public void onEvent(Short msg) { + if (msg == EventBusMSG.NEW_TRACK) { + AsyncTODO ast = new AsyncTODO(); + ast.TaskType = "TASK_NEWTRACK"; + ast.location = null; + AsyncTODOQueue.add(ast); + return; + } + if (msg == EventBusMSG.ADD_PLACEMARK) { + AsyncTODO ast = new AsyncTODO(); + ast.TaskType = "TASK_ADDPLACEMARK"; + ast.location = _currentPlacemark; + _currentPlacemark.setDescription(PlacemarkDescription); + AsyncTODOQueue.add(ast); + return; + } + if (msg == EventBusMSG.APP_PAUSE) { + handler.postDelayed(r, getHandlerTimer()); // Starts the switch-off handler (delayed by HandlerTimer) + if ((_currentTrack.getNumberOfLocations() == 0) && (_currentTrack.getNumberOfPlacemarks() == 0) + && (!Recording) && (!PlacemarkRequest)) StopAndUnbindGPSService(); + System.gc(); // Clear mem from released objects with Garbage Collector + return; + } + if (msg == EventBusMSG.APP_RESUME) { + //Log.w("myApp", "[#] GPSApplication.java - Received EventBusMSG.APP_RESUME"); + AsyncPrepareTracklistContextMenu asyncPrepareTracklistContextMenu = new AsyncPrepareTracklistContextMenu(); + asyncPrepareTracklistContextMenu.start(); + handler.removeCallbacks(r); // Cancel the switch-off handler + setHandlerTimer(DEFAULTHANDLERTIMER); + setGPSLocationUpdates(true); + if (MustUpdatePrefs) { + MustUpdatePrefs = false; + LoadPreferences(); + } + StartAndBindGPSService(); + return; + } + if (msg == EventBusMSG.UPDATE_SETTINGS) { + MustUpdatePrefs = true; + return; + } + } + + + public void setGPSLocationUpdates (boolean state) { + // Request permissions = https://developer.android.com/training/permissions/requesting.html + if (!state && !getRecording() && isGPSLocationUpdatesActive + && (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED)) { + GPSStatus = GPS_SEARCHING; + mlocManager.removeGpsStatusListener(this); + mlocManager.removeUpdates(this); + isGPSLocationUpdatesActive = false; + //Log.w("myApp", "[#] GPSApplication.java - setGPSLocationUpdates = false"); + } + if (state && !isGPSLocationUpdatesActive + && (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED)) { + mlocManager.addGpsStatusListener(this); + mlocManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, prefGPSupdatefrequency, 0, this); // Requires Location update + isGPSLocationUpdatesActive = true; + //Log.w("myApp", "[#] GPSApplication.java - setGPSLocationUpdates = true"); + if (prefGPSupdatefrequency >= 1000) StabilizingSamples = (int) Math.ceil(STABILIZERVALUE / prefGPSupdatefrequency); + else StabilizingSamples = (int) Math.ceil(STABILIZERVALUE / 1000); + } + } + + public void updateGPSLocationFrequency () { + if (isGPSLocationUpdatesActive + && (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED)) { + //Log.w("myApp", "[#] GPSApplication.java - updateGPSLocationFrequency"); + mlocManager.removeGpsStatusListener(this); + mlocManager.removeUpdates(this); + if (prefGPSupdatefrequency >= 1000) StabilizingSamples = (int) Math.ceil(STABILIZERVALUE / prefGPSupdatefrequency); + else StabilizingSamples = (int) Math.ceil(STABILIZERVALUE / 1000); + mlocManager.addGpsStatusListener(this); + mlocManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, prefGPSupdatefrequency, 0, this); + } + } + + public void updateSats() { + try { + if ((mlocManager != null) && (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED)) { + GpsStatus gs = mlocManager.getGpsStatus(null); + int sats_inview = 0; // Satellites in view; + int sats_used = 0; // Satellites used in fix; + + if (gs != null) { + Iterable sats = gs.getSatellites(); + for (GpsSatellite sat : sats) { + sats_inview++; + + if (sat.usedInFix()) { + sats_used++; + int i =0; + if(i<5){ + + _satelliteList = _satelliteList+ "PRN No:" +sat.getPrn() + ", SNR: " + sat.getSnr() + ", Azimuth " +sat.getAzimuth()+", Elevation:"+sat.getElevation()+"\n"; + Log.d("Satellite_info",_satelliteList); + i = i+1; + + + } + } + //Log.w("myApp", "[#] GPSApplication.java - updateSats: i=" + i); + } + _NumberOfSatellites = sats_inview; + _NumberOfSatellitesUsedInFix = sats_used; + } else { + _NumberOfSatellites = NOT_AVAILABLE; + _NumberOfSatellitesUsedInFix = NOT_AVAILABLE; + } + } else { + _NumberOfSatellites = NOT_AVAILABLE; + _NumberOfSatellitesUsedInFix = NOT_AVAILABLE; + } + } catch (NullPointerException e) { + _NumberOfSatellites = NOT_AVAILABLE; + _NumberOfSatellitesUsedInFix = NOT_AVAILABLE; + //Log.w("myApp", "[#] GPSApplication.java - updateSats: Caught NullPointerException: " + e); + } + //Log.w("myApp", "[#] GPSApplication.java - updateSats: Total=" + _NumberOfSatellites + " Used=" + _NumberOfSatellitesUsedInFix); + } + + + private void ViewTrack(ExportingTask exportingTask) { + if (prefViewTracksWith == 0) { // KML Viewer + File file = new File(Environment.getExternalStorageDirectory() + "/GPSLogger/AppData/", exportingTask.getName() + ".kml"); + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setDataAndType(Uri.fromFile(file), "application/vnd.google-earth.kml+xml"); + try { + startActivity(intent); + } catch (ActivityNotFoundException e) { + Log.w("myApp", "[#] GPSApplication.java - ViewTrack: Unable to view the track: " + e); + AsyncPrepareTracklistContextMenu asyncPrepareTracklistContextMenu = new AsyncPrepareTracklistContextMenu(); + asyncPrepareTracklistContextMenu.start(); + } + } + if (prefViewTracksWith == 1) { // GPX Viewer + File file = new File(Environment.getExternalStorageDirectory() + "/GPSLogger/AppData/", exportingTask.getName() + ".gpx"); + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setDataAndType(Uri.fromFile(file), "gpx+xml"); + try { + startActivity(intent); + } catch (ActivityNotFoundException e) { + Log.w("myApp", "[#] GPSApplication.java - ViewTrack: Unable to view the track: " + e); + AsyncPrepareTracklistContextMenu asyncPrepareTracklistContextMenu = new AsyncPrepareTracklistContextMenu(); + asyncPrepareTracklistContextMenu.start(); + } + } + } + + + public ArrayList getSelectedTracks() { + ArrayList selTracks = new ArrayList(); + synchronized(_ArrayListTracks) { + for (Track T : _ArrayListTracks) { + if (T.isSelected()) { + selTracks.add(T); + } + } + } + return (selTracks); + } + + + public int getNumberOfSelectedTracks() { + int nsel = 0; + synchronized(_ArrayListTracks) { + for (Track T : _ArrayListTracks) { + if (T.isSelected()) nsel++; + } + } + return nsel; + } + + + public void DeselectAllTracks() { + synchronized(_ArrayListTracks) { + for (Track T : _ArrayListTracks) { + if (T.isSelected()) { + T.setSelected(false); + // EventBus.getDefault().post(new EventBusMSGNormal(EventBusMSG.TRACKLIST_DESELECT, T.getId())); + } + } + } + EventBus.getDefault().post(EventBusMSG.REFRESH_TRACKLIST); + } + + + public void LoadJob (int jobType) { + ExportingTaskList.clear(); + synchronized(_ArrayListTracks) { + for (Track T : _ArrayListTracks) { + if (T.isSelected()) { + ExportingTask ET = new ExportingTask(); + ET.setId(T.getId()); + ET.setName(T.getName()); + ET.setNumberOfPoints_Total(T.getNumberOfLocations() + T.getNumberOfPlacemarks()); + ET.setNumberOfPoints_Processed(0); + ExportingTaskList.add(ET); + } + } + } + JobsPending = ExportingTaskList.size(); + JobType = jobType; + } + + /* + public void ExecuteExportingTask (ExportingTask exportingTask) { + switch (JobType) { + case JOB_TYPE_NONE: + case JOB_TYPE_DELETE: + break; + case JOB_TYPE_EXPORT: + Ex = new Exporter(exportingTask, prefExportKML, prefExportGPX, prefExportTXT, Environment.getExternalStorageDirectory() + "/GPSLogger"); + Ex.start(); + break; + case JOB_TYPE_VIEW: + if (prefViewTracksWith == 0) Ex = new Exporter(exportingTask, true, false, false, Environment.getExternalStorageDirectory() + "/GPSLogger/AppData"); + if (prefViewTracksWith == 1) Ex = new Exporter(exportingTask, false, true, false, Environment.getExternalStorageDirectory() + "/GPSLogger/AppData"); + Ex.start(); + break; + case JOB_TYPE_SHARE: + Ex = new Exporter(exportingTask, prefExportKML, prefExportGPX, prefExportTXT, Environment.getExternalStorageDirectory() + "/GPSLogger/AppData"); + Ex.start(); + break; + default: + break; + } + } + */ + public void ExecuteJob () { + if (!ExportingTaskList.isEmpty()) { + switch (JobType) { + case JOB_TYPE_NONE: + break; + case JOB_TYPE_DELETE: + String S = "TASK_DELETE_TRACKS"; + for (ExportingTask ET : ExportingTaskList) { + S = S + " " + ET.getId(); + } + AsyncTODO ast = new AsyncTODO(); + ast.TaskType = S; + ast.location = null; + AsyncTODOQueue.add(ast); + break; + case JOB_TYPE_EXPORT: + case JOB_TYPE_VIEW: + case JOB_TYPE_SHARE: + startExportingStatusChecker(); + break; + default: + break; + } + } else { + Log.w("myApp", "[#] GPSApplication.java - Empty Job, nothing processed"); + JobProgress = 0; + JobsPending = 0; + } + } + + + private class AsyncPrepareTracklistContextMenu extends Thread { + + public AsyncPrepareTracklistContextMenu() { + } + + public void run() { + isContextMenuShareVisible = false; + isContextMenuViewVisible = false; + ViewInApp = ""; + + final PackageManager pm = getPackageManager(); + + // ----- menu share + Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setType("text/xml"); + // Verify the intent will resolve to at least one activity + if ((intent.resolveActivity(pm) != null)) isContextMenuShareVisible = true; + + // ----- menu view + intent = new Intent(Intent.ACTION_VIEW); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setType("application/vnd.google-earth.kml+xml"); + + if (prefViewTracksWith == 0) { // KML Viewer + intent.setType("application/vnd.google-earth.kml+xml"); + } + if (prefViewTracksWith == 1) { // GPX Viewer + intent.setType("application/gpx+xml"); + } + ResolveInfo ri = pm.resolveActivity(intent, 0); // Find default app + if (ri != null) { + //Log.w("myApp", "[#] GPSApplication.java - Open with: " + ri.activityInfo.applicationInfo.loadLabel(getContext().getPackageManager())); + List lri = pm.queryIntentActivities(intent, 0); + //Log.w("myApp", "[#] GPSApplication.java - Found " + lri.size() + " viewers:"); + for (ResolveInfo tmpri : lri) { + //Log.w("myApp", "[#] " + ri.activityInfo.applicationInfo.packageName + " - " + tmpri.activityInfo.applicationInfo.packageName); + if (ri.activityInfo.applicationInfo.packageName.equals(tmpri.activityInfo.applicationInfo.packageName)) { + ViewInApp = ri.activityInfo.applicationInfo.loadLabel(pm).toString(); + //Log.w("myApp", "[#] DEFAULT --> " + tmpri.activityInfo.applicationInfo.loadLabel(getPackageManager())); + } //else Log.w("myApp", "[#] " + tmpri.activityInfo.applicationInfo.loadLabel(getContext().getPackageManager())); + } + isContextMenuViewVisible = true; + } + Log.w("myApp", "[#] GPSApplication.java - Tracklist ContextMenu prepared"); + EventBus.getDefault().post(EventBusMSG.UPDATE_ACTIONBAR); + } + } + + + // ------------------------------------------------------------------------- GpsStatus.Listener + @Override + public void onGpsStatusChanged(final int event) { + switch (event) { + case GpsStatus.GPS_EVENT_SATELLITE_STATUS: + // TODO: get here the status of the GPS, and save into a GpsStatus to be used for satellites visualization; + // Use GpsStatus getGpsStatus (GpsStatus status) + // https://developer.android.com/reference/android/location/LocationManager.html#getGpsStatus(android.location.GpsStatus) + updateSats(); + break; + } + } + + + // --------------------------------------------------------------------------- LocationListener + @Override + public void onLocationChanged(Location loc) { + //if ((loc != null) && (loc.getProvider().equals(LocationManager.GPS_PROVIDER)) { + if (loc != null) { // Location data is valid + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { // For API >= 18 + if ((PrevFix == null) || (loc.isFromMockProvider()!=isMockProvider)) { // Reset the number of satellites when the provider changes between GPS and MOCK + isMockProvider = loc.isFromMockProvider(); + _NumberOfSatellites = NOT_AVAILABLE; + _NumberOfSatellitesUsedInFix = NOT_AVAILABLE; + if (isMockProvider) Log.w("myApp", "[#] GPSApplication.java - Provider Type = MOCK PROVIDER"); + else Log.w("myApp", "[#] GPSApplication.java - Provider Type = GPS PROVIDER"); + } + } + + //Log.w("myApp", "[#] GPSApplication.java - onLocationChanged: provider=" + loc.getProvider()); + if (loc.hasSpeed() && (loc.getSpeed() == 0)) loc.removeBearing(); // Removes bearing if the speed is zero + // --------- Workaround for old GPS that are affected to Week Rollover + //loc.setTime(loc.getTime() - 619315200000L); // Commented out, it simulate the old GPS hardware Timestamp + if (loc.getTime() <= 1388534400000L) // if the Location Time is <= 01/01/2014 00:00:00.000 + loc.setTime(loc.getTime() + 619315200000L); // Timestamp incremented by 1024×7×24×60×60×1000 = 619315200000 ms + // This value must be doubled every 1024 weeks !!! + LocationExtended eloc = new LocationExtended(loc); + eloc.setNumberOfSatellites(getNumberOfSatellites()); + eloc.setNumberOfSatellitesUsedInFix(getNumberOfSatellitesUsedInFix()); + eloc.setSatellite_info(get_satelliteList()); + + // eloc.setSatelliteDescription(get_satelliteList()); + boolean ForceRecord = false; + + gpsunavailablehandler.removeCallbacks(unavailr); // Cancel the previous unavail countdown handler + gpsunavailablehandler.postDelayed(unavailr, GPSUNAVAILABLEHANDLERTIMER); // starts the unavailability timeout (in 7 sec.) + + if (GPSStatus != GPS_OK) { + if (GPSStatus != GPS_STABILIZING) { + GPSStatus = GPS_STABILIZING; + _Stabilizer = StabilizingSamples; + EventBus.getDefault().post(EventBusMSG.UPDATE_FIX); + } + else _Stabilizer--; + if (_Stabilizer == 0) GPSStatus = GPS_OK; + PrevFix = eloc; + PrevRecordedFix = eloc; + isPrevFixRecorded = true; + } + + // Save fix in case this is a STOP or a START (the speed is "old>0 and new=0" or "old=0 and new>0") + if ((PrevFix != null) && (PrevFix.getLocation().hasSpeed()) && (eloc.getLocation().hasSpeed()) && (GPSStatus == GPS_OK) && (Recording) + && (((eloc.getLocation().getSpeed() == 0) && (PrevFix.getLocation().getSpeed() != 0)) || ((eloc.getLocation().getSpeed() != 0) && (PrevFix.getLocation().getSpeed() == 0)))) { + if (!isPrevFixRecorded) { // Record the old sample if not already recorded + AsyncTODO ast = new AsyncTODO(); + ast.TaskType = "TASK_ADDLOCATION"; + ast.location = PrevFix; + AsyncTODOQueue.add(ast); + PrevRecordedFix = PrevFix; + isPrevFixRecorded = true; + } + + ForceRecord = true; // + Force to record the new + } + + if (GPSStatus == GPS_OK) { + AsyncTODO ast = new AsyncTODO(); + if ((Recording) && ((prefGPSdistance == 0) || (PrevRecordedFix == null) || (ForceRecord) || (loc.distanceTo(PrevRecordedFix.getLocation()) >= prefGPSdistance))) { + PrevRecordedFix = eloc; + ast.TaskType = "TASK_ADDLOCATION"; + ast.location = eloc; + AsyncTODOQueue.add(ast); + isPrevFixRecorded = true; + } else { + ast.TaskType = "TASK_UPDATEFIX"; + ast.location = eloc; + AsyncTODOQueue.add(ast); + isPrevFixRecorded = false; + } + + if (PlacemarkRequest) { + _currentPlacemark = new LocationExtended(loc); + _currentPlacemark.setNumberOfSatellites(getNumberOfSatellites()); + _currentPlacemark.setNumberOfSatellitesUsedInFix(getNumberOfSatellitesUsedInFix()); + _currentPlacemark.setSatellite_info(get_satelliteList()); + PlacemarkRequest = false; + EventBus.getDefault().post(EventBusMSG.UPDATE_TRACK); + EventBus.getDefault().post(EventBusMSG.REQUEST_ADD_PLACEMARK); + + } + PrevFix = eloc; + } + } + } + + @Override + public void onProviderDisabled(String provider) { + GPSStatus = GPS_DISABLED; + EventBus.getDefault().post(EventBusMSG.UPDATE_FIX); + } + + @Override + public void onProviderEnabled(String provider) { + GPSStatus = GPS_SEARCHING; + EventBus.getDefault().post(EventBusMSG.UPDATE_FIX); + } + + + @Override + public void onStatusChanged(String provider, int status, Bundle extras) { + // This is called when the GPS status changes + switch (status) { + case LocationProvider.OUT_OF_SERVICE: + //Log.w("myApp", "[#] GPSApplication.java - GPS Out of Service"); + gpsunavailablehandler.removeCallbacks(unavailr); // Cancel the previous unavail countdown handler + GPSStatus = GPS_OUTOFSERVICE; + EventBus.getDefault().post(EventBusMSG.UPDATE_FIX); + //Toast.makeText( getApplicationContext(), "GPS Out of Service", Toast.LENGTH_SHORT).show(); + break; + case LocationProvider.TEMPORARILY_UNAVAILABLE: + //Log.w("myApp", "[#] GPSApplication.java - GPS Temporarily Unavailable"); + gpsunavailablehandler.removeCallbacks(unavailr); // Cancel the previous unavail countdown handler + GPSStatus = GPS_TEMPORARYUNAVAILABLE; + EventBus.getDefault().post(EventBusMSG.UPDATE_FIX); + //Toast.makeText( getApplicationContext(), "GPS Temporarily Unavailable", Toast.LENGTH_SHORT).show(); + break; + case LocationProvider.AVAILABLE: + gpsunavailablehandler.removeCallbacks(unavailr); // Cancel the previous unavail countdown handler + //Log.w("myApp", "[#] GPSApplication.java - GPS Available: " + _NumberOfSatellites + " satellites"); + break; + } + } + + + public void UpdateTrackList() { + long ID = GPSDataBase.getLastTrackID(); + List _OldArrayListTracks = new ArrayList(); + _OldArrayListTracks.addAll(_ArrayListTracks); + + if (ID > 0) { + synchronized(_ArrayListTracks) { + // Save Selections + ArrayList SelectedT = new ArrayList<>(); + for (Track T : _ArrayListTracks) { + if (T.isSelected()) SelectedT.add(T.getId()); + } + + // Update the List + _ArrayListTracks.clear(); + _ArrayListTracks.addAll(GPSDataBase.getTracksList(0, ID - 1)); + if ((ID > 1) && (GPSDataBase.getTrack(ID - 1) != null)) { + String fname = (ID - 1) + ".png"; + File file = new File(getApplicationContext().getFilesDir() + "/Thumbnails/", fname); + if (!file.exists()) Th = new Thumbnailer(ID - 1); + } + if (_currentTrack.getNumberOfLocations() + _currentTrack.getNumberOfPlacemarks() > 0) { + Log.w("myApp", "[#] GPSApplication.java - Update Tracklist: current track (" + _currentTrack.getId() + ") visible into the tracklist"); + _ArrayListTracks.add(0, _currentTrack); + } else + Log.w("myApp", "[#] GPSApplication.java - Update Tracklist: current track not visible into the tracklist"); + + // Restore the selection state + for (Track T : _ArrayListTracks) { + for (Long SelT : SelectedT) { + if (SelT == T.getId()) { + T.setSelected(true); + break; + } + } + } + } + EventBus.getDefault().post(EventBusMSG.UPDATE_TRACKLIST); + //Log.w("myApp", "[#] GPSApplication.java - Update Tracklist: Added " + _ArrayListTracks.size() + " tracks"); + } + } + + +// PREFERENCES LOADER ------------------------------------------------------------------------------ + + private void LoadPreferences() { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); + + // ---------Conversion from the previous versions of GPS Logger preferences + if (preferences.contains("prefShowImperialUnits")) { // The old boolean setting for imperial units in v.1.1.5 + Log.w("myApp", "[#] GPSApplication.java - Old setting prefShowImperialUnits present. Converting to new preference PrefUM."); + boolean imperialUM = preferences.getBoolean("prefShowImperialUnits", false); + SharedPreferences.Editor editor = preferences.edit(); + editor.putString("prefUM", (imperialUM ? "8" : "0")); + editor.remove("prefShowImperialUnits"); + editor.commit(); + } + + // ---------Remove the prefIsStoragePermissionChecked in preferences if present + + if (preferences.contains("prefIsStoragePermissionChecked")) { + SharedPreferences.Editor editor = preferences.edit(); + editor.remove("prefIsStoragePermissionChecked"); + editor.commit(); + } + + // ----------------------------------------------------------------------- + + //prefKeepScreenOn = preferences.getBoolean("prefKeepScreenOn", true); + prefGPSWeekRolloverCorrected = preferences.getBoolean("prefGPSWeekRolloverCorrected", false); + prefShowDecimalCoordinates = preferences.getBoolean("prefShowDecimalCoordinates", false); + prefViewTracksWith = Integer.valueOf(preferences.getString("prefViewTracksWith", "0")); + prefUM = Integer.valueOf(preferences.getString("prefUM", "0")) + Integer.valueOf(preferences.getString("prefUMSpeed", "1")); + prefGPSdistance = Float.valueOf(preferences.getString("prefGPSdistance", "0")); + prefEGM96AltitudeCorrection = preferences.getBoolean("prefEGM96AltitudeCorrection", false); + prefAltitudeCorrection = Double.valueOf(preferences.getString("prefAltitudeCorrection", "0")); + Log.w("myApp", "[#] GPSApplication.java - Manual Correction set to " + prefAltitudeCorrection + " m"); + prefExportKML = preferences.getBoolean("prefExportKML", true); + prefExportGPX = preferences.getBoolean("prefExportGPX", true); + prefExportTXT = preferences.getBoolean("prefExportTXT", false); + prefKMLAltitudeMode = Integer.valueOf(preferences.getString("prefKMLAltitudeMode", "1")); + prefGPXVersion = Integer.valueOf(preferences.getString("prefGPXVersion", "100")); // Default value = v.1.0 + prefShowTrackStatsType = Integer.valueOf(preferences.getString("prefShowTrackStatsType", "0")); + prefShowDirections = Integer.valueOf(preferences.getString("prefShowDirections", "0")); + + long oldGPSupdatefrequency = prefGPSupdatefrequency; + prefGPSupdatefrequency = Long.valueOf(preferences.getString("prefGPSupdatefrequency", "1000")); + + // ---------------------------------------------- Update the GPS Update Frequency if needed + if (oldGPSupdatefrequency != prefGPSupdatefrequency) updateGPSLocationFrequency(); + + // ---------------------------------------------------------------- If no Exportation formats are enabled, enable the GPX one + if (!prefExportKML && !prefExportGPX && !prefExportTXT) { + SharedPreferences.Editor editor = preferences.edit(); + editor.putBoolean("prefExportGPX", true); + editor.commit(); + prefExportGPX = true; + } + + // ---------------------------------------------------------------- Load EGM Grid if needed + EGM96 egm96 = EGM96.getInstance(); + if (egm96 != null) { + if (!egm96.isEGMGridLoaded()) { + egm96.LoadGridFromFile(Environment.getExternalStorageDirectory() + "/GPSLogger/AppData/WW15MGH.DAC", getApplicationContext().getFilesDir() + "/WW15MGH.DAC"); + } + } + + // ------------------------------------------------------------------- Request of UI Update + EventBus.getDefault().post(EventBusMSG.APPLY_SETTINGS); + EventBus.getDefault().post(EventBusMSG.UPDATE_FIX); + EventBus.getDefault().post(EventBusMSG.UPDATE_TRACK); + EventBus.getDefault().post(EventBusMSG.UPDATE_TRACKLIST); + } + + +// THE THREAD THAT DOES ASYNCHRONOUS OPERATIONS --------------------------------------------------- + + + class AsyncTODO { + String TaskType; + LocationExtended location; + } + + private BlockingQueue AsyncTODOQueue = new LinkedBlockingQueue<>(); + + private class AsyncUpdateThreadClass extends Thread { + + Track track; + LocationExtended locationExtended; + + public AsyncUpdateThreadClass() {} + + public void run() { + + track = _currentTrack; + EventBus.getDefault().post(EventBusMSG.UPDATE_TRACK); + UpdateTrackList(); + + // ---------------------------------------------------------------------------------------- + // Apply the GPS Week Rollover Correction, for data already stored into the DB + // ---------------------------------------------------------------------------------------- + + if (!prefGPSWeekRolloverCorrected) { + if (!isFirstRun) { + Log.w("myApp", "[#] GPSApplication.java - CORRECTING DATA FOR GPS WEEK ROLLOVER"); + GPSDataBase.CorrectGPSWeekRollover(); + Log.w("myApp", "[#] GPSApplication.java - DATA FOR GPS WEEK ROLLOVER CORRECTED"); + UpdateTrackList(); + Log.w("myApp", "[#] GPSApplication.java - TRACKLIST UPDATED WITH THE CORRECTED NAMES"); + } + prefGPSWeekRolloverCorrected = true; + SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()).edit(); + editor.putBoolean("prefGPSWeekRolloverCorrected", true); + editor.commit(); + } + // ---------------------------------------------------------------------------------------- + + + while (true) { + AsyncTODO asyncTODO; + try { + asyncTODO = AsyncTODOQueue.take(); + } catch (InterruptedException e) { + Log.w("myApp", "[!] Buffer not available: " + e.getMessage()); + break; + } + + // Task: Create new track (if needed) + if (asyncTODO.TaskType.equals("TASK_NEWTRACK")) { + if ((track.getNumberOfLocations() != 0) || (track.getNumberOfPlacemarks() != 0)) { + // ---- Delete 2 thumbs files forward - in case of user deleted DB in App manager (pngs could be already presents for the new IDS) + String fname = (track.getId() + 1) +".png"; + File file = new File(getApplicationContext().getFilesDir() + "/Thumbnails/", fname); + if (file.exists ()) file.delete (); + fname = (track.getId() + 2) +".png"; + file = new File(getApplicationContext().getFilesDir() + "/Thumbnails/", fname); + if (file.exists ()) file.delete (); + track = new Track(); + // ---- + track.setId(GPSDataBase.addTrack(track)); + Log.w("myApp", "[#] GPSApplication.java - TASK_NEWTRACK: " + track.getId()); + _currentTrack = track; + UpdateTrackList(); + } else Log.w("myApp", "[#] GPSApplication.java - TASK_NEWTRACK: Track " + track.getId() + " already empty (New track not created)"); + _currentTrack = track; + EventBus.getDefault().post(EventBusMSG.UPDATE_TRACK); + } + + // Task: Add location to current track + if (asyncTODO.TaskType.equals("TASK_ADDLOCATION")) { + locationExtended = new LocationExtended(asyncTODO.location.getLocation()); + locationExtended.setNumberOfSatellites(asyncTODO.location.getNumberOfSatellites()); + locationExtended.setNumberOfSatellitesUsedInFix(asyncTODO.location.getNumberOfSatellitesUsedInFix()); + locationExtended.setSatellite_info(asyncTODO.location.getSatellite_info()); + _currentLocationExtended = locationExtended; + EventBus.getDefault().post(EventBusMSG.UPDATE_FIX); + track.add(locationExtended); + GPSDataBase.addLocationToTrack(locationExtended, track); + //Log.w("myApp", "[#] GPSApplication.java - TASK_ADDLOCATION: Added new Location in " + track.getId()); + _currentTrack = track; + EventBus.getDefault().post(EventBusMSG.UPDATE_TRACK); + if (_currentTrack.getNumberOfLocations() + _currentTrack.getNumberOfPlacemarks() == 1) UpdateTrackList(); + } + + // Task: Add a placemark to current track + if (asyncTODO.TaskType.equals("TASK_ADDPLACEMARK")) { + locationExtended = new LocationExtended(asyncTODO.location.getLocation()); + locationExtended.setDescription(asyncTODO.location.getDescription()); + locationExtended.setNumberOfSatellites(asyncTODO.location.getNumberOfSatellites()); + locationExtended.setNumberOfSatellitesUsedInFix(asyncTODO.location.getNumberOfSatellitesUsedInFix()); + locationExtended.setSatellite_info(asyncTODO.location.getSatellite_info()); + // locationExtended.setSatelliteinfo(asyncTODO.location.get_satellit) + track.addPlacemark(locationExtended); + GPSDataBase.addPlacemarkToTrack(locationExtended, track); + _currentTrack = track; + EventBus.getDefault().post(EventBusMSG.UPDATE_TRACK); + if (_currentTrack.getNumberOfLocations() + _currentTrack.getNumberOfPlacemarks() == 1) UpdateTrackList(); + } + + // Task: Update current Fix + if (asyncTODO.TaskType.equals("TASK_UPDATEFIX")) { + _currentLocationExtended = new LocationExtended(asyncTODO.location.getLocation()); + _currentLocationExtended.setNumberOfSatellites(asyncTODO.location.getNumberOfSatellites()); + _currentLocationExtended.setNumberOfSatellitesUsedInFix(asyncTODO.location.getNumberOfSatellitesUsedInFix()); + _currentLocationExtended.setSatellite_info(asyncTODO.location.getSatellite_info()); + EventBus.getDefault().post(EventBusMSG.UPDATE_FIX); + } + + // Task: Delete some tracks + if (asyncTODO.TaskType.startsWith("TASK_DELETE_TRACKS")) { + + String STokens = asyncTODO.TaskType.substring(19); + List tokens = new ArrayList<>(); + StringTokenizer tokenizer = new StringTokenizer(STokens, " "); + while (tokenizer.hasMoreElements()) { + tokens.add(tokenizer.nextToken()); + } + if (!tokens.isEmpty()) { + JobProgress = 0; + int TracksToBeDeleted = tokens.size(); + int TracksDeleted = 0; + for (String s : tokens) { + Track track = null; // The track found in the _ArrayListTracks + int i = Integer.valueOf(s); + if (i != _currentTrack.getId()) { // Prevent the deletion of the current track + synchronized (_ArrayListTracks) { + for (Track T : _ArrayListTracks) { + if (T.getId() == i) { + track = T; + GPSDataBase.DeleteTrack(i); + Log.w("myApp", "[#] GPSApplication.java - TASK_DELETE_TRACKS: Track " + i + " deleted."); + _ArrayListTracks.remove(T); + break; + } + } + } + if (track != null) { + // Delete track files + DeleteFile(Environment.getExternalStorageDirectory() + "/GPSLogger/AppData/" + track.getName() + ".txt"); + DeleteFile(Environment.getExternalStorageDirectory() + "/GPSLogger/AppData/" + track.getName() + ".kml"); + DeleteFile(Environment.getExternalStorageDirectory() + "/GPSLogger/AppData/" + track.getName() + ".gpx"); + DeleteFile(getApplicationContext().getFilesDir() + "/Thumbnails/" + track.getId() + ".png"); + if (DeleteAlsoExportedFiles) { + // Delete exported files + DeleteFile(Environment.getExternalStorageDirectory() + "/GPSLogger/" + track.getName() + ".txt"); + DeleteFile(Environment.getExternalStorageDirectory() + "/GPSLogger/" + track.getName() + ".kml"); + DeleteFile(Environment.getExternalStorageDirectory() + "/GPSLogger/" + track.getName() + ".gpx"); + } + + TracksDeleted++; + JobProgress = (int) Math.round(1000L * TracksDeleted / TracksToBeDeleted); + EventBus.getDefault().post(EventBusMSG.UPDATE_JOB_PROGRESS); + if (JobsPending > 0) JobsPending--; + } + } else { + Log.w("myApp", "[#] GPSApplication.java - TASK_DELETE_TRACKS: Unable to delete the current track!"); + TracksDeleted++; + JobProgress = (int) Math.round(1000L * TracksDeleted / TracksToBeDeleted); + EventBus.getDefault().post(EventBusMSG.UPDATE_JOB_PROGRESS); + if (JobsPending > 0) JobsPending--; + } + } + } + JobProgress = 0; + EventBus.getDefault().post(EventBusMSG.UPDATE_JOB_PROGRESS); + EventBus.getDefault().post(EventBusMSG.NOTIFY_TRACKS_DELETED); + } + } + } + } + + + + + +// THE THREAD THAT GENERATES A TRACK THUMBNAIL ----------------------------------------------------- + + public class Thumbnailer { + + long Id; + long NumberOfLocations; + + private Paint drawPaint = new Paint(); + private Paint BGPaint = new Paint(); + private Paint EndDotdrawPaint = new Paint(); + private Paint EndDotBGPaint = new Paint(); + private int Size = (int)(getResources().getDimension(R.dimen.thumbSize)); + + private int Margin = (int) Math.ceil(getResources().getDimension(R.dimen.thumbLineWidth) * 3); + private int Size_Minus_Margins = Size - 2 * Margin; + + private double MinLatitude; + private double MinLongitude; + + double Distance_Proportion; + double DrawScale; + double Lat_Offset; + double Lon_Offset; + + private AsyncThumbnailThreadClass asyncThumbnailThreadClass = new AsyncThumbnailThreadClass(); + + public Thumbnailer(long ID) { + + Track track = GPSDataBase.getTrack(ID); + //Log.w("myApp", "[#] GPSApplication.java - Bitmap Size = " + Size); + + if ((track.getNumberOfLocations() > 2) && (track.getDistance() >= 15) && (track.getValidMap() != 0)) { + Id = track.getId(); + NumberOfLocations = track.getNumberOfLocations(); + + // Setup Paints + //drawPaint.setColor(getResources().getColor(R.color.colorThumbnailLineColor)); + drawPaint.setAntiAlias(true); + drawPaint.setStrokeWidth(getResources().getDimension(R.dimen.thumbLineWidth)); + //drawPaint.setStrokeWidth(2); + drawPaint.setStyle(Paint.Style.STROKE); + drawPaint.setStrokeJoin(Paint.Join.ROUND); + drawPaint.setStrokeCap(Paint.Cap.ROUND); + + BGPaint.setColor(Color.BLACK); + BGPaint.setAntiAlias(true); + BGPaint.setStrokeWidth(getResources().getDimension(R.dimen.thumbLineWidth) * 3); + //BGPaint.setStrokeWidth(6); + BGPaint.setStyle(Paint.Style.STROKE); + BGPaint.setStrokeJoin(Paint.Join.ROUND); + BGPaint.setStrokeCap(Paint.Cap.ROUND); + + // EndDotdrawPaint.setColor(getResources().getColor(R.color.colorThumbnailLineColor)); + EndDotdrawPaint.setAntiAlias(true); + EndDotdrawPaint.setStrokeWidth(getResources().getDimension(R.dimen.thumbLineWidth) * 2.5f); + EndDotdrawPaint.setStyle(Paint.Style.STROKE); + EndDotdrawPaint.setStrokeJoin(Paint.Join.ROUND); + EndDotdrawPaint.setStrokeCap(Paint.Cap.ROUND); + + EndDotBGPaint.setColor(Color.BLACK); + EndDotBGPaint.setAntiAlias(true); + EndDotBGPaint.setStrokeWidth(getResources().getDimension(R.dimen.thumbLineWidth) * 4.5f); + EndDotBGPaint.setStyle(Paint.Style.STROKE); + EndDotBGPaint.setStrokeJoin(Paint.Join.ROUND); + EndDotBGPaint.setStrokeCap(Paint.Cap.ROUND); + + // Calculate the drawing scale + double Mid_Latitude = (track.getMax_Latitude() + track.getMin_Latitude()) / 2; + double Angle_From_Equator = Math.abs(Mid_Latitude); + + Distance_Proportion = Math.cos(Math.toRadians(Angle_From_Equator)); + //Log.w("myApp", "[#] GPSApplication.java - Distance_Proportion = " + Distance_Proportion); + + DrawScale = Math.max(track.getMax_Latitude() - track.getMin_Latitude(), Distance_Proportion * (track.getMax_Longitude() - track.getMin_Longitude())); + Lat_Offset = Size_Minus_Margins * (1 - (track.getMax_Latitude() - track.getMin_Latitude()) / DrawScale) / 2; + Lon_Offset = Size_Minus_Margins * (1 - (Distance_Proportion * (track.getMax_Longitude() - track.getMin_Longitude()) / DrawScale)) / 2; + + MinLatitude = track.getMin_Latitude(); + MinLongitude = track.getMin_Longitude(); + + asyncThumbnailThreadClass.start(); + } + } + + private class AsyncThumbnailThreadClass extends Thread { + + public AsyncThumbnailThreadClass() {} + + public void run() { + Thread.currentThread().setPriority(Thread.MIN_PRIORITY); + + String fname = Id + ".png"; + File file = new File(getApplicationContext().getFilesDir() + "/Thumbnails/", fname); + if (file.exists()) file.delete(); + + if (DrawScale > 0) { + int GroupOfLocations = 200; + Path path = new Path(); + List latlngList = new ArrayList<>(); + + //Log.w("myApp", "[#] GPSApplication.java - Thumbnailer Thread started"); + for (int i = 0; i < NumberOfLocations; i += GroupOfLocations) { + latlngList.addAll(GPSDataBase.getLatLngList(Id, i, i + GroupOfLocations - 1)); + } + //Log.w("myApp", "[#] GPSApplication.java - Added " + latlngList.size() + " items to Path"); + if (!latlngList.isEmpty()) { + Bitmap ThumbBitmap = Bitmap.createBitmap(Size, Size, Bitmap.Config.ARGB_8888); + Canvas ThumbCanvas = new Canvas(ThumbBitmap); + + for (int i = 0; i < latlngList.size(); i++) { + if (i == 0) + path.moveTo((float) (Lon_Offset + Margin + Size_Minus_Margins * ((latlngList.get(i).Longitude - MinLongitude) * Distance_Proportion / DrawScale)), + (float) (-Lat_Offset + Size - (Margin + Size_Minus_Margins * ((latlngList.get(i).Latitude - MinLatitude) / DrawScale)))); + else + path.lineTo((float) (Lon_Offset + Margin + Size_Minus_Margins * ((latlngList.get(i).Longitude - MinLongitude) * Distance_Proportion / DrawScale)), + (float) (-Lat_Offset + Size - (Margin + Size_Minus_Margins * ((latlngList.get(i).Latitude - MinLatitude) / DrawScale)))); + } + ThumbCanvas.drawPath(path, BGPaint); + ThumbCanvas.drawPoint((float) (Lon_Offset + Margin + Size_Minus_Margins * ((latlngList.get(latlngList.size()-1).Longitude - MinLongitude) * Distance_Proportion / DrawScale)), + (float) (-Lat_Offset + Size - (Margin + Size_Minus_Margins * ((latlngList.get(latlngList.size()-1).Latitude - MinLatitude) / DrawScale))), EndDotBGPaint); + ThumbCanvas.drawPath(path, drawPaint); + ThumbCanvas.drawPoint((float) (Lon_Offset + Margin + Size_Minus_Margins * ((latlngList.get(latlngList.size()-1).Longitude - MinLongitude) * Distance_Proportion / DrawScale)), + (float) (-Lat_Offset + Size - (Margin + Size_Minus_Margins * ((latlngList.get(latlngList.size()-1).Latitude - MinLatitude) / DrawScale))), EndDotdrawPaint); + + try { + FileOutputStream out = new FileOutputStream(file); + //Log.w("myApp", "[#] GPSApplication.java - FileOutputStream out = new FileOutputStream(file)"); + //boolean res = ThumbBitmap.compress(Bitmap.CompressFormat.PNG, 60, out); + ThumbBitmap.compress(Bitmap.CompressFormat.PNG, 60, out); + //Log.w("myApp", "[#] GPSApplication.java - ThumbBitmap.compress(Bitmap.CompressFormat.PNG, 60, out): " + res); + out.flush(); + //Log.w("myApp", "[#] GPSApplication.java - out.flush();"); + out.close(); + //Log.w("myApp", "[#] GPSApplication.java - out.close();"); + } catch (Exception e) { + e.printStackTrace(); + //Log.w("myApp", "[#] GPSApplication.java - Unable to save: " + Environment.getExternalStorageDirectory() + "/GPSLogger/AppData/" + fname); + } + + EventBus.getDefault().post(EventBusMSG.REFRESH_TRACKLIST); + } + } + } + } + } +} diff --git a/app/src/main/java/Satellite/GPSService.java b/app/src/main/java/Satellite/GPSService.java new file mode 100644 index 0000000..2cd2787 --- /dev/null +++ b/app/src/main/java/Satellite/GPSService.java @@ -0,0 +1,121 @@ + +package Satellite; + +import android.app.Notification; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Intent; +import android.os.Binder; +import android.os.IBinder; +import android.os.PowerManager; +import android.support.v4.app.NotificationCompat; +import android.util.Log; + +public class GPSService extends Service { + // Singleton instance + private static GPSService singleton; + public static GPSService getInstance(){ + return singleton; + } + // IBinder + private final IBinder mBinder = new LocalBinder(); + public class LocalBinder extends Binder { //returns the instance of the service + public GPSService getServiceInstance(){ + return GPSService.this; + } + } + + // PARTIAL_WAKELOCK + private PowerManager powerManager; + PowerManager.WakeLock wakeLock; + + private Notification getNotification() { + final String CHANNEL_ID = "GPSLoggerServiceChannel"; + + NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID); + //builder.setSmallIcon(R.drawable.ic_notification_24dp) + /* builder.setSmallIcon(R.mipmap.ic_notify_24dp) + .setColor(getResources().getColor(R.color.colorPrimaryLight)) + .setContentTitle(getString(R.string.app_name)) + .setShowWhen(false) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setCategory(NotificationCompat.CATEGORY_SERVICE) + .setOngoing(true) + .setContentText(getString(R.string.notification_contenttext)); +*/ + //if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){ + // builder.setPriority(NotificationCompat.PRIORITY_LOW); + //} + + final Intent startIntent = new Intent(getApplicationContext(), GPSActivity.class); + startIntent.setAction(Intent.ACTION_MAIN); + startIntent.addCategory(Intent.CATEGORY_LAUNCHER); + //startIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startIntent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); + PendingIntent contentIntent = PendingIntent.getActivity(getApplicationContext(), 1, startIntent, 0); + builder.setContentIntent(contentIntent); + return builder.build(); + } + + /* THREAD FOR DEBUG PURPOSE + Thread t = new Thread() { + public void run() { + boolean i = true; + while (i) { + try { + sleep(1000); + Log.w("myApp", "[#] GPSService.java - ** RUNNING **"); + } catch (InterruptedException e) { + i = false; + } + } + } + }; */ + + @Override + public void onCreate() { + super.onCreate(); + singleton = this; + // THREAD FOR DEBUG PURPOSE + //if (!t.isAlive()) { + // t.start(); + //} + + // PARTIAL_WAKELOCK + powerManager = (PowerManager) getSystemService(POWER_SERVICE); + wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,"GPSLogger:wakelock"); + Log.w("myApp", "[#] GPSService.java - CREATE = onCreate"); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + startForeground(1, getNotification()); + Log.w("myApp", "[#] GPSService.java - START = onStartCommand"); + return START_NOT_STICKY; + } + + @Override + public IBinder onBind(Intent intent) { + if (wakeLock != null && !wakeLock.isHeld()) { + wakeLock.acquire(); + Log.w("myApp", "[#] GPSService.java - WAKELOCK acquired"); + } + Log.w("myApp", "[#] GPSService.java - BIND = onBind"); + return mBinder; + //return null; + } + + @Override + public void onDestroy() { + // PARTIAL_WAKELOCK + if (wakeLock != null && wakeLock.isHeld()) { + wakeLock.release(); + Log.w("myApp", "[#] GPSService.java - WAKELOCK released"); + } + + Log.w("myApp", "[#] GPSService.java - DESTROY = onDestroy"); + // THREAD FOR DEBUG PURPOSE + //if (t.isAlive()) t.interrupt(); + super.onDestroy(); + } +} diff --git a/app/src/main/java/Satellite/LatLng.java b/app/src/main/java/Satellite/LatLng.java new file mode 100644 index 0000000..63de7fc --- /dev/null +++ b/app/src/main/java/Satellite/LatLng.java @@ -0,0 +1,7 @@ + +package Satellite; + +class LatLng { + double Latitude; + double Longitude; +} diff --git a/app/src/main/java/Satellite/LocationExtended.java b/app/src/main/java/Satellite/LocationExtended.java new file mode 100644 index 0000000..c8686c0 --- /dev/null +++ b/app/src/main/java/Satellite/LocationExtended.java @@ -0,0 +1,97 @@ + +package Satellite; + +import android.location.Location; + +public class LocationExtended { + + private final int NOT_AVAILABLE = -100000; + + private Location _Location; + private String _Description = ""; + private double _AltitudeEGM96Correction = NOT_AVAILABLE; + private int _NumberOfSatellites = NOT_AVAILABLE; + private int _NumberOfSatellitesUsedInFix = NOT_AVAILABLE; + private String Satellite_info = ""; + + + // Constructor + public LocationExtended(Location location) { + _Location = location; + EGM96 egm96 = EGM96.getInstance(); + if (egm96 != null) { + if (egm96.isEGMGridLoaded()) _AltitudeEGM96Correction = egm96.getEGMCorrection(_Location.getLatitude(), _Location.getLongitude()); + } + } + + // Getters and Setters ------------------------------------------------------------------------- + + public Location getLocation() { + return _Location; + } + + public double getLatitude() { return _Location.getLatitude(); } + public double getLongitude() { return _Location.getLongitude(); } + public double getAltitude() { return _Location.hasAltitude() ? _Location.getAltitude() : NOT_AVAILABLE; } + public float getSpeed() { return _Location.hasSpeed() ? _Location.getSpeed() : NOT_AVAILABLE; } + public float getAccuracy() { return _Location.hasAccuracy() ? _Location.getAccuracy() : NOT_AVAILABLE; } + public float getBearing() { return _Location.hasBearing() ? _Location.getBearing() : NOT_AVAILABLE; } + public long getTime() { return _Location.getTime(); } + public String getSatellite_info(){return Satellite_info; } + + public String getDescription() { + return _Description; + } + + public void setDescription(String Description) { + this._Description = Description; + } + + public void setNumberOfSatellites(int numberOfSatellites) { + _NumberOfSatellites = numberOfSatellites; + } + + public int getNumberOfSatellites() { + return _NumberOfSatellites; + } + + public void setNumberOfSatellitesUsedInFix(int numberOfSatellites) { + _NumberOfSatellitesUsedInFix = numberOfSatellites; + } + + public void setSatellite_info(String satellite_info){ + this.Satellite_info = satellite_info; + + + } + + public int getNumberOfSatellitesUsedInFix() { + return _NumberOfSatellitesUsedInFix; + } + + + + public double getAltitudeEGM96Correction(){ + if (_AltitudeEGM96Correction == NOT_AVAILABLE) { + //Log.w("myApp", "[#] LocationExtended.java - _AltitudeEGM96Correction == NOT_AVAILABLE"); + EGM96 egm96 = EGM96.getInstance(); + if (egm96 != null) { + if (egm96.isEGMGridLoaded()) _AltitudeEGM96Correction = egm96.getEGMCorrection(_Location.getLatitude(), _Location.getLongitude()); + } + } + return _AltitudeEGM96Correction; + } + + public double getAltitudeCorrected(double AltitudeManualCorrection, boolean EGMCorrection) { + if (_Location != null) { + if (!_Location.hasAltitude()) return NOT_AVAILABLE; + if ((EGMCorrection) && (getAltitudeEGM96Correction() != NOT_AVAILABLE)) return _Location.getAltitude() - getAltitudeEGM96Correction() + AltitudeManualCorrection; + else return _Location.getAltitude() + AltitudeManualCorrection; + } + return NOT_AVAILABLE; + } + + + +} + diff --git a/app/src/main/java/Satellite/PhysicalData.java b/app/src/main/java/Satellite/PhysicalData.java new file mode 100644 index 0000000..74e18f1 --- /dev/null +++ b/app/src/main/java/Satellite/PhysicalData.java @@ -0,0 +1,7 @@ + +package Satellite; + +class PhysicalData { + String Value; //The string of the Numerical value of the Physical Quantity + String UM; //The string of the Unit of Measurement +} diff --git a/app/src/main/java/Satellite/PhysicalDataFormatter.java b/app/src/main/java/Satellite/PhysicalDataFormatter.java new file mode 100644 index 0000000..64b0c6d --- /dev/null +++ b/app/src/main/java/Satellite/PhysicalDataFormatter.java @@ -0,0 +1,258 @@ + +package Satellite; + +import android.location.Location; + +import java.text.SimpleDateFormat; +import java.util.TimeZone; + +import fr.geolabs.dev.mapmint4me.R; + +class PhysicalDataFormatter { + + private final int NOT_AVAILABLE = -100000; + + private final int UM_METRIC_MS = 0; + private final int UM_METRIC_KMH = 1; + private final int UM_IMPERIAL_FPS = 8; + private final int UM_IMPERIAL_MPH = 9; + private final int UM_NAUTICAL_KN = 16; + private final int UM_NAUTICAL_MPH = 17; + + static final byte FORMAT_LATITUDE = 1; + static final byte FORMAT_LONGITUDE = 2; + static final byte FORMAT_ALTITUDE = 3; + static final byte FORMAT_SPEED = 4; + static final byte FORMAT_ACCURACY = 5; + static final byte FORMAT_BEARING = 6; + static final byte FORMAT_DURATION = 7; + static final byte FORMAT_SPEED_AVG = 8; + static final byte FORMAT_DISTANCE = 9; + static final byte FORMAT_TIME = 10; + + private final float M_TO_FT = 3.280839895f; + private final float M_TO_NM = 0.000539957f; + private final float MS_TO_MPH = 2.2369363f; + private final float MS_TO_KMH = 3.6f; + private final float MS_TO_KN = 1.943844491f; + private final float KM_TO_MI = 0.621371192237f; + + //private PhysicalData _PhysicalData = new PhysicalData(); + private GPSApplication gpsApplication = GPSApplication.getInstance(); + + + public PhysicalData format(float Number, byte Format) { + PhysicalData _PhysicalData = new PhysicalData(); + _PhysicalData.Value = ""; + _PhysicalData.UM = ""; + + if (Number == NOT_AVAILABLE) return(_PhysicalData); // Returns empty fields if the data is not available + + switch (Format) { + case FORMAT_SPEED: // Speed + switch (gpsApplication.getPrefUM()) { + case UM_METRIC_KMH: + _PhysicalData.Value = String.valueOf(Math.round(Number * MS_TO_KMH)); + _PhysicalData.UM = gpsApplication.getString(R.string.UM_km_h); + return(_PhysicalData); + case UM_METRIC_MS: + _PhysicalData.Value = String.valueOf(Math.round(Number)); + _PhysicalData.UM = gpsApplication.getString(R.string.UM_m_s); + return(_PhysicalData); + case UM_IMPERIAL_MPH: + case UM_NAUTICAL_MPH: + _PhysicalData.Value = String.valueOf(Math.round(Number * MS_TO_MPH)); + _PhysicalData.UM = gpsApplication.getString(R.string.UM_mph); + return(_PhysicalData); + case UM_IMPERIAL_FPS: + _PhysicalData.Value = String.valueOf(Math.round(Number * M_TO_FT)); + _PhysicalData.UM = gpsApplication.getString(R.string.UM_fps); + return(_PhysicalData); + case UM_NAUTICAL_KN: + _PhysicalData.Value = String.valueOf(Math.round(Number * MS_TO_KN)); + _PhysicalData.UM = gpsApplication.getString(R.string.UM_kn); + return(_PhysicalData); + } + + case FORMAT_SPEED_AVG: // Average Speed, formatted with 1 decimal + switch (gpsApplication.getPrefUM()) { + case UM_METRIC_KMH: + _PhysicalData.Value = String.format("%.1f", (Number * MS_TO_KMH)); + _PhysicalData.UM = gpsApplication.getString(R.string.UM_km_h); + return(_PhysicalData); + case UM_METRIC_MS: + _PhysicalData.Value = String.format("%.1f", (Number)); + _PhysicalData.UM = gpsApplication.getString(R.string.UM_m_s); + return(_PhysicalData); + case UM_IMPERIAL_MPH: + case UM_NAUTICAL_MPH: + _PhysicalData.Value = String.format("%.1f", (Number * MS_TO_MPH)); + _PhysicalData.UM = gpsApplication.getString(R.string.UM_mph); + return(_PhysicalData); + case UM_IMPERIAL_FPS: + _PhysicalData.Value = String.format("%.1f", (Number * M_TO_FT)); + _PhysicalData.UM = gpsApplication.getString(R.string.UM_fps); + return(_PhysicalData); + case UM_NAUTICAL_KN: + _PhysicalData.Value = String.format("%.1f", (Number * MS_TO_KN)); + _PhysicalData.UM = gpsApplication.getString(R.string.UM_kn); + return(_PhysicalData); + } + + case FORMAT_ACCURACY: // Accuracy + switch (gpsApplication.getPrefUM()) { + case UM_METRIC_KMH: + case UM_METRIC_MS: + _PhysicalData.Value = String.valueOf(Math.round(Number)); + _PhysicalData.UM = gpsApplication.getString(R.string.UM_m); + return(_PhysicalData); + case UM_IMPERIAL_MPH: + case UM_IMPERIAL_FPS: + case UM_NAUTICAL_MPH: + case UM_NAUTICAL_KN: + _PhysicalData.Value = String.valueOf(Math.round(Number * M_TO_FT)); + _PhysicalData.UM = gpsApplication.getString(R.string.UM_ft); + return(_PhysicalData); + } + + case FORMAT_BEARING: // Bearing (Direction) + switch (gpsApplication.getPrefShowDirections()) { + case 0: // NSWE + int dr = (int) Math.round(Number / 22.5); + switch (dr) { + case 0: _PhysicalData.Value = gpsApplication.getString(R.string.north); return(_PhysicalData); + case 1: _PhysicalData.Value = gpsApplication.getString(R.string.north_northeast); return(_PhysicalData); + case 2: _PhysicalData.Value = gpsApplication.getString(R.string.northeast); return(_PhysicalData); + case 3: _PhysicalData.Value = gpsApplication.getString(R.string.east_northeast); return(_PhysicalData); + case 4: _PhysicalData.Value = gpsApplication.getString(R.string.east); return(_PhysicalData); + case 5: _PhysicalData.Value = gpsApplication.getString(R.string.east_southeast); return(_PhysicalData); + case 6: _PhysicalData.Value = gpsApplication.getString(R.string.southeast); return(_PhysicalData); + case 7: _PhysicalData.Value = gpsApplication.getString(R.string.south_southeast); return(_PhysicalData); + case 8: _PhysicalData.Value = gpsApplication.getString(R.string.south); return(_PhysicalData); + case 9: _PhysicalData.Value = gpsApplication.getString(R.string.south_southwest); return(_PhysicalData); + case 10: _PhysicalData.Value = gpsApplication.getString(R.string.southwest); return(_PhysicalData); + case 11: _PhysicalData.Value = gpsApplication.getString(R.string.west_southwest); return(_PhysicalData); + case 12: _PhysicalData.Value = gpsApplication.getString(R.string.west); return(_PhysicalData); + case 13: _PhysicalData.Value = gpsApplication.getString(R.string.west_northwest); return(_PhysicalData); + case 14: _PhysicalData.Value = gpsApplication.getString(R.string.northwest); return(_PhysicalData); + case 15: _PhysicalData.Value = gpsApplication.getString(R.string.north_northwest); return(_PhysicalData); + case 16: _PhysicalData.Value = gpsApplication.getString(R.string.north); return(_PhysicalData); + } + case 1: // Angle + _PhysicalData.Value = String.valueOf(Math.round(Number)); + return(_PhysicalData); + } + + case FORMAT_DISTANCE: // Distance + switch (gpsApplication.getPrefUM()) { + case UM_METRIC_KMH: + case UM_METRIC_MS: + if (Number < 1000) { + _PhysicalData.Value = String.format("%.0f", (Math.floor(Number))); + _PhysicalData.UM = gpsApplication.getString(R.string.UM_m); + } + else { + if (Number < 10000) _PhysicalData.Value = String.format("%.2f" , ((Math.floor(Number / 10.0)))/100.0); + else _PhysicalData.Value = String.format("%.1f" , ((Math.floor(Number / 100.0)))/10.0); + _PhysicalData.UM = gpsApplication.getString(R.string.UM_km); + } + return(_PhysicalData); + case UM_IMPERIAL_MPH: + case UM_IMPERIAL_FPS: + if ((Number * M_TO_FT) < 1000) { + _PhysicalData.Value = String.format("%.0f", (Math.floor(Number * M_TO_FT))); + _PhysicalData.UM = gpsApplication.getString(R.string.UM_ft); + } + else { + if ((Number * KM_TO_MI) < 10000) _PhysicalData.Value = String.format("%.2f", ((Math.floor((Number * KM_TO_MI) / 10.0)))/100.0); + else _PhysicalData.Value = String.format("%.1f", ((Math.floor((Number * KM_TO_MI) / 100.0)))/10.0); + _PhysicalData.UM = gpsApplication.getString(R.string.UM_mi); + } + return(_PhysicalData); + case UM_NAUTICAL_KN: + case UM_NAUTICAL_MPH: + if ((Number * M_TO_NM) < 100) _PhysicalData.Value = String.format("%.2f", ((Math.floor((Number * M_TO_NM) * 100.0))) / 100.0); + else _PhysicalData.Value = String.format("%.1f", ((Math.floor((Number * M_TO_NM) * 10.0))) / 10.0); + _PhysicalData.UM = gpsApplication.getString(R.string.UM_nm); + return(_PhysicalData); + } + } + return(_PhysicalData); + } + + public PhysicalData format(double Number, byte Format) { + PhysicalData _PhysicalData = new PhysicalData(); + _PhysicalData.Value = ""; + _PhysicalData.UM = ""; + + if (Number == NOT_AVAILABLE) return(_PhysicalData); // Returns empty fields if the data is not available + + switch (Format) { + case FORMAT_LATITUDE: // Latitude + _PhysicalData.Value = gpsApplication.getPrefShowDecimalCoordinates() ? + String.format("%.9f", Math.abs(Number)) : Location.convert(Math.abs(Number), Location.FORMAT_SECONDS); + _PhysicalData.UM = Number >= 0 ? gpsApplication.getString(R.string.north) : gpsApplication.getString(R.string.south); + return(_PhysicalData); + + case FORMAT_LONGITUDE: // Longitude + _PhysicalData.Value = gpsApplication.getPrefShowDecimalCoordinates() ? + String.format("%.9f", Math.abs(Number)) : Location.convert(Math.abs(Number), Location.FORMAT_SECONDS); + _PhysicalData.UM = Number >= 0 ? + gpsApplication.getString(R.string.east) : gpsApplication.getString(R.string.west); + return(_PhysicalData); + + case FORMAT_ALTITUDE: // Altitude + switch (gpsApplication.getPrefUM()) { + case UM_METRIC_KMH: + case UM_METRIC_MS: + _PhysicalData.Value = String.valueOf(Math.round(Number)); + _PhysicalData.UM = gpsApplication.getString(R.string.UM_m); + return(_PhysicalData); + case UM_IMPERIAL_MPH: + case UM_IMPERIAL_FPS: + case UM_NAUTICAL_KN: + case UM_NAUTICAL_MPH: + _PhysicalData.Value = String.valueOf(Math.round(Number * M_TO_FT)); + _PhysicalData.UM = gpsApplication.getString(R.string.UM_ft); + return(_PhysicalData); + } + } + return(_PhysicalData); + } + + public PhysicalData format(long Number, byte Format) { + PhysicalData _PhysicalData = new PhysicalData(); + _PhysicalData.Value = ""; + _PhysicalData.UM = ""; + + if (Number == NOT_AVAILABLE) return(_PhysicalData); // Returns empty fields if the data is not available + + switch (Format) { + case FORMAT_DURATION: // Durations + long time = Number / 1000; + String seconds = Integer.toString((int) (time % 60)); + String minutes = Integer.toString((int) ((time % 3600) / 60)); + String hours = Integer.toString((int) (time / 3600)); + for (int i = 0; i < 2; i++) { + if (seconds.length() < 2) { + seconds = "0" + seconds; + } + if (minutes.length() < 2) { + minutes = "0" + minutes; + } + if (hours.length() < 2) { + hours = "0" + hours; + } + } + _PhysicalData.Value = hours.equals("00") ? minutes + ":" + seconds : hours + ":" + minutes + ":" + seconds; + return(_PhysicalData); + + case FORMAT_TIME: // Timestamps + SimpleDateFormat dfdTime = new SimpleDateFormat("HH:mm:ss"); // date and time formatter + dfdTime.setTimeZone(TimeZone.getTimeZone("GMT")); + _PhysicalData.Value = dfdTime.format(Number); + return(_PhysicalData); + } + return(_PhysicalData); + } +} diff --git a/app/src/main/java/Satellite/SpikesChecker.java b/app/src/main/java/Satellite/SpikesChecker.java new file mode 100644 index 0000000..5ad63a9 --- /dev/null +++ b/app/src/main/java/Satellite/SpikesChecker.java @@ -0,0 +1,76 @@ +/* + * SpikesChecker - Java Class for Android + * Created by G.Capelli (BasicAirData) on 15/9/2016 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package Satellite; + + +class SpikesChecker { + private static final int NOT_AVAILABLE = -100000; + + private long Good_Time = NOT_AVAILABLE; // The time of the last good value + + private double Prev_Altitude = NOT_AVAILABLE; // the previous data loaded + private long Prev_Time = NOT_AVAILABLE; + private float Prev_VerticalSpeed = NOT_AVAILABLE; + + private double New_Altitude = NOT_AVAILABLE; // the new (current) data loaded + private long New_Time = NOT_AVAILABLE; + private float New_VerticalSpeed = NOT_AVAILABLE; + + private long Time_Interval = NOT_AVAILABLE; // Interval between fixes (in seconds) + private float VerticalAcceleration; + + private float MAX_ACCELERATION; // The maximum vertical acceleration allowed + private int STABILIZATION_TIME = 4; // Stabilization window, in seconds. It must be > 0 + + // Constructor + SpikesChecker(float max_acceleration, int Stabilization_Time) { + MAX_ACCELERATION = max_acceleration; + STABILIZATION_TIME = Stabilization_Time; + } + + void load(long Time, double Altitude) { + if (Time > New_Time) { + Prev_Time = New_Time; + New_Time = Time; + Prev_Altitude = New_Altitude; + Prev_VerticalSpeed = New_VerticalSpeed; + } + + Time_Interval = Prev_Time != NOT_AVAILABLE ? (New_Time - Prev_Time) / 1000 : NOT_AVAILABLE; + New_Altitude = Altitude; + + if ((Time_Interval > 0) && (Prev_Altitude != NOT_AVAILABLE)) { + New_VerticalSpeed = (float) (New_Altitude - Prev_Altitude) / Time_Interval; + + if (Prev_VerticalSpeed != NOT_AVAILABLE) { + if (Time_Interval > 1000) VerticalAcceleration = NOT_AVAILABLE; // Prevent Vertical Acceleration value from exploding + else VerticalAcceleration = 2 * (-Prev_VerticalSpeed * Time_Interval + (float)(New_Altitude - Prev_Altitude)) / (Time_Interval * Time_Interval); + } + } + + if (Math.abs(VerticalAcceleration) >= MAX_ACCELERATION) Good_Time = New_Time ; + + //Log.w("myApp", "[#] SpikesChecker.java - Vertical Acceleration = " + VerticalAcceleration); + //Log.w("myApp", "[#] SpikesChecker.java - Validation window = " + (New_Time - Good_Time) / 1000); + } + + boolean isValid() { + return (New_Time - Good_Time) / 1000 >= STABILIZATION_TIME; + } +} diff --git a/app/src/main/java/Satellite/Track.java b/app/src/main/java/Satellite/Track.java new file mode 100644 index 0000000..5d6caf4 --- /dev/null +++ b/app/src/main/java/Satellite/Track.java @@ -0,0 +1,724 @@ + +package Satellite; + +import android.location.Location; + +import java.text.SimpleDateFormat; + +public class Track { + + // Constants + private static final int NOT_AVAILABLE = -100000; + private static final double MIN_ALTITUDE_STEP = 8.0; + private static final float MOVEMENT_SPEED_THRESHOLD = 0.5f; // The minimum speed (in m/s) to consider the user in movement + private static final float STANDARD_ACCURACY = 10.0f; + private static final float SECURITY_COEFF = 1.7f; + + private final int TRACK_TYPE_STEADY = 0; + private final int TRACK_TYPE_WALK = 1; + private final int TRACK_TYPE_MOUNTAIN = 2; + private final int TRACK_TYPE_RUN = 3; + private final int TRACK_TYPE_BICYCLE = 4; + private final int TRACK_TYPE_CAR = 5; + private final int TRACK_TYPE_FLIGHT = 6; + private final int TRACK_TYPE_ND = NOT_AVAILABLE; + + // Variables + private long id; // Saved in DB + private String Name = ""; // Saved in DB + + private double Start_Latitude = NOT_AVAILABLE; // Saved in DB + private double Start_Longitude = NOT_AVAILABLE; // Saved in DB + private double Start_Altitude = NOT_AVAILABLE; // Saved in DB + private double Start_EGMAltitudeCorrection = NOT_AVAILABLE; + private float Start_Accuracy = STANDARD_ACCURACY;// Saved in DB + private float Start_Speed = NOT_AVAILABLE; // Saved in DB + private long Start_Time = NOT_AVAILABLE; // Saved in DB + + private long LastFix_Time = NOT_AVAILABLE; // Saved in DB + + private double End_Latitude = NOT_AVAILABLE; // Saved in DB + private double End_Longitude = NOT_AVAILABLE; // Saved in DB + private double End_Altitude = NOT_AVAILABLE; // Saved in DB + private double End_EGMAltitudeCorrection = NOT_AVAILABLE; + private float End_Accuracy = STANDARD_ACCURACY;// Saved in DB + private float End_Speed = NOT_AVAILABLE; // Saved in DB + private long End_Time = NOT_AVAILABLE; // Saved in DB + + private double LastStepDistance_Latitude = NOT_AVAILABLE; // Saved in DB + private double LastStepDistance_Longitude = NOT_AVAILABLE; // Saved in DB + private float LastStepDistance_Accuracy = STANDARD_ACCURACY;// Saved in DB + + private double LastStepAltitude_Altitude = NOT_AVAILABLE; // Saved in DB + private float LastStepAltitude_Accuracy = STANDARD_ACCURACY;// Saved in DB + + private double Min_Latitude = NOT_AVAILABLE; // Saved in DB + private double Min_Longitude = NOT_AVAILABLE; // Saved in DB + + private double Max_Latitude = NOT_AVAILABLE; // Saved in DB + private double Max_Longitude = NOT_AVAILABLE; // Saved in DB + + private long Duration = NOT_AVAILABLE; // Saved in DB + private long Duration_Moving = NOT_AVAILABLE; // Saved in DB + + private float Distance = NOT_AVAILABLE; // Saved in DB + private float DistanceInProgress = NOT_AVAILABLE; // Saved in DB + private long DistanceLastAltitude = NOT_AVAILABLE; // Saved in DB + + private double Altitude_Up = NOT_AVAILABLE; // Saved in DB + private double Altitude_Down = NOT_AVAILABLE; // Saved in DB + private double Altitude_InProgress = NOT_AVAILABLE; // Saved in DB + + private float SpeedMax = NOT_AVAILABLE; // Saved in DB + private float SpeedAverage = NOT_AVAILABLE; // Saved in DB + private float SpeedAverageMoving = NOT_AVAILABLE; // Saved in DB + + private long NumberOfLocations = 0; // Saved in DB + private long NumberOfPlacemarks = 0; // Saved in DB + + private int ValidMap = 1; // Saved in DB + // 1 = Map extents valid, OK generation of Thumb + // 0 = Do not generate thumb (track crosses antimeridian) + + private int Type = TRACK_TYPE_ND; // Saved in DB + + // True if the card view is selected + private boolean Selected = false; + + // The altitude validator (the anti spikes filter): + // - Max Acceleration = 12 m/s^2 + // - Stabilization time = 4 s + private SpikesChecker AltitudeFilter = new SpikesChecker(12, 4); + + public void add(LocationExtended location) { + if (NumberOfLocations == 0) { + // Init "Start" variables + Start_Latitude = location.getLocation().getLatitude(); + Start_Longitude = location.getLocation().getLongitude(); + if (location.getLocation().hasAltitude()) { + Start_Altitude = location.getLocation().getAltitude(); + } else { + Start_Altitude = NOT_AVAILABLE; + } + Start_EGMAltitudeCorrection = location.getAltitudeEGM96Correction(); + Start_Speed = location.getLocation().hasSpeed() ? location.getLocation().getSpeed() : NOT_AVAILABLE; + Start_Accuracy = location.getLocation().hasAccuracy() ? location.getLocation().getAccuracy() : STANDARD_ACCURACY; + Start_Time = location.getLocation().getTime(); + + LastStepDistance_Latitude = Start_Latitude; + LastStepDistance_Longitude = Start_Longitude; + LastStepDistance_Accuracy = Start_Accuracy; + + Max_Latitude = Start_Latitude; + Max_Longitude = Start_Longitude; + Min_Latitude = Start_Latitude; + Min_Longitude = Start_Longitude; + + if (Name.equals("")) { + SimpleDateFormat df2 = new SimpleDateFormat("yyyyMMdd-HHmmss"); + Name = df2.format(Start_Time); + } + + LastFix_Time = Start_Time; + End_Time = Start_Time; + + Duration_Moving = 0; + Duration = 0; + Distance = 0; + } + + LastFix_Time = End_Time; + + End_Latitude = location.getLocation().getLatitude(); + End_Longitude = location.getLocation().getLongitude(); + if (location.getLocation().hasAltitude()) { + End_Altitude = location.getLocation().getAltitude(); + } else { + End_Altitude = NOT_AVAILABLE; + } + End_EGMAltitudeCorrection = location.getAltitudeEGM96Correction(); + + End_Speed = location.getLocation().hasSpeed() ? location.getLocation().getSpeed() : NOT_AVAILABLE; + End_Accuracy = location.getLocation().hasAccuracy() ? location.getLocation().getAccuracy() : STANDARD_ACCURACY; + End_Time = location.getLocation().getTime(); + + if (End_EGMAltitudeCorrection == NOT_AVAILABLE) getEnd_EGMAltitudeCorrection(); + if (Start_EGMAltitudeCorrection == NOT_AVAILABLE) getStart_EGMAltitudeCorrection(); + + // ---------------------------------------------- Load the new value into antispikes filter + if (End_Altitude != NOT_AVAILABLE) AltitudeFilter.load(End_Time, End_Altitude); + + // ------------------------------------------------------------- Coords for thumb and stats + + if (ValidMap != 0) { + if (End_Latitude > Max_Latitude) Max_Latitude = End_Latitude; + if (End_Longitude > Max_Longitude) Max_Longitude = End_Longitude; + if (End_Latitude < Min_Latitude) Min_Latitude = End_Latitude; + if (End_Longitude < Min_Longitude) Min_Longitude = End_Longitude; + + if (Math.abs(LastStepDistance_Longitude - End_Longitude) > 90) ValidMap = 0; + // YOU PASS FROM -180 TO +180, OR REVERSE. iN THE PACIFIC OCEAN. + // in that case the app doesn't generate the thumb map. + } + + // ---------------------------------------------------------------------------------- Times + + Duration = End_Time - Start_Time; + if (End_Speed >= MOVEMENT_SPEED_THRESHOLD) Duration_Moving += End_Time - LastFix_Time; + + // --------------------------- Spaces (Distances) increment if distance > sum of accuracies + + // -- Temp locations for "DistanceTo" + Location LastStepDistanceLoc = new Location("TEMP"); + LastStepDistanceLoc.setLatitude(LastStepDistance_Latitude); + LastStepDistanceLoc.setLongitude(LastStepDistance_Longitude); + + Location EndLoc = new Location("TEMP"); + EndLoc.setLatitude(End_Latitude); + EndLoc.setLongitude(End_Longitude); + // ----------------------------------- + + DistanceInProgress = LastStepDistanceLoc.distanceTo(EndLoc); + float DeltaDistancePlusAccuracy = DistanceInProgress + End_Accuracy; + + if (DeltaDistancePlusAccuracy < DistanceInProgress + End_Accuracy) { + LastStepDistance_Accuracy = DeltaDistancePlusAccuracy; + //Log.w("myApp", "[#] Track.java - LastStepDistance_Accuracy updated to " + LastStepDistance_Accuracy ); + } + + if (DistanceInProgress > End_Accuracy + LastStepDistance_Accuracy) { + Distance += DistanceInProgress; + if (DistanceLastAltitude != NOT_AVAILABLE) DistanceLastAltitude += DistanceInProgress; + DistanceInProgress = 0; + + LastStepDistance_Latitude = End_Latitude; + LastStepDistance_Longitude = End_Longitude; + LastStepDistance_Accuracy = End_Accuracy; + } + + // Found a first fix with altitude!! + if ((End_Altitude != NOT_AVAILABLE) && (DistanceLastAltitude == NOT_AVAILABLE)) { + DistanceLastAltitude = 0; + Altitude_Up = 0; + Altitude_Down = 0; + if (Start_Altitude == NOT_AVAILABLE) Start_Altitude = End_Altitude; + LastStepAltitude_Altitude = End_Altitude; + LastStepAltitude_Accuracy = End_Accuracy; + } + + if ((LastStepAltitude_Altitude != NOT_AVAILABLE) && (End_Altitude != NOT_AVAILABLE)) { + Altitude_InProgress = End_Altitude - LastStepAltitude_Altitude; + // Improve last step accuracy in case of new data elements: + float DeltaAltitudePlusAccuracy = (float) Math.abs(Altitude_InProgress) + End_Accuracy; + if (DeltaAltitudePlusAccuracy <= LastStepAltitude_Accuracy) { + LastStepAltitude_Accuracy = DeltaAltitudePlusAccuracy; + DistanceLastAltitude = 0; + //Log.w("myApp", "[#] Track.java - LastStepAltitude_Accuracy updated to " + LastStepAltitude_Accuracy ); + } + // Evaluate the altitude step convalidation: + if ((Math.abs(Altitude_InProgress) > MIN_ALTITUDE_STEP) && AltitudeFilter.isValid() + && ((float) Math.abs(Altitude_InProgress) > (SECURITY_COEFF * (LastStepAltitude_Accuracy + End_Accuracy)))) { + // Altitude step: + // increment distance only if the inclination is relevant (assume deltah=20m in max 5000m) + if (DistanceLastAltitude < 5000) { + float hypotenuse = (float) Math.sqrt((double) (DistanceLastAltitude * DistanceLastAltitude) + (Altitude_InProgress * Altitude_InProgress)); + Distance = Distance + hypotenuse - DistanceLastAltitude; + //Log.w("myApp", "[#] Track.java - Distance += " + (hypotenuse - DistanceLastAltitude)); + } + //Reset variables + LastStepAltitude_Altitude = End_Altitude; + LastStepAltitude_Accuracy = End_Accuracy; + DistanceLastAltitude = 0; + + if (Altitude_InProgress > 0) Altitude_Up += Altitude_InProgress; // Increment the correct value of Altitude UP/DOWN + else Altitude_Down -= Altitude_InProgress; + Altitude_InProgress = 0; + } + + } + + // --------------------------------------------------------------------------------- Speeds + + if ((End_Speed != NOT_AVAILABLE) && (End_Speed > SpeedMax)) SpeedMax = End_Speed; + if (Duration > 0) SpeedAverage = (Distance + DistanceInProgress) / (((float) Duration) / 1000f); + if (Duration_Moving > 0) SpeedAverageMoving = (Distance + DistanceInProgress) / (((float) Duration_Moving) / 1000f); + NumberOfLocations++; + } + + // Empty constructor + public Track(){ + } + + // constructor + public Track(String Name){ + this.Name = Name; + } + + public void FromDB(long id, String Name, String From, String To, + double Start_Latitude, double Start_Longitude, double Start_Altitude, + float Start_Accuracy, float Start_Speed, long Start_Time, long LastFix_Time, + double End_Latitude, double End_Longitude, double End_Altitude, + float End_Accuracy, float End_Speed, long End_Time, + double LastStepDistance_Latitude, double LastStepDistance_Longitude, float LastStepDistance_Accuracy, + double LastStepAltitude_Altitude, float LastStepAltitude_Accuracy, + double Min_Latitude, double Min_Longitude, + double Max_Latitude, double Max_Longitude, + long Duration, long Duration_Moving, float Distance, float DistanceInProgress, + long DistanceLastAltitude, double Altitude_Up, double Altitude_Down, + double Altitude_InProgress, float SpeedMax, float SpeedAverage, + float SpeedAverageMoving, long NumberOfLocations, long NumberOfPlacemarks, + int ValidMap, int Type) { + this.id = id; + this.Name = Name; + + this.Start_Latitude = Start_Latitude; + this.Start_Longitude = Start_Longitude; + this.Start_Altitude = Start_Altitude; + this.Start_Accuracy = Start_Accuracy; + this.Start_Speed = Start_Speed; + this.Start_Time = Start_Time; + + this.LastFix_Time = LastFix_Time; + + this.End_Latitude = End_Latitude; + this.End_Longitude = End_Longitude; + this.End_Altitude = End_Altitude; + this.End_Accuracy = End_Accuracy; + this.End_Speed = End_Speed; + this.End_Time = End_Time; + + this.LastStepDistance_Latitude = LastStepDistance_Latitude; + this.LastStepDistance_Longitude = LastStepDistance_Longitude; + this.LastStepDistance_Accuracy = LastStepDistance_Accuracy; + + this.LastStepAltitude_Altitude = LastStepAltitude_Altitude; + this.LastStepAltitude_Accuracy = LastStepAltitude_Accuracy; + + this.Min_Latitude = Min_Latitude; + this.Min_Longitude = Min_Longitude; + + this.Max_Latitude = Max_Latitude; + this.Max_Longitude = Max_Longitude; + + this.Duration = Duration; + this.Duration_Moving = Duration_Moving; + + this.Distance = Distance; + this.DistanceInProgress = DistanceInProgress; + this.DistanceLastAltitude = DistanceLastAltitude; + + this.Altitude_Up = Altitude_Up; + this.Altitude_Down = Altitude_Down; + this.Altitude_InProgress = Altitude_InProgress; + + this.SpeedMax = SpeedMax; + this.SpeedAverage = SpeedAverage; + this.SpeedAverageMoving = SpeedAverageMoving; + + this.NumberOfLocations = NumberOfLocations; + this.NumberOfPlacemarks = NumberOfPlacemarks; + + this.ValidMap = ValidMap; + this.Type = Type; + + EGM96 egm96 = EGM96.getInstance(); + if (egm96 != null) { + if (egm96.isEGMGridLoaded()) { + if (Start_Latitude != NOT_AVAILABLE) Start_EGMAltitudeCorrection = egm96.getEGMCorrection(Start_Latitude, Start_Longitude); + if (End_Latitude != NOT_AVAILABLE) End_EGMAltitudeCorrection = egm96.getEGMCorrection(End_Latitude, End_Longitude); + } + } + } + + + // ------------------------------------------------------------------------ Getters and Setters + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getName() { + return Name; + } + + public void setName(String name) { + Name = name; + } + + public double getStart_Latitude() { + return Start_Latitude; + } + + public double getStart_Longitude() { + return Start_Longitude; + } + + public double getStart_Altitude() { + return Start_Altitude; + } + + public double getStart_EGMAltitudeCorrection() { + + if (Start_EGMAltitudeCorrection == NOT_AVAILABLE) { + EGM96 egm96 = EGM96.getInstance(); + if (egm96 != null) { + if (egm96.isEGMGridLoaded()) { + if (Start_Latitude != NOT_AVAILABLE) + Start_EGMAltitudeCorrection = egm96.getEGMCorrection(Start_Latitude, Start_Longitude); + } + } + } + return Start_EGMAltitudeCorrection; + } + + public float getStart_Accuracy() { + return Start_Accuracy; + } + + public float getStart_Speed() { + return Start_Speed; + } + + public long getStart_Time() { + return Start_Time; + } + + public long getLastFix_Time() { + return LastFix_Time; + } + + public double getEnd_Latitude() { + return End_Latitude; + } + + public double getEnd_Longitude() { + return End_Longitude; + } + + public double getEnd_Altitude() { + return End_Altitude; + } + + public double getEnd_EGMAltitudeCorrection() { + if (End_EGMAltitudeCorrection == NOT_AVAILABLE) { + EGM96 egm96 = EGM96.getInstance(); + if (egm96 != null) { + if (egm96.isEGMGridLoaded()) { + if (End_Latitude != NOT_AVAILABLE) + End_EGMAltitudeCorrection = egm96.getEGMCorrection(End_Latitude, End_Longitude); + } + } + } + return End_EGMAltitudeCorrection; + } + + public float getEnd_Accuracy() { + return End_Accuracy; + } + + public float getEnd_Speed() { + return End_Speed; + } + + public long getEnd_Time() { + return End_Time; + } + + public double getLastStepDistance_Latitude() { + return LastStepDistance_Latitude; + } + + public double getLastStepDistance_Longitude() { + return LastStepDistance_Longitude; + } + + public float getLastStepDistance_Accuracy() { + return LastStepDistance_Accuracy; + } + + public double getLastStepAltitude_Altitude() { + return LastStepAltitude_Altitude; + } + + public float getLastStepAltitude_Accuracy() { + return LastStepAltitude_Accuracy; + } + + public double getMin_Latitude() { + return Min_Latitude; + } + + public double getMin_Longitude() { + return Min_Longitude; + } + + public double getMax_Latitude() { + return Max_Latitude; + } + + public double getMax_Longitude() { + return Max_Longitude; + } + + public long getDuration() { + return Duration; + } + + public long getDuration_Moving() { + return Duration_Moving; + } + + public float getDistance() { + return Distance; + } + + public float getDistanceInProgress() { + return DistanceInProgress; + } + + public long getDistanceLastAltitude() { + return DistanceLastAltitude; + } + + public double getAltitude_Up() { + return Altitude_Up; + } + + public double getAltitude_Down() { + return Altitude_Down; + } + + public double getAltitude_InProgress() { + return Altitude_InProgress; + } + + public float getSpeedMax() { + return SpeedMax; + } + + public float getSpeedAverage() { + return SpeedAverage; + } + + public float getSpeedAverageMoving() { + return SpeedAverageMoving; + } + + public long getNumberOfLocations() { + return NumberOfLocations; + } + + public long getNumberOfPlacemarks() { + return NumberOfPlacemarks; + } + + public int getValidMap() { + return ValidMap; + } + + public int getType() { + return Type; + } + + public boolean isSelected() { + return Selected; + } + + public void setSelected(boolean selected) { + Selected = selected; + } + + // -------------------------------------------------------------------------------------------- + + public boolean isValidAltitude() { + return AltitudeFilter.isValid(); + } + + public long addPlacemark(LocationExtended location) { + this.NumberOfPlacemarks++ ; + + if (Name.equals("")) { + SimpleDateFormat df2 = new SimpleDateFormat("yyyyMMdd-HHmmss"); + Name = df2.format(location.getLocation().getTime()); + } + + return NumberOfPlacemarks; + } + + public float getEstimatedDistance(){ + if (NumberOfLocations == 0) return NOT_AVAILABLE; + if (NumberOfLocations == 1) return 0; + return Distance + DistanceInProgress; + } + + + public double getEstimatedAltitudeUp(boolean EGMCorrection){ + // Retrieve EGM Corrections if available + if ((Start_EGMAltitudeCorrection == NOT_AVAILABLE) || (End_EGMAltitudeCorrection == NOT_AVAILABLE)) { + EGM96 egm96 = EGM96.getInstance(); + if (egm96 != null) { + if (egm96.isEGMGridLoaded()) { + if (Start_Latitude != NOT_AVAILABLE) Start_EGMAltitudeCorrection = egm96.getEGMCorrection(Start_Latitude, Start_Longitude); + if (End_Latitude != NOT_AVAILABLE) End_EGMAltitudeCorrection = egm96.getEGMCorrection(End_Latitude, End_Longitude); + } + } + } + double egmcorr = 0; + if ((EGMCorrection) && ((Start_EGMAltitudeCorrection != NOT_AVAILABLE) && (End_EGMAltitudeCorrection != NOT_AVAILABLE))) { + egmcorr = Start_EGMAltitudeCorrection - End_EGMAltitudeCorrection; + } + double dresultUp = Altitude_InProgress > 0 ? Altitude_Up + Altitude_InProgress : Altitude_Up; + dresultUp -= egmcorr < 0 ? egmcorr : 0; + double dresultDown = Altitude_InProgress < 0 ? Altitude_Down - Altitude_InProgress : Altitude_Down; + dresultDown -= egmcorr > 0 ? egmcorr : 0; + + if (dresultUp < 0) { + dresultDown -= dresultUp; + dresultUp = 0; + } + if (dresultDown < 0) { + dresultUp -= dresultDown; + //dresultDown = 0; + } + return dresultUp; + } + + + public double getEstimatedAltitudeDown(boolean EGMCorrection){ + // Retrieve EGM Corrections if available + if ((Start_EGMAltitudeCorrection == NOT_AVAILABLE) || (End_EGMAltitudeCorrection == NOT_AVAILABLE)) { + EGM96 egm96 = EGM96.getInstance(); + if (egm96 != null) { + if (egm96.isEGMGridLoaded()) { + if (Start_Latitude != NOT_AVAILABLE) Start_EGMAltitudeCorrection = egm96.getEGMCorrection(Start_Latitude, Start_Longitude); + if (End_Latitude != NOT_AVAILABLE) End_EGMAltitudeCorrection = egm96.getEGMCorrection(End_Latitude, End_Longitude); + } + } + } + double egmcorr = 0; + if ((EGMCorrection) && ((Start_EGMAltitudeCorrection != NOT_AVAILABLE) && (End_EGMAltitudeCorrection != NOT_AVAILABLE))) { + egmcorr = Start_EGMAltitudeCorrection - End_EGMAltitudeCorrection; + } + double dresultUp = Altitude_InProgress > 0 ? Altitude_Up + Altitude_InProgress : Altitude_Up; + dresultUp -= egmcorr < 0 ? egmcorr : 0; + double dresultDown = Altitude_InProgress < 0 ? Altitude_Down - Altitude_InProgress : Altitude_Down; + dresultDown -= egmcorr > 0 ? egmcorr : 0; + + if (dresultUp < 0) { + dresultDown -= dresultUp; + dresultUp = 0; + } + if (dresultDown < 0) { + //dresultUp -= dresultDown; + dresultDown = 0; + } + return dresultDown; + } + + public double getEstimatedAltitudeGap(boolean EGMCorrection){ + return getEstimatedAltitudeUp(EGMCorrection) - getEstimatedAltitudeDown(EGMCorrection); + } + + + public float getBearing() { + if (End_Latitude != NOT_AVAILABLE) { + if (((Start_Latitude == End_Latitude) && (Start_Longitude == End_Longitude)) || (Distance == 0)) + return NOT_AVAILABLE; + Location EndLoc = new Location("TEMP"); + EndLoc.setLatitude(End_Latitude); + EndLoc.setLongitude(End_Longitude); + Location StartLoc = new Location("TEMP"); + StartLoc.setLatitude(Start_Latitude); + StartLoc.setLongitude(Start_Longitude); + float BTo = StartLoc.bearingTo(EndLoc); + if (BTo < 0) BTo += 360f; + return BTo; + } + return NOT_AVAILABLE; + } + + + // Returns the time, based on preferences (Total or Moving) + public long getPrefTime() { + GPSApplication gpsApplication = GPSApplication.getInstance(); + int pTime = gpsApplication.getPrefShowTrackStatsType(); + switch (pTime) { + case 0: // Total based + return Duration; + case 1: // Moving based + return Duration_Moving; + default: + return Duration; + } + } + + + // Returns the average speed, based on preferences (Total or Moving) + public float getPrefSpeedAverage() { + if (NumberOfLocations == 0) return NOT_AVAILABLE; + GPSApplication gpsApplication = GPSApplication.getInstance(); + int pTime = gpsApplication.getPrefShowTrackStatsType(); + switch (pTime) { + case 0: // Total based + return SpeedAverage; + case 1: // Moving based + return SpeedAverageMoving; + default: + return SpeedAverage; + } + } + + + public int getTrackType() { + + //if (Type != TRACK_TYPE_ND) return Type; + + if ((Distance == NOT_AVAILABLE) || (SpeedMax == NOT_AVAILABLE)) { + if (NumberOfPlacemarks == 0) return TRACK_TYPE_ND; + else return TRACK_TYPE_STEADY; + } + if ((Distance < 15.0f) || (SpeedMax == 0.0f) || (SpeedAverageMoving == NOT_AVAILABLE)) return TRACK_TYPE_STEADY; + if (SpeedMax < (7.0f / 3.6f)) { + if ((Altitude_Up != NOT_AVAILABLE) && (Altitude_Down != NOT_AVAILABLE)) + if ((Altitude_Down + Altitude_Up > (0.1f * Distance)) && (Distance > 500.0f)) return TRACK_TYPE_MOUNTAIN; + else return TRACK_TYPE_WALK; + } + if (SpeedMax < (15.0f / 3.6f)) { + if (SpeedAverageMoving > 8.0f / 3.6f) return TRACK_TYPE_RUN; + else { + if ((Altitude_Up != NOT_AVAILABLE) && (Altitude_Down != NOT_AVAILABLE)) + if ((Altitude_Down + Altitude_Up > (0.1f * Distance)) && (Distance > 500.0f)) return TRACK_TYPE_MOUNTAIN; + else return TRACK_TYPE_WALK; + } + } + if (SpeedMax < (50.0f / 3.6f)) { + if ((SpeedAverageMoving + SpeedMax) / 2 > 35.0f / 3.6f) return TRACK_TYPE_CAR; + if ((SpeedAverageMoving + SpeedMax) / 2 > 20.0f / 3.6) return TRACK_TYPE_BICYCLE; + else if ((SpeedAverageMoving + SpeedMax) / 2 > 12.0f / 3.6f) return TRACK_TYPE_RUN; + else { + if ((Altitude_Up != NOT_AVAILABLE) && (Altitude_Down != NOT_AVAILABLE)) + if ((Altitude_Down + Altitude_Up > (0.1f * Distance)) && (Distance > 500.0f)) + return TRACK_TYPE_MOUNTAIN; + else return TRACK_TYPE_WALK; + } + /* + if (SpeedAverageMoving > 20.0f / 3.6f) return TRACK_TYPE_CAR; + if (SpeedAverageMoving > 12.0f / 3.6) return TRACK_TYPE_BICYCLE; + else if (SpeedAverageMoving > 8.0f / 3.6f) return TRACK_TYPE_RUN; + else { + if ((Altitude_Up != NOT_AVAILABLE) && (Altitude_Down != NOT_AVAILABLE)) + if ((Altitude_Down + Altitude_Up > (0.1f * Distance)) && (Distance > 500.0f)) + return TRACK_TYPE_MOUNTAIN; + else return TRACK_TYPE_WALK; + }*/ + } + if ((Altitude_Up != NOT_AVAILABLE) && (Altitude_Down != NOT_AVAILABLE)) + if ((Altitude_Down + Altitude_Up > 5000.0) && (SpeedMax > 300.0f / 3.6f)) return TRACK_TYPE_FLIGHT; + + return TRACK_TYPE_CAR; + } +} diff --git a/app/src/main/java/fr/geolabs/dev/mapmint4me/MapMint4ME.java b/app/src/main/java/fr/geolabs/dev/mapmint4me/MapMint4ME.java index 555324a..6a11842 100644 --- a/app/src/main/java/fr/geolabs/dev/mapmint4me/MapMint4ME.java +++ b/app/src/main/java/fr/geolabs/dev/mapmint4me/MapMint4ME.java @@ -1,12 +1,10 @@ package fr.geolabs.dev.mapmint4me; -import android.*; import android.Manifest; import android.annotation.SuppressLint; import android.app.Activity; import android.app.NotificationChannel; import android.app.NotificationManager; -import android.app.PendingIntent; import android.content.ContentValues; import android.content.Context; import android.content.Intent; @@ -15,8 +13,6 @@ import android.content.pm.ResolveInfo; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; import android.hardware.GeomagneticField; import android.hardware.Sensor; import android.hardware.SensorEvent; @@ -24,42 +20,24 @@ import android.hardware.SensorManager; import android.location.Location; import android.location.LocationManager; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; import android.net.Uri; -import android.net.http.SslError; import android.os.Build; -import android.os.Environment; -import android.os.ParcelFileDescriptor; +import android.os.Bundle; +import android.os.Handler; import android.provider.MediaStore; -import android.provider.SyncStateContract; import android.support.v4.app.ActivityCompat; -import android.support.v4.app.NotificationCompat; -import android.support.v4.app.NotificationManagerCompat; import android.support.v4.content.ContextCompat; import android.support.v4.content.FileProvider; -import android.support.v7.app.ActionBar; -import android.support.v7.app.AppCompatActivity; -import android.os.Bundle; -import android.os.Handler; -import android.support.v7.widget.AlertDialogLayout; -import android.text.format.DateFormat; import android.util.Log; -import android.view.Gravity; import android.view.MotionEvent; import android.view.View; -import android.view.Window; import android.webkit.CookieManager; -import android.webkit.CookieSyncManager; import android.webkit.GeolocationPermissions; import android.webkit.JavascriptInterface; -import android.webkit.SslErrorHandler; import android.webkit.WebChromeClient; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; -import android.widget.ImageView; -import android.widget.LinearLayout; import android.widget.Toast; import com.google.android.gms.appindexing.Action; @@ -73,7 +51,6 @@ import java.io.BufferedOutputStream; import java.io.File; -import java.io.FileDescriptor; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; @@ -84,12 +61,6 @@ import java.util.List; import java.util.Locale; import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -import static android.provider.MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE; -import static java.lang.Thread.sleep; - - /** @@ -341,6 +312,11 @@ public void launchWelcomeScreen3() { finish(); } + // public void launchWelcomeScreen5() { + // startActivity(new Intent(getApplicationContext(), GPSActivity_sat.class)); + // finish(); + // } + public boolean mInternetActivated=false; public boolean isInternetActivated(){ @@ -527,6 +503,12 @@ public void onAccuracyChanged(Sensor sensor, int accuracy) { // not in use } + public void launchWelcomeScreen7() { + startActivity(new Intent(getApplicationContext(), Satellite.GPSActivity.class)); + finish(); + + + } /** diff --git a/app/src/main/java/fr/geolabs/dev/mapmint4me/WebAppInterface.java b/app/src/main/java/fr/geolabs/dev/mapmint4me/WebAppInterface.java index 997142e..f325a08 100644 --- a/app/src/main/java/fr/geolabs/dev/mapmint4me/WebAppInterface.java +++ b/app/src/main/java/fr/geolabs/dev/mapmint4me/WebAppInterface.java @@ -1,10 +1,8 @@ package fr.geolabs.dev.mapmint4me; -import android.*; import android.accounts.Account; import android.accounts.AccountManager; import android.annotation.TargetApi; -import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; @@ -41,25 +39,19 @@ import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; -import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; -import java.io.PrintStream; import java.io.PrintWriter; import java.io.StringWriter; -import java.net.HttpCookie; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLConnection; -import java.util.Arrays; -import java.util.List; import java.util.Locale; import java.util.regex.Pattern; @@ -588,6 +580,14 @@ public void startWelcomeScreen(String s) { ((MapMint4ME) mContext).launchWelcomeScreen4(); ((MapMint4ME) mContext).finish(); } + if (s.equals("sat_finder")) { + + ((MapMint4ME) mContext).launchWelcomeScreen7(); + ((MapMint4ME) mContext).finish(); + } + + + if (s.equals("help")) { ((MapMint4ME) mContext).launchWelcomeScreen(); ((MapMint4ME) mContext).finish(); diff --git a/app/src/main/java/fr/geolabs/dev/mapmint4me/WelcomeScreen.java b/app/src/main/java/fr/geolabs/dev/mapmint4me/WelcomeScreen.java index 23e92d3..a382dcb 100644 --- a/app/src/main/java/fr/geolabs/dev/mapmint4me/WelcomeScreen.java +++ b/app/src/main/java/fr/geolabs/dev/mapmint4me/WelcomeScreen.java @@ -2,23 +2,19 @@ import android.annotation.SuppressLint; import android.app.Activity; -import android.content.Intent; -import android.os.Bundle; -import android.support.v7.app.AppCompatActivity; - import android.content.Context; import android.content.Intent; +import android.os.Bundle; +import android.support.v4.view.PagerAdapter; +import android.support.v4.view.ViewPager; +import android.text.Html; import android.util.Log; import android.view.LayoutInflater; -import android.view.ViewGroup; import android.view.View; -import android.text.Html; +import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; import android.widget.Button; -import android.os.Bundle; -import android.support.v4.view.PagerAdapter; -import android.support.v4.view.ViewPager; import android.widget.LinearLayout; import android.widget.TextView; diff --git a/app/src/main/res/drawable/launch_screen.xml b/app/src/main/res/drawable/launch_screen.xml new file mode 100644 index 0000000..f77c2eb --- /dev/null +++ b/app/src/main/res/drawable/launch_screen.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/scrollbar_vertical_thumb.xml b/app/src/main/res/drawable/scrollbar_vertical_thumb.xml new file mode 100644 index 0000000..aef8cd2 --- /dev/null +++ b/app/src/main/res/drawable/scrollbar_vertical_thumb.xml @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/scrollbar_vertical_track.xml b/app/src/main/res/drawable/scrollbar_vertical_track.xml new file mode 100644 index 0000000..e98305c --- /dev/null +++ b/app/src/main/res/drawable/scrollbar_vertical_track.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_gps.xml b/app/src/main/res/layout/activity_gps.xml new file mode 100644 index 0000000..22e700d --- /dev/null +++ b/app/src/main/res/layout/activity_gps.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_gpsfix.xml b/app/src/main/res/layout/fragment_gpsfix.xml new file mode 100644 index 0000000..bd7648c --- /dev/null +++ b/app/src/main/res/layout/fragment_gpsfix.xml @@ -0,0 +1,450 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout.xml b/app/src/main/res/layout/layout.xml new file mode 100644 index 0000000..85dc9f7 --- /dev/null +++ b/app/src/main/res/layout/layout.xml @@ -0,0 +1,90 @@ + + + + +