add first pieces of support for webGL maps
[lantea.git] / js / map.js
index ec60e01d905c6093cf8c6641feb8a98b2cfb637b..9d2ecdb9dad546160b857aa3bbc5dbad37659fe6 100644 (file)
--- a/js/map.js
+++ b/js/map.js
@@ -2,7 +2,7 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-var gMapCanvas, gMapContext, gTrackCanvas, gTrackContext, gGeolocation;
+var gMapCanvas, gMapContext, gGLMapCanvas, gMapGL, gTrackCanvas, gTrackContext, gGeolocation;
 var gDebug = false;
 
 var gTileSize = 256;
@@ -61,7 +61,7 @@ var gLoadingTile;
 var gMapPrefsLoaded = false;
 
 var gDragging = false;
-var gDragTouchID;
+var gDragTouchID, gPinchStartWidth;
 
 var gGeoWatchID;
 var gTrack = [];
@@ -72,8 +72,23 @@ var gCurPosMapCache;
 
 function initMap() {
   gGeolocation = navigator.geolocation;
+  // Set up canvas contexts. TODO: Remove 2D map once GL support works.
   gMapCanvas = document.getElementById("map");
   gMapContext = gMapCanvas.getContext("2d");
+  gGLMapCanvas = document.getElementById("glmap");
+  try {
+    // Try to grab the standard context. If it fails, fallback to experimental.
+    // We also try to tell it we do not need a depth buffer.
+    gMapGL = gGLMapCanvas.getContext("webgl", {depth: false}) ||
+             gGLMapCanvas.getContext("experimental-webgl", {depth: false});
+    gMapGL.viewport(0, 0, gMapGL.drawingBufferWidth, gMapGL.drawingBufferHeight);
+  }
+  catch(e) {}
+  // If we don't have a GL context, give up now
+  if (!gMapGL) {
+    showGLWarningDialog();
+    gMapGL = null;
+  }
   gTrackCanvas = document.getElementById("track");
   gTrackContext = gTrackCanvas.getContext("2d");
   if (!gActiveMap)
@@ -89,80 +104,227 @@ function initMap() {
     }
   }
 
-  var loopCnt = 0;
-  var getPersistentPrefs = function() {
-    if (mainDB) {
-      gWaitCounter++;
-      gPrefs.get("position", function(aValue) {
-        if (aValue) {
-          gPos = aValue;
-          gWaitCounter--;
-        }
-      });
-      gWaitCounter++;
-      gPrefs.get("center_map", function(aValue) {
-        if (aValue === undefined)
-          document.getElementById("centerCheckbox").checked = true;
-        else
-          document.getElementById("centerCheckbox").checked = aValue;
-        setCentering(document.getElementById("centerCheckbox"));
-        gWaitCounter--;
-      });
-      gWaitCounter++;
-      gPrefs.get("tracking_enabled", function(aValue) {
-        if (aValue === undefined)
-          document.getElementById("trackCheckbox").checked = true;
-        else
-          document.getElementById("trackCheckbox").checked = aValue;
-        gWaitCounter--;
-      });
-      gWaitCounter++;
-      gTrackStore.getList(function(aTPoints) {
-        if (gDebug)
-          console.log(aTPoints.length + " points loaded.");
-        if (aTPoints.length) {
-          gTrack = aTPoints;
+  gAction.addEventListener("prefload-done", initGL, false);
+
+  console.log("map vars set, loading prefs...");
+  loadPrefs();
+}
+
+function loadPrefs(aEvent) {
+  if (aEvent && aEvent.type == "prefs-step") {
+    console.log("wait: " + gWaitCounter);
+    if (gWaitCounter == 0) {
+      gAction.removeEventListener(aEvent.type, loadPrefs, false);
+      gMapPrefsLoaded = true;
+      console.log("prefs loaded.");
+
+      gTrackCanvas.addEventListener("mouseup", mapEvHandler, false);
+      gTrackCanvas.addEventListener("mousemove", mapEvHandler, false);
+      gTrackCanvas.addEventListener("mousedown", mapEvHandler, false);
+      gTrackCanvas.addEventListener("mouseout", mapEvHandler, false);
+
+      gTrackCanvas.addEventListener("touchstart", mapEvHandler, false);
+      gTrackCanvas.addEventListener("touchmove", mapEvHandler, false);
+      gTrackCanvas.addEventListener("touchend", mapEvHandler, false);
+      gTrackCanvas.addEventListener("touchcancel", mapEvHandler, false);
+      gTrackCanvas.addEventListener("touchleave", mapEvHandler, false);
+
+      gTrackCanvas.addEventListener("wheel", mapEvHandler, false);
+
+      document.getElementById("body").addEventListener("keydown", mapEvHandler, false);
+
+      document.getElementById("copyright").innerHTML =
+          gMapStyles[gActiveMap].copyright;
+
+      gLoadingTile = new Image();
+      gLoadingTile.src = "style/loading.png";
+      gLoadingTile.onload = function() {
+        var throwEv = new CustomEvent("prefload-done");
+        gAction.dispatchEvent(throwEv);
+      };
+    }
+  }
+  else {
+    if (aEvent)
+      gAction.removeEventListener(aEvent.type, loadPrefs, false);
+    gAction.addEventListener("prefs-step", loadPrefs, false);
+    gWaitCounter++;
+    gPrefs.get("position", function(aValue) {
+      if (aValue) {
+        gPos = aValue;
+      }
+      gWaitCounter--;
+      var throwEv = new CustomEvent("prefs-step");
+      gAction.dispatchEvent(throwEv);
+    });
+    gWaitCounter++;
+    gPrefs.get("center_map", function(aValue) {
+      if (aValue === undefined)
+        document.getElementById("centerCheckbox").checked = true;
+      else
+        document.getElementById("centerCheckbox").checked = aValue;
+      setCentering(document.getElementById("centerCheckbox"));
+      gWaitCounter--;
+      var throwEv = new CustomEvent("prefs-step");
+      gAction.dispatchEvent(throwEv);
+    });
+    gWaitCounter++;
+    gPrefs.get("tracking_enabled", function(aValue) {
+      if (aValue === undefined)
+        document.getElementById("trackCheckbox").checked = true;
+      else
+        document.getElementById("trackCheckbox").checked = aValue;
+      gWaitCounter--;
+      var throwEv = new CustomEvent("prefs-step");
+      gAction.dispatchEvent(throwEv);
+    });
+    gWaitCounter++;
+    var trackLoadStarted = false;
+    var redrawBase = 100;
+    gTrackStore.getListStepped(function(aTPoint) {
+      if (aTPoint) {
+        // Add in front and return new length.
+        var tracklen = gTrack.unshift(aTPoint);
+        // Redraw track periodically, larger distance the longer it gets.
+        // Initial paint will do initial track drawing.
+        if (tracklen % redrawBase == 0) {
+          drawTrack();
+          redrawBase = tracklen;
         }
+      }
+      else {
+        // Last point received.
+        drawTrack();
+      }
+      if (!trackLoadStarted) {
+        // We have the most recent point, if present, rest will load async.
+        trackLoadStarted = true;
         gWaitCounter--;
-      });
+        var throwEv = new CustomEvent("prefs-step");
+        gAction.dispatchEvent(throwEv);
+      }
+    });
+  }
+}
+
+function initGL() {
+  if (gMapGL) {
+    gMapGL.clearColor(0.0, 0.0, 0.0, 0.5);                          // Set clear color to black, fully opaque.
+    gMapGL.clear(gMapGL.COLOR_BUFFER_BIT|gMapGL.DEPTH_BUFFER_BIT);  // Clear the color.
+
+    // Create and initialize the shaders.
+    var vertShader = gMapGL.createShader(gMapGL.VERTEX_SHADER);
+    var vertShaderSource =
+      'attribute vec2 aVertexPosition;\n' +
+      'attribute vec2 aTextureCoord;\n\n' +
+      'uniform vec2 uResolution;\n\n' +
+      'varying highp vec2 vTextureCoord;\n\n' +
+      'void main(void) {\n' +
+      // convert the rectangle from pixels to -1.0 to +1.0 (clipspace) 0.0 to 1.0
+      '  vec2 clipSpace = aVertexPosition * 2.0 / uResolution - 1.0;\n' +
+      '  gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);\n' +
+      '  vTextureCoord = aTextureCoord;\n' +
+      '}';
+    var fragShader = gMapGL.createShader(gMapGL.FRAGMENT_SHADER);
+    var fragShaderSource =
+      'varying highp vec2 vTextureCoord;\n\n' +
+      'uniform sampler2D uImage;\n\n' +
+      'void main(void) {\n' +
+      '  gl_FragColor = texture2D(uImage, vTextureCoord);\n' +
+      '}';
+
+    gMapGL.shaderSource(vertShader, vertShaderSource);
+    // Compile the shader program.
+    gMapGL.compileShader(vertShader);
+    // See if it compiled successfully.
+    if (!gMapGL.getShaderParameter(vertShader, gMapGL.COMPILE_STATUS)) {
+      console.log("An error occurred compiling the vertix shader: " + gMapGL.getShaderInfoLog(vertShader));
+      return null;
     }
-    else
-      setTimeout(getPersistentPrefs, 100);
-    loopCnt++;
-    if (loopCnt > 50) {
-      console.log("Loading prefs failed.");
+    gMapGL.shaderSource(fragShader, fragShaderSource);
+    // Compile the shader program.
+    gMapGL.compileShader(fragShader);
+    // See if it compiled successfully.
+    if (!gMapGL.getShaderParameter(fragShader, gMapGL.COMPILE_STATUS)) {
+      console.log("An error occurred compiling the fragment shader: " + gMapGL.getShaderInfoLog(fragShader));
+      return null;
     }
-  };
-  getPersistentPrefs();
-
-  gTrackCanvas.addEventListener("mouseup", mapEvHandler, false);
-  gTrackCanvas.addEventListener("mousemove", mapEvHandler, false);
-  gTrackCanvas.addEventListener("mousedown", mapEvHandler, false);
-  gTrackCanvas.addEventListener("mouseout", mapEvHandler, false);
 
-  gTrackCanvas.addEventListener("touchstart", mapEvHandler, false);
-  gTrackCanvas.addEventListener("touchmove", mapEvHandler, false);
-  gTrackCanvas.addEventListener("touchend", mapEvHandler, false);
-  gTrackCanvas.addEventListener("touchcancel", mapEvHandler, false);
-  gTrackCanvas.addEventListener("touchleave", mapEvHandler, false);
-
-  gTrackCanvas.addEventListener("wheel", mapEvHandler, false);
-
-  document.getElementById("copyright").innerHTML =
-      gMapStyles[gActiveMap].copyright;
+    var shaderProgram = gMapGL.createProgram();
+    gMapGL.attachShader(shaderProgram, vertShader);
+    gMapGL.attachShader(shaderProgram, fragShader);
+    gMapGL.linkProgram(shaderProgram);
+    // If creating the shader program failed, alert
+    if (!gMapGL.getProgramParameter(shaderProgram, gMapGL.LINK_STATUS)) {
+      alert("Unable to initialize the shader program.");
+    }
+    gMapGL.useProgram(shaderProgram);
+    var vertexPositionAttribute = gMapGL.getAttribLocation(shaderProgram, "aVertexPosition");
+    var textureCoordAttribute = gMapGL.getAttribLocation(shaderProgram, "aTextureCoord");
+    var resolutionAttribute = gMapGL.getUniformLocation(shaderProgram, "uResolution");
+
+    var tileVerticesBuffer = gMapGL.createBuffer();
+    gMapGL.bindBuffer(gMapGL.ARRAY_BUFFER, tileVerticesBuffer);
+    // The vertices are the coordinates of the corner points of the square.
+    var vertices = [
+      0.0,  0.0,
+      1.0,  0.0,
+      0.0,  1.0,
+      0.0,  1.0,
+      1.0,  0.0,
+      1.0,  1.0];
+    gMapGL.bufferData(gMapGL.ARRAY_BUFFER, new Float32Array(vertices), gMapGL.STATIC_DRAW);
+    gMapGL.enableVertexAttribArray(textureCoordAttribute);
+    gMapGL.vertexAttribPointer(textureCoordAttribute, 2, gMapGL.FLOAT, false, 0, 0);
+
+    // Map Texture
+    var mapTexture = gMapGL.createTexture();
+    gMapGL.bindTexture(gMapGL.TEXTURE_2D, mapTexture);
+    // Set the parameters so we can render any size image.
+    gMapGL.texParameteri(gMapGL.TEXTURE_2D, gMapGL.TEXTURE_WRAP_S, gMapGL.CLAMP_TO_EDGE);
+    gMapGL.texParameteri(gMapGL.TEXTURE_2D, gMapGL.TEXTURE_WRAP_T, gMapGL.CLAMP_TO_EDGE);
+    gMapGL.texParameteri(gMapGL.TEXTURE_2D, gMapGL.TEXTURE_MIN_FILTER, gMapGL.NEAREST);
+    gMapGL.texParameteri(gMapGL.TEXTURE_2D, gMapGL.TEXTURE_MAG_FILTER, gMapGL.NEAREST);
+    // Upload the image into the texture.
+    gMapGL.texImage2D(gMapGL.TEXTURE_2D, 0, gMapGL.RGBA, gMapGL.RGBA, gMapGL.UNSIGNED_BYTE, gLoadingTile);
+
+    gMapGL.uniform2f(resolutionAttribute, gGLMapCanvas.width, gGLMapCanvas.height);
+
+    // Create a buffer for the position of the rectangle corners.
+    var mapVerticesTextureCoordBuffer = gMapGL.createBuffer();
+    gMapGL.bindBuffer(gMapGL.ARRAY_BUFFER, mapVerticesTextureCoordBuffer);
+    var x_start = 10;
+    var i_width = 512;
+    var y_start = 10;
+    var i_height = 512;
+    var textureCoordinates = [
+      x_start, y_start,
+      x_start + i_width, y_start,
+      x_start, y_start + i_height,
+      x_start, y_start + i_height,
+      x_start + i_width, y_start,
+      x_start + i_width, y_start + i_height];
+    gMapGL.bufferData(gMapGL.ARRAY_BUFFER, new Float32Array(textureCoordinates), gMapGL.STATIC_DRAW);
+    gMapGL.enableVertexAttribArray(vertexPositionAttribute);
+    gMapGL.vertexAttribPointer(vertexPositionAttribute, 2, gMapGL.FLOAT, false, 0, 0);
+
+    // There are 6 indices in textureCoordinates.
+    gMapGL.drawArrays(gMapGL.TRIANGLES, 0, 6);
+  }
 
-  gLoadingTile = new Image();
-  gLoadingTile.src = "style/loading.png";
-  gWaitCounter++;
-  gLoadingTile.onload = function() { gWaitCounter--; };
+  var throwEv = new CustomEvent("mapinit-done");
+  gAction.dispatchEvent(throwEv);
 }
 
 function resizeAndDraw() {
   var viewportWidth = Math.min(window.innerWidth, window.outerWidth);
   var viewportHeight = Math.min(window.innerHeight, window.outerHeight);
-  if (gMapCanvas && gTrackCanvas) {
+  if (gMapCanvas && gGLMapCanvas && gTrackCanvas) {
     gMapCanvas.width = viewportWidth;
     gMapCanvas.height = viewportHeight;
+    gGLMapCanvas.width = viewportWidth;
+    gGLMapCanvas.height = viewportHeight;
+    gMapGL.viewport(0, 0, gMapGL.drawingBufferWidth, gMapGL.drawingBufferHeight);
     gTrackCanvas.width = viewportWidth;
     gTrackCanvas.height = viewportHeight;
     drawMap();
@@ -187,6 +349,14 @@ function zoomOut() {
   }
 }
 
+function zoomTo(aTargetLevel) {
+  aTargetLevel = parseInt(aTargetLevel);
+  if (aTargetLevel >= 0 && aTargetLevel <= gMaxZoom) {
+    gPos.z = aTargetLevel;
+    drawMap();
+  }
+}
+
 function gps2xy(aLatitude, aLongitude) {
   var maxZoomFactor = Math.pow(2, gMaxZoom) * gTileSize;
   var convLat = aLatitude * Math.PI / 180;
@@ -329,6 +499,10 @@ function drawMap(aPixels, aOverdraw) {
       }
     }
   }
+  drawTrack();
+}
+
+function drawTrack() {
   gLastDrawnPoint = null;
   gCurPosMapCache = undefined;
   gTrackContext.clearRect(0, 0, gTrackCanvas.width, gTrackCanvas.height);
@@ -417,8 +591,16 @@ var mapEvHandler = {
   handleEvent: function(aEvent) {
     var touchEvent = aEvent.type.indexOf('touch') != -1;
 
-    // Bail out on unwanted map moves, but not zoom-changing events.
-    if (aEvent.type != "DOMMouseScroll" && aEvent.type != "mousewheel") {
+    if (touchEvent) {
+      aEvent.stopPropagation();
+    }
+
+    // Bail out if the event is happening on an input.
+    if (aEvent.target.tagName.toLowerCase() == "input")
+      return;
+
+    // Bail out on unwanted map moves, but not zoom or keyboard events.
+    if (aEvent.type.indexOf("mouse") === 0 || aEvent.type.indexOf("touch") === 0) {
       // Bail out if this is neither a touch nor left-click.
       if (!touchEvent && aEvent.button != 0)
         return;
@@ -437,6 +619,14 @@ var mapEvHandler = {
       case "mousedown":
       case "touchstart":
         if (touchEvent) {
+          if (aEvent.targetTouches.length == 2) {
+            gPinchStartWidth = Math.sqrt(
+                Math.pow(aEvent.targetTouches.item(1).clientX -
+                         aEvent.targetTouches.item(0).clientX, 2) +
+                Math.pow(aEvent.targetTouches.item(1).clientY -
+                         aEvent.targetTouches.item(0).clientY, 2)
+            );
+          }
           gDragTouchID = aEvent.changedTouches.item(0).identifier;
           coordObj = aEvent.changedTouches.identifiedTouch(gDragTouchID);
         }
@@ -452,6 +642,47 @@ var mapEvHandler = {
         break;
       case "mousemove":
       case "touchmove":
+        if (touchEvent && aEvent.targetTouches.length == 2) {
+          curPinchStartWidth = Math.sqrt(
+              Math.pow(aEvent.targetTouches.item(1).clientX -
+                       aEvent.targetTouches.item(0).clientX, 2) +
+              Math.pow(aEvent.targetTouches.item(1).clientY -
+                       aEvent.targetTouches.item(0).clientY, 2)
+          );
+          if (!gPinchStartWidth)
+            gPinchStartWidth = curPinchStartWidth;
+
+          if (gPinchStartWidth / curPinchStartWidth > 1.7 ||
+              gPinchStartWidth / curPinchStartWidth < 0.6) {
+            var newZoomLevel = gPos.z + (gPinchStartWidth < curPinchStartWidth ? 1 : -1);
+            if ((newZoomLevel >= 0) && (newZoomLevel <= gMaxZoom)) {
+              // Calculate new center of the map - preserve middle of pinch.
+              // This means that pixel distance between old center and middle
+              // must equal pixel distance of new center and middle.
+              var x = (aEvent.targetTouches.item(1).clientX +
+                       aEvent.targetTouches.item(0).clientX) / 2 -
+                      gMapCanvas.offsetLeft;
+              var y = (aEvent.targetTouches.item(1).clientY +
+                       aEvent.targetTouches.item(0).clientY) / 2 -
+                      gMapCanvas.offsetTop;
+
+              // Zoom factor after this action.
+              var newZoomFactor = Math.pow(2, gMaxZoom - newZoomLevel);
+              gPos.x -= (x - gMapCanvas.width / 2) * (newZoomFactor - gZoomFactor);
+              gPos.y -= (y - gMapCanvas.height / 2) * (newZoomFactor - gZoomFactor);
+
+              if (gPinchStartWidth < curPinchStartWidth)
+                zoomIn();
+              else
+                zoomOut();
+
+              // Reset pinch start width and start another pinch gesture.
+              gPinchStartWidth = null;
+            }
+          }
+          // If we are in a pinch, do not drag.
+          break;
+        }
         var x = coordObj.clientX - gMapCanvas.offsetLeft;
         var y = coordObj.clientY - gMapCanvas.offsetTop;
         if (gDragging === true) {
@@ -459,13 +690,20 @@ var mapEvHandler = {
           var dY = y - gLastMouseY;
           gPos.x -= dX * gZoomFactor;
           gPos.y -= dY * gZoomFactor;
-          var mapData = gMapContext.getImageData(0, 0, gMapCanvas.width, gMapCanvas.height);
-          gMapContext.clearRect(0, 0, gMapCanvas.width, gMapCanvas.height);
-          gMapContext.putImageData(mapData, dX, dY);
-          drawMap({left: (dX > 0) ? dX : 0,
-                   right: (dX < 0) ? -dX : 0,
-                   top: (dY > 0) ? dY : 0,
-                   bottom: (dY < 0) ? -dY : 0});
+          if (true) { // use optimized path
+            var mapData = gMapContext.getImageData(0, 0,
+                                                   gMapCanvas.width,
+                                                   gMapCanvas.height);
+            gMapContext.clearRect(0, 0, gMapCanvas.width, gMapCanvas.height);
+            gMapContext.putImageData(mapData, dX, dY);
+            drawMap({left: (dX > 0) ? dX : 0,
+                     right: (dX < 0) ? -dX : 0,
+                     top: (dY > 0) ? dY : 0,
+                     bottom: (dY < 0) ? -dY : 0});
+          }
+          else {
+            drawMap(false, true);
+          }
           showUI();
         }
         gLastMouseX = x;
@@ -473,6 +711,7 @@ var mapEvHandler = {
         break;
       case "mouseup":
       case "touchend":
+        gPinchStartWidth = null;
         gDragging = false;
         showUI();
         break;
@@ -519,6 +758,88 @@ var mapEvHandler = {
             zoomOut();
         }
         break;
+      case "keydown":
+        // Allow keyboard control to move and zoom the map.
+        // Should use aEvent.key instead of aEvent.which but needs bug 680830.
+        // See https://developer.mozilla.org/en-US/docs/DOM/Mozilla_event_reference/keydown
+        var dX = 0;
+        var dY = 0;
+        switch (aEvent.which) {
+          case 39: // right
+            dX = -gTileSize / 2;
+          break;
+          case 37: // left
+            dX = gTileSize / 2;
+          break;
+          case 38: // up
+            dY = gTileSize / 2;
+          break;
+          case 40: // down
+            dY = -gTileSize / 2;
+          break;
+          case 87: // w
+          case 107: // + (numpad)
+          case 171: // + (normal key)
+            zoomIn();
+          break;
+          case 83: // s
+          case 109: // - (numpad)
+          case 173: // - (normal key)
+            zoomOut();
+          break;
+          case 48: // 0
+          case 49: // 1
+          case 50: // 2
+          case 51: // 3
+          case 52: // 4
+          case 53: // 5
+          case 54: // 6
+          case 55: // 7
+          case 56: // 8
+            zoomTo(aEvent.which - 38);
+          break;
+          case 57: // 9
+            zoomTo(9);
+          break;
+          case 96: // 0 (numpad)
+          case 97: // 1 (numpad)
+          case 98: // 2 (numpad)
+          case 99: // 3 (numpad)
+          case 100: // 4 (numpad)
+          case 101: // 5 (numpad)
+          case 102: // 6 (numpad)
+          case 103: // 7 (numpad)
+          case 104: // 8 (numpad)
+            zoomTo(aEvent.which - 86);
+          break;
+          case 105: // 9 (numpad)
+            zoomTo(9);
+          break;
+          default: // not supported
+            console.log("key not supported: " + aEvent.which);
+          break;
+        }
+
+        // Move if needed.
+        if (dX || dY) {
+          gPos.x -= dX * gZoomFactor;
+          gPos.y -= dY * gZoomFactor;
+          if (true) { // use optimized path
+            var mapData = gMapContext.getImageData(0, 0,
+                                                   gMapCanvas.width,
+                                                   gMapCanvas.height);
+            gMapContext.clearRect(0, 0, gMapCanvas.width, gMapCanvas.height);
+            gMapContext.putImageData(mapData, dX, dY);
+            drawMap({left: (dX > 0) ? dX : 0,
+                     right: (dX < 0) ? -dX : 0,
+                     top: (dY > 0) ? dY : 0,
+                     bottom: (dY < 0) ? -dY : 0});
+          }
+          else {
+            drawMap(false, true);
+          }
+        }
+        break;
     }
   }
 };
@@ -637,7 +958,7 @@ function endTracking() {
 function clearTrack() {
   gTrack = [];
   gTrackStore.clear();
-  drawMap({left: 0, right: 0, top: 0, bottom: 0});
+  drawTrack();
 }
 
 var gTileService = {