+var gMap = {
+ gl: null,
+ glShaderProgram: null,
+ glVertexPositionAttr: null,
+ glTextureCoordAttr: null,
+ glResolutionAttr: null,
+ glMapTexture: null,
+ glTextures: {},
+ glTxCleanIntervalID: null,
+ glTexturesPerZoomLevel: 0,
+
+ activeMap: "osm_mapnik",
+ tileSize: 256,
+ maxZoom: 18, // The minimum is 0.
+ zoomFactor: null,
+ pos: {
+ x: 35630000.0, // Current position in the map in pixels at the maximum zoom level (18)
+ y: 23670000.0, // The range is 0-67108864 (2^gMap.maxZoom * gMap.tileSize)
+ z: 5 // This could be fractional if supported being between zoom levels.
+ },
+ baseDim: { // Map width, height and tile size in level 18 pixels.
+ wid: null,
+ ht: null,
+ tsize: null,
+ },
+
+ get width() { return gMap.gl ? gMap.gl.drawingBufferWidth : gGLMapCanvas.width; },
+ get height() { return gMap.gl ? gMap.gl.drawingBufferHeight : gGLMapCanvas.height; },
+
+ getVertShaderSource: function() {
+ return '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' +
+ '}'; },
+ getFragShaderSource: function() {
+ return 'varying highp vec2 vTextureCoord;\n\n' +
+ 'uniform sampler2D uImage;\n\n' +
+ 'void main(void) {\n' +
+ ' gl_FragColor = texture2D(uImage, vTextureCoord);\n' +
+ '}'; },
+
+ 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());
+ // Compile the shader program.
+ gMap.gl.compileShader(vertShader);
+ // See if it compiled successfully.
+ if (!gMap.gl.getShaderParameter(vertShader, gMap.gl.COMPILE_STATUS)) {
+ console.log("An error occurred compiling the vertex shader: " + gMap.gl.getShaderInfoLog(vertShader));
+ return null;
+ }
+ gMap.gl.shaderSource(fragShader, gMap.getFragShaderSource());
+ // Compile the shader program.
+ gMap.gl.compileShader(fragShader);
+ // See if it compiled successfully.
+ if (!gMap.gl.getShaderParameter(fragShader, gMap.gl.COMPILE_STATUS)) {
+ console.log("An error occurred compiling the fragment shader: " + gMap.gl.getShaderInfoLog(fragShader));
+ 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);
+ gMap.gl.linkProgram(gMap.glShaderProgram);
+ // If creating the shader program failed, alert
+ if (!gMap.gl.getProgramParameter(gMap.glShaderProgram, gMap.gl.LINK_STATUS)) {
+ alert("Unable to initialize the shader program.");
+ }
+ gMap.gl.useProgram(gMap.glShaderProgram);
+ // Get locations of the attributes.
+ gMap.glVertexPositionAttr = gMap.gl.getAttribLocation(gMap.glShaderProgram, "aVertexPosition");
+ 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.
+ 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,
+ ];
+ gMap.gl.bufferData(gMap.gl.ARRAY_BUFFER, new Float32Array(vertices), gMap.gl.STATIC_DRAW);
+ gMap.gl.enableVertexAttribArray(gMap.glTextureCoordAttr);
+ gMap.gl.vertexAttribPointer(gMap.glTextureCoordAttr, 2, gMap.gl.FLOAT, false, 0, 0);
+
+ gMap.loadImageToTexture(gLoadingTile, getTileKey("loading", {x: 0, y: 0, z: 0}));
+
+ 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);
+ gMap.gl.vertexAttribPointer(gMap.glVertexPositionAttr, 2, gMap.gl.FLOAT, false, 0, 0);
+
+ // Call texture cleaning every 30 seconds, for now (is 60 better?).
+ gMap.glTxCleanIntervalID = window.setInterval(gMap.cleanTextures, 30 * 1000);
+ }
+
+ 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() {
+ gMap.assembleGL();
+ drawTrack();
+ },
+
+ assembleGL: function() {
+ if (!gMap.gl) { return; }
+
+ document.getElementById("zoomLevel").textContent = gMap.pos.z;
+ gMap.zoomFactor = Math.pow(2, gMap.maxZoom - gMap.pos.z);
+ gMap.baseDim.wid = gMap.gl.drawingBufferWidth * gMap.zoomFactor;
+ gMap.baseDim.ht = gMap.gl.drawingBufferHeight * gMap.zoomFactor;
+ gMap.baseDim.tsize = gMap.tileSize * gMap.zoomFactor;
+
+ var xMin = gMap.pos.x - gMap.baseDim.wid / 2; // Corners of the window in level 18 pixels.
+ var yMin = gMap.pos.y - gMap.baseDim.ht / 2;
+ var xMax = gMap.pos.x + gMap.baseDim.wid / 2;
+ var yMax = gMap.pos.y + gMap.baseDim.ht / 2;
+
+ if (gMapPrefsLoaded && mainDB)
+ gPrefs.set("position", gMap.pos);
+
+ // Go through all the tiles in the map, find out if to draw them and do so.
+ for (var x = Math.floor(xMin / gMap.baseDim.tsize); x < Math.ceil(xMax / gMap.baseDim.tsize); x++) {
+ for (var y = Math.floor(yMin / gMap.baseDim.tsize); y < Math.ceil(yMax / gMap.baseDim.tsize); y++) {
+ // Only go to loading step if we haven't loaded the texture.
+ var coords = {x: x, y: y, z: gMap.pos.z};
+ var tileKey = getTileKey(gMap.activeMap, normalizeCoords(coords));
+ if (!gMap.glTextures[tileKey]) {
+ // Initiate loading/drawing of the actual tile.
+ gTileService.get(gMap.activeMap, coords,
+ function(aImage, aStyle, aCoords, aTileKey) {
+ // Only actually load if this still applies for the current view.
+ if ((aStyle == gMap.activeMap) && (aCoords.z == gMap.pos.z)) {
+ var URL = window.URL;
+ var imgURL = URL.createObjectURL(aImage);
+ var imgObj = new Image();
+ imgObj.onload = function() {
+ gMap.loadImageToTexture(imgObj, aTileKey);
+ if (document.hidden != true) { // Only draw if we're actually visible.
+ window.requestAnimationFrame(function(aTimestamp) { gMap.drawGL() });
+ }
+ URL.revokeObjectURL(imgURL);
+ }
+ imgObj.src = imgURL;
+ }
+ });
+ }
+ }
+ }
+ if (document.hidden != true) { // Only draw if we're actually visible.
+ window.requestAnimationFrame(function(aTimestamp) { gMap.drawGL() });
+ }
+ },
+
+ drawGL: function() {
+ var xMin = gMap.pos.x - gMap.baseDim.wid / 2; // Corners of the window in level 18 pixels.
+ var yMin = gMap.pos.y - gMap.baseDim.ht / 2;
+ var xMax = gMap.pos.x + gMap.baseDim.wid / 2;
+ var yMax = gMap.pos.y + gMap.baseDim.ht / 2;
+
+ // Go through all the tiles in the map, find out if to draw them and do so.
+ for (var x = Math.floor(xMin / gMap.baseDim.tsize); x < Math.ceil(xMax / gMap.baseDim.tsize); x++) {
+ for (var y = Math.floor(yMin / gMap.baseDim.tsize); y < Math.ceil(yMax / gMap.baseDim.tsize); y++) {
+ // Rounding the pixel offsets ensures we position the tiles precisely.
+ var xoff = Math.round((x * gMap.baseDim.tsize - xMin) / gMap.zoomFactor);
+ var yoff = Math.round((y * gMap.baseDim.tsize - yMin) / gMap.zoomFactor);
+ // Draw the tile, first find out the actual texture to use.
+ var norm = normalizeCoords({x: x, y: y, z: gMap.pos.z});
+ var tileKey = getTileKey(gMap.activeMap, norm);
+ if (!gMap.glTextures[tileKey]) {
+ tileKey = getTileKey("loading", {x: 0, y: 0, z: 0});
+ }
+ gMap.drawTileGL(xoff, yoff, tileKey);
+ }
+ }
+ },
+
+ resizeAndDraw: function() {
+ var viewportWidth = Math.min(window.innerWidth, window.outerWidth);
+ var viewportHeight = Math.min(window.innerHeight, window.outerHeight);
+ if (gGLMapCanvas && gTrackCanvas) {
+ gGLMapCanvas.width = viewportWidth;
+ gGLMapCanvas.height = viewportHeight;
+ gTrackCanvas.width = viewportWidth;
+ gTrackCanvas.height = viewportHeight;
+ if (gMap.gl) {
+ // Size viewport to canvas size.
+ gMap.gl.viewport(0, 0, gMap.gl.drawingBufferWidth, gMap.gl.drawingBufferHeight);
+ // Clear the color.
+ gMap.gl.clear(gMap.gl.COLOR_BUFFER_BIT);
+ // Make sure the vertex shader get the right resolution.
+ gMap.gl.uniform2f(gMap.glResolutionAttr, gGLMapCanvas.width, gGLMapCanvas.height);
+ // Prepare recalculation of textures to keep for one zoom level.
+ gMap.glTexturesPerZoomLevel = 0;
+ }
+ gMap.draw();
+ showUI();
+ }
+ },
+
+ drawTileGL: function(aLeft, aRight, aTileKey) {
+ gMap.gl.activeTexture(gMap.gl.TEXTURE0);
+ gMap.gl.bindTexture(gMap.gl.TEXTURE_2D, gMap.glTextures[aTileKey]);
+ // Set uImage to refer to TEXTURE0
+ gMap.gl.uniform1i(gMap.gl.getUniformLocation(gMap.glShaderProgram, "uImage"), 0);
+ var x_start = aLeft;
+ var i_width = gMap.tileSize;
+ var y_start = aRight;
+ var i_height = gMap.tileSize;
+ 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,
+ ];
+ gMap.gl.bufferData(gMap.gl.ARRAY_BUFFER, new Float32Array(textureCoordinates), gMap.gl.STATIC_DRAW);
+
+ // There are 6 indices in textureCoordinates.
+ gMap.gl.drawArrays(gMap.gl.TRIANGLES, 0, 6);
+ },
+
+ loadImageToTexture: function(aImage, aTileKey) {
+ // Create and bind texture.
+ gMap.glTextures[aTileKey] = gMap.gl.createTexture();
+ gMap.gl.bindTexture(gMap.gl.TEXTURE_2D, gMap.glTextures[aTileKey]);
+ // Set params for how the texture minifies and magnifies (wrap params are not needed as we're power-of-two).
+ gMap.gl.texParameteri(gMap.gl.TEXTURE_2D, gMap.gl.TEXTURE_MIN_FILTER, gMap.gl.NEAREST);
+ gMap.gl.texParameteri(gMap.gl.TEXTURE_2D, gMap.gl.TEXTURE_MAG_FILTER, gMap.gl.NEAREST);
+ // Upload the image into the texture.
+ gMap.gl.texImage2D(gMap.gl.TEXTURE_2D, 0, gMap.gl.RGBA, gMap.gl.RGBA, gMap.gl.UNSIGNED_BYTE, aImage);
+ },
+
+ cleanTextures: function() {
+ // Get rid of unneeded textures to save memory.
+ // TODO: Be less aggressive, maybe keep neighboring zoom levels (but x/y coords there are zoom-specific).
+ if (!gMap.glTexturesPerZoomLevel) {
+ // Calculate how many textures we need to keep for one zoom level.
+ // ceil(width/size) gives us the minimum, keep one on either side as well.
+ gMap.glTexturesPerZoomLevel =
+ Math.ceil(gMap.gl.drawingBufferWidth / gMap.tileSize + 2) *
+ Math.ceil(gMap.gl.drawingBufferHeight / gMap.tileSize + 2);
+ console.log("Keeping " + gMap.glTexturesPerZoomLevel + " textures per level");
+ }
+ if (Object.keys(gMap.glTextures).length > gMap.glTexturesPerZoomLevel) {
+ console.log("Cleaning textures... (have " + Object.keys(gMap.glTextures).length + " atm)");
+
+ // Find coordinate ranges for tiles to keep.
+ var tMin = normalizeCoords({x: Math.floor((gMap.pos.x - gMap.baseDim.wid / 2) / gMap.baseDim.tsize) - 1,
+ y: Math.floor((gMap.pos.y - gMap.baseDim.ht / 2) / gMap.baseDim.tsize) - 1,
+ z: gMap.pos.z});
+ var tMax = normalizeCoords({x: Math.ceil((gMap.pos.x + gMap.baseDim.wid / 2) / gMap.baseDim.tsize) + 1,
+ 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 (var tileKey in gMap.glTextures) {
+ var keyMatches = tileKey.match(/([^:]+)::(\d+),(\d+),(\d+)/);
+ if (keyMatches && keyMatches[1] != "loading") {
+ var txData = {
+ style: keyMatches[1],
+ x: keyMatches[2],
+ y: keyMatches[3],
+ z: keyMatches[4],
+ }
+ var delTx = false;
+ if (txData.style != gMap.activeMap) { delTx = true; console.log("Different map style: " + txData.style); }
+ if (!delTx && (txData.z < tMin.z || txData.z > tMax.z)) { delTx = true; console.log("Out-of-range zoom: " + txData.z); }
+ if (tMin.x < tMax.x) {
+ if (!delTx && (txData.x < tMin.x || txData.x > tMax.x)) { delTx = true; console.log("Out-of-range X: " + txData.x); }
+ }
+ else {
+ // We are crossing over the 0 coordinate!
+ if (!delTx && (txData.x < tMin.x && txData.x > tMax.x)) { delTx = true; console.log("Out-of-range X: " + txData.x); }
+ }
+ if (tMin.y < tMax.y) {
+ if (!delTx && (txData.y < tMin.y || txData.y > tMax.y)) { delTx = true; console.log("Out-of-range Y: " + txData.y); }
+ }
+ else {
+ // We are crossing over the 0 coordinate!
+ if (!delTx && (txData.y < tMin.y && txData.y > tMax.y)) { delTx = true; console.log("Out-of-range Y: " + txData.y); }
+ }
+ if (delTx) {
+ // Delete texture from GL and from the array we are holding.
+ gMap.gl.deleteTexture(gMap.glTextures[tileKey]);
+ delete gMap.glTextures[tileKey];
+ }
+ }
+ }
+ console.log("Cleaning complete, " + Object.keys(gMap.glTextures).length + " textures left)");
+ }
+ },
+
+ 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();
+ },