limit the length of track sections to paint at once on app load; implement a display...
[lantea.git] / js / map.js
index ccfa87d419f74d138bd2d67923d72fbad74ce8f7..f9ce632f83c9ecdb2d497bc159b16025a52d0177 100644 (file)
--- a/js/map.js
+++ b/js/map.js
@@ -72,11 +72,19 @@ function initMap() {
               gGLMapCanvas.getContext("experimental-webgl", {depth: false});
   }
   catch(e) {}
-  // If we don't have a GL context, give up now
   if (!gMap.gl) {
+    // If we don't have a GL context, give up now
     showGLWarningDialog();
     gMap.gl = null;
   }
+  else {
+    // GL context can be lost at any time, handle that.
+    // See http://www.khronos.org/webgl/wiki/HandlingContextLost
+    gGLMapCanvas.addEventListener("webglcontextlost",
+                                  gMap.handleContextLost, false);
+    gGLMapCanvas.addEventListener("webglcontextrestored",
+                                  gMap.handleContextRestored, false);
+  }
   gTrackCanvas = document.getElementById("track");
   gTrackContext = gTrackCanvas.getContext("2d");
   if (!gMap.activeMap)
@@ -121,15 +129,21 @@ function loadPrefs(aEvent) {
 
       document.getElementById("body").addEventListener("keydown", mapEvHandler, false);
 
+      document.addEventListener("visibilitychange", visibilityEvHandler, false);
+
+      console.log("Events added.");
       document.getElementById("copyright").innerHTML =
           gMapStyles[gMap.activeMap].copyright;
 
+      console.log("Init loading tile...");
       gLoadingTile = new Image();
-      gLoadingTile.src = "style/loading.png";
       gLoadingTile.onload = function() {
+        console.log("Loading tile loaded.");
         var throwEv = new CustomEvent("prefload-done");
         gAction.dispatchEvent(throwEv);
       };
+      console.log("Set loading tile...");
+      gLoadingTile.src = "style/loading.png";
     }
   }
   else {
@@ -173,11 +187,14 @@ function loadPrefs(aEvent) {
       if (aTPoint) {
         // Add in front and return new length.
         var tracklen = gTrack.unshift(aTPoint);
-        // Redraw track periodically, larger distance the longer it gets.
+        // Redraw track periodically, larger distance the longer it gets
+        // (but clamped to the first value over a certain limit).
         // Initial paint will do initial track drawing.
         if (tracklen % redrawBase == 0) {
           drawTrack();
-          redrawBase = tracklen;
+          if (redrawBase < 1000) {
+            redrawBase = tracklen;
+          }
         }
       }
       else {
@@ -235,7 +252,7 @@ var gMap = {
     '  gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);\n' +
     '  vTextureCoord = aTextureCoord;\n' +
     '}'; },
-  getFragShaderSource:function() {
+  getFragShaderSource: function() {
     return 'varying highp vec2 vTextureCoord;\n\n' +
     'uniform sampler2D uImage;\n\n' +
     'void main(void) {\n' +
@@ -244,12 +261,14 @@ var gMap = {
 
   initGL: function() {
     // When called from the event listener, the "this" reference doesn't work, so use the object name.
+    console.log("Initializing WebGL...");
     if (gMap.gl) {
       gMap.gl.viewport(0, 0, gMap.gl.drawingBufferWidth, gMap.gl.drawingBufferHeight);
       gMap.gl.clearColor(0.0, 0.0, 0.0, 0.5);                          // Set clear color to black, fully opaque.
       gMap.gl.clear(gMap.gl.COLOR_BUFFER_BIT|gMap.gl.DEPTH_BUFFER_BIT);  // Clear the color.
 
       // Create and initialize the shaders.
+      console.log("Create and compile shaders...");
       var vertShader = gMap.gl.createShader(gMap.gl.VERTEX_SHADER);
       var fragShader = gMap.gl.createShader(gMap.gl.FRAGMENT_SHADER);
       gMap.gl.shaderSource(vertShader, gMap.getVertShaderSource());
@@ -269,6 +288,7 @@ var gMap = {
         return null;
       }
 
+      console.log("Create and link shader program...");
       gMap.glShaderProgram = gMap.gl.createProgram();
       gMap.gl.attachShader(gMap.glShaderProgram, vertShader);
       gMap.gl.attachShader(gMap.glShaderProgram, fragShader);
@@ -283,6 +303,7 @@ var gMap = {
       gMap.glTextureCoordAttr = gMap.gl.getAttribLocation(gMap.glShaderProgram, "aTextureCoord");
       gMap.glResolutionAttr = gMap.gl.getUniformLocation(gMap.glShaderProgram, "uResolution");
 
+      console.log("Set up vertex buffer...");
       var tileVerticesBuffer = gMap.gl.createBuffer();
       gMap.gl.bindBuffer(gMap.gl.ARRAY_BUFFER, tileVerticesBuffer);
       // The vertices are the coordinates of the corner points of the square.
@@ -303,6 +324,7 @@ var gMap = {
       gMap.gl.uniform2f(gMap.glResolutionAttr, gGLMapCanvas.width, gGLMapCanvas.height);
 
       // Create a buffer for the position of the rectangle corners.
+      console.log("Set up coord buffer...");
       var mapVerticesTextureCoordBuffer = gMap.gl.createBuffer();
       gMap.gl.bindBuffer(gMap.gl.ARRAY_BUFFER, mapVerticesTextureCoordBuffer);
       gMap.gl.enableVertexAttribArray(gMap.glVertexPositionAttr);
@@ -312,8 +334,13 @@ var gMap = {
       gMap.glTxCleanIntervalID = window.setInterval(gMap.cleanTextures, 30 * 1000);
     }
 
-    var throwEv = new CustomEvent("mapinit-done");
-    gAction.dispatchEvent(throwEv);
+    if (!gAppInitDone) {
+      // We may be called when context was lost and destroyed,
+      // only send event when we are in app startup
+      // (gAppInitDone is set to true right after we return this event).
+      var throwEv = new CustomEvent("mapinit-done");
+      gAction.dispatchEvent(throwEv);
+    }
   },
 
   draw: function() {
@@ -355,7 +382,9 @@ var gMap = {
               var imgObj = new Image();
               imgObj.onload = function() {
                 gMap.loadImageToTexture(imgObj, aTileKey);
-                requestAnimationFrame(function(aTimestamp) { gMap.drawGL() });
+                if (document.hidden != true) { // Only draw if we're actually visible.
+                  window.requestAnimationFrame(function(aTimestamp) { gMap.drawGL() });
+                }
                 URL.revokeObjectURL(imgURL);
               }
               imgObj.src = imgURL;
@@ -364,7 +393,9 @@ var gMap = {
         }
       }
     }
-    requestAnimationFrame(function(aTimestamp) { gMap.drawGL() });
+    if (document.hidden != true) { // Only draw if we're actually visible.
+      window.requestAnimationFrame(function(aTimestamp) { gMap.drawGL() });
+    }
   },
 
   drawGL: function() {
@@ -469,8 +500,8 @@ var gMap = {
                                   y: Math.ceil((gMap.pos.y + gMap.baseDim.ht / 2) / gMap.baseDim.tsize) + 1,
                                   z: gMap.pos.z});
       console.log("In range: " + tMin.x + "," + tMin.y + "," + tMin.z + " - " + tMax.x + "," + tMax.y + "," + tMax.z);
-      for (aTileKey in gMap.glTextures) {
-        var keyMatches = aTileKey.match(/([^:]+)::(\d+),(\d+),(\d+)/);
+      for (var tileKey in gMap.glTextures) {
+        var keyMatches = tileKey.match(/([^:]+)::(\d+),(\d+),(\d+)/);
         if (keyMatches && keyMatches[1] != "loading") {
           var txData = {
             style: keyMatches[1],
@@ -497,15 +528,27 @@ var gMap = {
           }
           if (delTx) {
             // Delete texture from GL and from the array we are holding.
-            gMap.gl.deleteTexture(gMap.glTextures[aTileKey]);
-            delete gMap.glTextures[aTileKey];
+            gMap.gl.deleteTexture(gMap.glTextures[tileKey]);
+            delete gMap.glTextures[tileKey];
           }
         }
       }
       console.log("Cleaning complete, " + Object.keys(gMap.glTextures).length + " textures left)");
-      //clearInterval(gMap.glTxCleanIntervalID);
     }
   },
+
+  handleContextLost: function(event) {
+    event.preventDefault();
+    // GL context is gone, let's reset everything that depends on it.
+    clearInterval(gMap.glTxCleanIntervalID);
+    gMap.glTextures = {};
+  },
+
+  handleContextRestored: function(event) {
+    // When GL context is back, init GL again and draw.
+    gMap.initGL();
+    gMap.draw();
+  },
 }
 
 // Using scale(x, y) together with drawing old data on scaled canvas would be an improvement for zooming.
@@ -611,13 +654,15 @@ function decodeIndex(encodedIdx) {
 }
 
 function drawTrack() {
-  gLastDrawnPoint = null;
-  gCurPosMapCache = undefined;
-  gTrackContext.clearRect(0, 0, gTrackCanvas.width, gTrackCanvas.height);
-  if (gTrack.length) {
-    for (var i = 0; i < gTrack.length; i++) {
-      drawTrackPoint(gTrack[i].coords.latitude, gTrack[i].coords.longitude,
-                     (i + 1 >= gTrack.length));
+  if (gTrackContext && (document.hidden != true)) { // Only draw if we're actually visible.
+    gLastDrawnPoint = null;
+    gCurPosMapCache = undefined;
+    gTrackContext.clearRect(0, 0, gTrackCanvas.width, gTrackCanvas.height);
+    if (gTrack.length) {
+      for (var i = 0; i < gTrack.length; i++) {
+        drawTrackPoint(gTrack[i].coords.latitude, gTrack[i].coords.longitude,
+                      (i + 1 >= gTrack.length));
+      }
     }
   }
 }
@@ -695,6 +740,50 @@ function undrawCurrentLocation() {
   }
 }
 
+function calcTrackDuration() {
+  // Get the duration of the track in s.
+  var tDuration = 0;
+  if (gTrack.length > 1) {
+    for (var i = 1; i < gTrack.length; i++) {
+      tDuration += (gTrack[i].time - gTrack[i-1].time);
+    }
+  }
+  return Math.round(tDuration / 1000); // The timestamps are in ms but we return seconds.
+}
+
+function calcTrackLength() {
+  // Get the length of the track in km.
+  var tLength = 0;
+  if (gTrack.length > 1) {
+    for (var i = 1; i < gTrack.length; i++) {
+      tLength += getPointDistance(gTrack[i-1].coords, gTrack[i].coords);
+    }
+  }
+  return tLength;
+}
+
+function getPointDistance(aGPSPoint1, aGPSPoint2) {
+  // Get the distance in km between the two given GPS points.
+  // See http://stackoverflow.com/questions/365826/calculate-distance-between-2-gps-coordinates
+  // Earth is almost exactly a sphere and we calculate small distances on the surface, so we can do spherical great-circle math.
+  // Also see http://en.wikipedia.org/wiki/Great-circle_distance
+  var R = 6371; // km
+  var dLat = deg2rad(aGPSPoint2.latitude - aGPSPoint1.latitude);
+  var dLon = deg2rad(aGPSPoint2.longitude - aGPSPoint1.longitude);
+  var lat1 = deg2rad(aGPSPoint1.latitude);
+  var lat2 = deg2rad(aGPSPoint2.latitude);
+
+  var a = Math.sin(dLat/2) * Math.sin(dLat/2) +
+          Math.sin(dLon/2) * Math.sin(dLon/2) * Math.cos(lat1) * Math.cos(lat2);
+  var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
+  return R * c;
+}
+
+function deg2rad(aDegreeValue) {
+  // Convert an angle in degrees to radiants.
+  return aDegreeValue * (Math.PI / 180);
+}
+
 var mapEvHandler = {
   handleEvent: function(aEvent) {
     var touchEvent = aEvent.type.indexOf('touch') != -1;
@@ -926,6 +1015,15 @@ var mapEvHandler = {
   }
 };
 
+function visibilityEvHandler() {
+  // Immediately draw if we just got visible.
+  if (document.hidden != true) {
+    gMap.draw();
+  }
+  // No need to handle the event where we become invisible as we care only draw
+  // when we are visible anyhow.
+}
+
 var geofake = {
   tracking: false,
   lastPos: {x: undefined, y: undefined},
@@ -1069,8 +1167,8 @@ var gTileService = {
                   .replace("{x}", norm.x)
                   .replace("{y}", norm.y)
                   .replace("{z}", norm.z)
-                  .replace("[a-c]", String.fromCharCode(97 + Math.floor(Math.random() * 2)))
-                  .replace("[1-4]", 1 + Math.floor(Math.random() * 3)),
+                  .replace("[a-c]", String.fromCharCode(97 + Math.floor(Math.random() * 3)))
+                  .replace("[1-4]", 1 + Math.floor(Math.random() * 4)),
                 true);
       XHR.responseType = "blob";
       XHR.addEventListener("load", function () {