Search This Blog

Sunday 3 July 2016

One of the methods to create and use offline base maps for web based GIS, JavaScript example



In order to create a base map for web based and browser Geographic information system (GIS), which can be used in offline mode (i.e., when there is no Internet connectivity), the following methods may be used:


  • 1. Installing of a full-featured server with a whole set of maps (data) covering the entire globe (or not only a globe). Read about it on OpenStreetMap example here. As a rule this approach requires the installation of a variety of software to your PC or any other electronic devices. They are such as Tile servers, servers for sharing geospatial data such as Geoserver or the programs that are listed here: http://wiki.openstreetmap.org/wiki/Software - Look into the column "Stores map-data on -board"=yes for offline software. This approach requires the setting of such software on every device that you would like to work in an offline mode. As well you need a large amount of disk space for storing of maps, tiles. Positive aspects of this approach are: that you have full control over all of the data and they are available to you at any convenient time; local storage is often faster than making requests to the Internet; you can have a map, even if you are in a remote area; working where Internet connections are unreliable.

  • 2. Using only a very limited set of data - only the area you are interested in at the moment and using only the functionality of the browser in which you start the web-browser-based GIS. Positive aspects of this approach are: that it is not necessary to install any software in addition to your browser, which generally has already been installed on all modern devices by yourselves or it was there at the time of purchase; using a small volume of your local storage; easy to use. The first method sometimes includes some elements of the second method.

In this article, we will describe one variant of the second method implementation in the web-browser-based GIS on the example of OpenWebGIS (read more about this system at opensource.com). First, we will describe how it is implemented with the help of the interface (what buttons do you have to press or what windows do you have to open), and then the implementation described by a code of JavaScript will be shown. In this article the term «base offline map» («offline map») is considered to be a map created from image, that in its turn may consist of single maps which are called blocks or tiles or parts of the map – these are synonymous in this case.
To access the work settings with offline maps it is necessary in the main menu select the item «View->Offline mode settings», then the popup window opens shown in Figure 1.
Figure 1 - Popup window of OpenWebGIS with settings with offline maps options
Key moments in the creation and usage of OpenWebGIS offline base maps are:
  • 1. Offline base maps are generated from those base maps, that the user has activated at the moment, it can be OpenStreetMap, Google Maps, any WMS or Vector maps. First, the users must create a set of maps in advance (we call them blocks or tiles) that contain all geographic areas the users are interested in with various zoom. The users can do it manually - when they get a map to download while every panning and zooming, or in an automatic mode with different intervals (shift) in latitude, longitude and zooming. Maps are generated for the user to download in PNG (png) format (look at the Figure 2). 

Figure 2 - Offline maps are generated for the user to download in png format
 
PNG file names are formed as: 9p56335449228382_10p845757362767964_10p936645507717008_11p653892792756748_10.png,
where the first number "9p56335449228382" is the lower left longitude map area, and "p" - is a separator of degrees and its decimal fractions; "10p845757362767964" – is the lower left latitude; "10p936645507717008" is the upper right longitude; "11p653892792756748" -is the upper right latitude, and "10" is the zoom. During the map formation (generation) in a separate browser window the preview of this map appears.

  • 2. After preparing a set of map files (tiles) and selecting them with the help of a corresponding button, the user can run the offline mode. After that (if the map is positioned in the appropriate place) a new base map named «Offline_basemap» is added to a base maps list.

To start recording manually the base map it is necessary to activate the «Start offline map recording» option and to set the «Time delay» in milliseconds (look at the Figure 3).
Figure 3 - To start recording manually the base map it is necessary to activate the «Start offline map recording»

Usually the slower speed of your Internet, the greater the time delay you need to install. The setting of Time delay is necessary because the creation (generation) of png offline map file is asynchronous from multiple (each time their number may be different) images (from each tiles URLs of a base map), each of these images is loaded with different speeds which are affected by many conditions, including the speed of your Internet, your CPU, the availability of the tasks performed by your browser at this time. Taking this into account to calculate the exact time of the full load of all the images according to the work algorithm of javascript code in OpenWebGIS is difficult. Therefore, the time delay, after which with a high probability an offline map will be ready for the user, is set by the user at this stage of OpenWebGIS development. If readers have any suggestions to optimize this code please offer - JavaScript code of this process is described later in this article.

Then you can close the «Offline mode settings» window and while every zooming and panning through the set Time delay you will be offered to download the map in png format, as shown in Figure 2. To stop this process - Deactivate the option «Start offline map recording.»
To start recording automatically a base map it is necessary first to set the coordinates of a geographical area of interest - longitude from ... to, latitude from ... to (indicated by the number 1 in Figure 4) and step in degrees (indicated by the number 2 in Figure 4).
Figure 4 - Set options to start recording automatically a base map

Then it is necessary to set if there is the change in maps zoom and in which interval (indicated by the number 3 in Figure 4). Then set the «Time delay» (indicated by the number 4 in Figure 4). And finally press the button «Start generate». After this the user through «Time delay» will be given the opportunity to download a map (a tile of the map), which is generated according to the settings in a specified step in degrees. The step determines the shift of each next formed map from the lower left edge of the specified area to the top right edge of the specified area. It is connected with (an important point - please pay attention) that the size of each generated map in degrees and pixels corresponds to the size of the map window set by the user (the default pixel size is 1000 pixels in width and 600 pixels in height.) To change the size of the map window, click on the «additional toolbar» (look at the Figure 5).
Figure 5 - To change the size of the map window, click on the «additional toolbar»

Depending on the set size of the area and the map window size and the step - generated maps (parts of the map) may have overlapping geographic areas and extend well beyond the geographical area to be covered by maps window - such as it is shown in Figure 6, where 6 generated blocks of maps completely cover and extend beyond the edge of the map window. Blocks 1-6 are of the same size as the block number 1.
Figure 6 - Generated maps (parts of the map) may have overlapping geographic areas

After preparing the set of maps (blocks, tiles) for the offline mode, the user before activating the offline mode should select the files of generated maps (tiles) using the «Browse» button (look at the Figure 7), there is any number of files, in this case it is eight.
Figure 7 - The user before activating the offline mode should select the files of generated maps (tiles) using the «Browse» button
If you run OpenWebGIS on a device with Android OS, then at the moment Multiple file selection is possible in the browser «Google Chrome» . User must set the extent of the map close to the area which is covered by the generated maps, and then activate the option «Turn on/off offline mode.» (Look at the Figure 8).
Figure 8 - User must set the extent of the map close to the area which is covered by the generated maps, and then activate the option «Turn on/off offline mode.»

The work implementation with offline maps for Android version app of OpenWebGIS  is currently being developed. But you can run OpenWebGIS in most browsers on your Android or iOS devices, and it is likely to work.

After that, during every panning and zooming from a selected set of map blocks will be automatically selected and drawn as the base map that block which most closely matches the current extent of the map. If a set of blocks is small and they are generated with a relatively small step, there can be unfilled areas, as shown in Figure 9.
Figure 9 - If a set of blocks for offline map is small and they are generated with a relatively small step, there can be unfilled areas

Important: From all said above follows that the user as the basis for the offline base map can use not only png files, generated using OpenWebGIS, but also any other user's files jpg, bmp, gif. The only important point is that their names should follow the rules of OpenWebGIS:

x1x1pxxx_y1y1pyyy_x2x2pxxx_y2y2pyyy_zz.png,


where the first number x1x1pxxx -
is the longitude of the lower left corner of the map area, and «p» - is a degrees separator and its decimal fractions (there is any number of decimal places); y1y1pyyy -the lower latitude of the lower left corner; x2x2pxxx- is the longitude of the upper right corner; y2y2pyyyis the latitude of the upper right corner, and zz is the zoom. Let's turn to the description of JavaScript code with the help of which everything described above is done.
All JavaScript code of OpenWebGIS you can
see at GitHub. The base to load, display and render maps from multiple sources on web pages in OpenWebGIS the modified open-source JavaScript library - OpenLayers version 2.x is used.


All functions for the creation and usage of offline maps are in the Start_OpenWebGIS_en.html file. The code that creates a popup window «Offline mode settings» shown in Figure 1 is in function OfflineMap(). The checkbox with id = "id_startOfflineMap" under the name «Start offline map recording» (highlighted by red at the Figure 3) is responsible for activation of the manual recording (generation) blocks of an offline map. An event of changing the state of the checkbox is bound with function recordOfflineMap() in the following way:
document.getElementById("id_startOfflineMap").onchange=function()

{if(document.getElementById("id_startOfflineMap").checked==true)

{globalVarOfflineRecMap=true;recordOfflineMap();}
else{globalVarOfflineRecMap=false;}
}

Here globalVarOfflineRecMap — is a global variable. The value (true or false) of this variable is considered during every panning and zooming of map. If globalVarOfflineRecMap==true, i.e. the manual maps record is activated, then during every panning and zooming of map the function recordOfflineMap() is triggered.
We are not going to give the whole code of this function here, you can explore it yourself at the link. But it should be noted that at the beginning the coordinates of the lower left and upper right corner of the map extent and size of the specified area in meters are calculated, followed by the commands to create a new browser window for preview of a formed offline map. Then comes the loop through all the layers of the map that are basic and if the layer is active, so a png file for an offline map is starting to create. This png file creating from canvas. The HTML <canvas> element is used to draw graphics, on the fly, via scripting.
for(var a=0; a<map.layers.length;a++) 
{ 
//In the case that active base map is OpenStreetMap (OSM):
if(map.layers[a].CLASS_NAME=="OpenLayers.Layer.OSM"&&map.layers[a]==map.baseLayer)
{//creating canvas onto which the blocks of active base map will be added and
// then this canvas will be converted into png file. 
//Created png file will be offered to the user for downloading:
var canof=document.createElement("canvas");  
//setting the width for canvas like a Width of map window:
canof.width =map.viewPortDiv.clientWidth;
//setting the height for canvas like a Height of map window:
canof.height =map.viewPortDiv.clientHeight;
Later there is the creation of img elements, loop through all URLs of tiles OSM (covering current map extent) and set it to the src of imgs. The <img> element defines an image in an HTML page.
for(var y=0; y<map.layers[a].grid.length;y++) (1)
 { 
 for(var yj=0; yj<map.layers[a].grid[y].length;yj++) 
 { var imgN=document.createElement("img"); 
 imgN.src=map.layers[a].grid[y][yj].url; 
 imgN.width=map.layers[a].tileSize.w; 
 imgN.height=map.layers[a].tileSize.h;
... 

In order to avoid error «The operation is insecure» connected with Same-origin policy it is necessary to do the following:
imgN.crossOrigin =''; // or

imgN.crossOrigin='anonymous';

Read about Using Cross-domain images here.
An onload handler is added to every of these imgs (images) as loading is asynchronous. As a result of these handlers execution, each image is added onto the earlier created canvas in the strictly defined positions with the help of the function drawImage() - http://www.w3schools.com/tags/canvas_drawimage.asp
var aa=canvasContext; 
aa.globalAlpha=1;canvasContextf.globalAlpha=1;
var ab=imgN; var b=map.layers[a].grid[y][yj].position.x; 
var c=map.layers[a].grid[y][yj].position.y; var d=map.layers[a].tileSize.w; 
var e=map.layers[a].tileSize.h;
…

{imgN.onload=function (aa,ab,b,c,d,e,canvasContextf)
{return function() {aa.drawImage(ab,b,c,d,e);
canvasContextf.drawImage(ab,b,c,d,e);}}(aa,ab,b,c,d,e,canvasContextf);}

When all the tiles OSM in the loop are taken (1) - (see above) except the last tile, then the last tile also is converted in img and added onto canvas. And completely filled canvas, which in its turn is transfered into object Image with the help of the function toDataURL() and this image is offered by means of the browser for downloading by the user:

if(y==map.layers[a].grid.length-1&&yj==map.layers[a].grid[y].length-1)
{imgN.onload=function (aa,ab,b,c,d,e,canvasW,canvasH,canof,canvasContextf,georef,mzoom)
{return setTimeout(function() {aa.drawImage(ab,b,c,d,e);canvasContextf.drawImage(ab,b,c,d,e); 


var left=georef.left+""; var bottom=georef.bottom+"";var right=georef.right+"";
var top=georef.top+""; 

var nameid=left.split(".")[0]+"p"+left.split(".")[1]+"_"+bottom.split(".")[0]+"p"
+bottom.split(".")[1]+"_"+right.split(".")[0]+"p"+right.split(".")[1]+"_"
+top.split(".")[0]+"p"+top.split(".")[1]+"_"+mzoom+".png"; 
var imgf = new Image(); 
 imgf.crossOrigin = '';  imgf.crossOrigin='anonymous';
imgf.src=canof.toDataURL();imgf.id=nameid; 
 
imgf.onload=function(canof,imgf){var aimg= document.createElement("a"); 
 aimg.href=imgf.src;aimg.download=imgf.id; aimg.id=imgf.id; 
 document.body.appendChild(aimg);aimg.click(); //impultion of the window
// appearing that offers to download the map or the downloading beginning,
// which depends on the browser and its settings.
 document.getElementById(aimg.id).parentNode.removeChild(document.getElementById(aimg.id)); 
document.getElementById(canof.id).parentNode.removeChild(document.getElementById(canof.id));}(canof,imgf) 
if(document.getElementById("id_waitOfflinemap2"))
{document.getElementById("id_waitOfflinemap2").parentNode.removeChild(document.getElementById("id_waitOfflinemap2"))} 
},parseFloat(document.getElementById("delay_OfflineMap2").value)) }(aa,ab,b,c,d,e,canvasW,canvasH,canof,canvasContextf,georef,mzoom);}

The information on getting and usage of tiles OSM without OpenLayers you can get at the official OSM site.
Now let's describe some nuances in offline map creating on the base of Google Maps. The principles of base maps (tiles) creating on the base of Google Maps with the help of JavaScript and OpenLayers as a whole is similar to that is described above for OSM. But img elements creating, loop through all URLs of tiles is done with the help of Google Static Maps API and considering limits 640x640 maximum image resolution:
if(map.layers[j].CLASS_NAME=="OpenLayers.Layer.Google"&&map.layers[j]==map.baseLayer) 
 { 
 var canof=document.createElement("canvas");  
canof.width =map.viewPortDiv.clientWidth; 
canof.height =map.viewPortDiv.clientHeight; 
 var canvasContextf =canof.getContext('2d');  var left=georef.left+""; 
var bottom=georef.bottom+"";var right=georef.right+"";var top=georef.top+""; 
var nameidf=left.split(".")[0]+"p"+left.split(".")[1]+"_"+bottom.split(".")[0]+"p"+
bottom.split(".")[1]+"_"+right.split(".")[0]+"p"+
right.split(".")[1]+"_"+top.split(".")[0]+"p"+top.split(".")[1]+"_"+mzoom+"_png"; 
….
var MapGw=map.viewPortDiv.clientWidth;var MapGh=map.viewPortDiv.clientHeight; 
var Warray=[];var Harray=[];var WHarray=[]; 
if (MapGw>640) // consider  640 x 640 maximum image resolution
{ 
var countMapW=Math.ceil(MapGw/640); 
for (var m=0;m<countMapW-1;m++){Warray.push(640)};
Warray.push(MapGw-(640*(countMapW-1))); } 
else 
{Warray.push(MapGw);}
…..
var ImgArray=[]; 
for(var k=0; k<WHarray.length;k++) 
 {  { 
var imgNG=document.createElement("img"); 
imgNG.crossOrigin = ''; // no credentials flag. Same as 
 imgNG.crossOrigin='anonymous'; 
var typG=map.layers[j].type; 
// using of  Google Static Maps API:
imgNG.src="http://maps.googleapis.com/maps/api/staticmap?sensor=false&maptype="+ 
typG+"&scale=1&center="+CentXY[k].split(';')[1]+","+
CentXY[k].split(';')[0]+"&zoom="+map.zoom+"&size="+WHarray[k].split(';')[0]+
"x"+WHarray[k].split(';')[1]; 
imgNG.width=WHarray[k].split(';')[0]; 
imgNG.height=WHarray[k].split(';')[1]; 
ImgArray.push(imgNG); 
 }  }
To use Google Static Maps API in your own new projects after June 22 2016 you should at first necessarily get an API key and take into consideration Google Static Maps API Usage Limits.

To start recording automatically a base map after setting all the required parameters shown in Figure 4, it is necessary to click on the button «Start generate». After that the function Record_AutomaticOfflineMap() is activated. The essence of this function is that it provides an automatic iteration of the above described function recordOfflineMap() as many times as it will be required by the set extent of a user-defined geographical area for an offline map and the step in degrees by latitude and longitude to generate this map blocks:
function Record_AutomaticOfflineMap() 
{var numberofimages=0 
for(var i=parseFloat(document.getElementById("longitude_OfflineMap1").value);
i<=parseFloat(document.getElementById("longitude_OfflineMap2").value);
i=i+parseFloat(document.getElementById("slongitude_OfflineMap").value)) 
{ 
for(var j=parseFloat(document.getElementById("latitude_OfflineMap1").value);
j<=parseFloat(document.getElementById("latitude_OfflineMap2").value);
j=j+parseFloat(document.getElementById("slatitude_OfflineMap").value)) 
{if (document.getElementById("zoom_OfflineMapCheck").checked==true) 
{for (var y=parseInt(document.getElementById("zoom_OfflineMap1").value);
y<=parseInt(document.getElementById("zoom_OfflineMap2").value);y++) 
{numberofimages++} 
}else{numberofimages++} 
}} 
if(numberofimages>20) 
{if (!confirm("Too many images. The number of Images is "+numberofimages+
". Are you sure?")){return;}} 
var delay=0;var zoomm=map.zoom; 
for(var i=parseFloat(document.getElementById("longitude_OfflineMap1").value);
i<=parseFloat(document.getElementById("longitude_OfflineMap2").value);
i=i+parseFloat(document.getElementById("slongitude_OfflineMap").value)) 
{ 
for(var j=parseFloat(document.getElementById("latitude_OfflineMap1").value);
j<=parseFloat(document.getElementById("latitude_OfflineMap2").value);
j=j+parseFloat(document.getElementById("slatitude_OfflineMap").value)) 
{delay=parseFloat(delay)+parseFloat(document.getElementById("delay_OfflineMap3").value); 
setTimeout(function(i,j,zoomm)
{var  boundImageA=new OpenLayers.Bounds(); 
boundImageA.left=parseFloat(i);boundImageA.bottom=parseFloat(j);
boundImageA.right=parseFloat(i+parseFloat(document.getElementById("slongitude_OfflineMap").value));
boundImageA.top=parseFloat(j+parseFloat(document.getElementById("slatitude_OfflineMap").value));
boundImageA.transform(new OpenLayers.Projection("EPSG:4326"),map.getProjectionObject());
map.zoomToExtent(boundImageA,true);map.zoomToExtent(boundImageA,false);
if (document.getElementById("zoom_OfflineMapCheck").checked==true) 
{var indz=0;var indz2=zoomm+1;var indz3=0; 
for (var y=zoomm;y<=indz2;y++) 
{indz++; 
if(indz==1){recordOfflineMap();} 
if(indz==2) 
{y=parseInt(document.getElementById("zoom_OfflineMap1").value);
indz2=parseInt(document.getElementById("zoom_OfflineMap2").value);
if(y==zoomm){}else{map.zoomTo(y);recordOfflineMap();}} 
if(indz!==1&&indz!==2) 
{map.zoomTo(y);recordOfflineMap();} 
} }else{recordOfflineMap()} }(i,j,zoomm),delay) 
} } 
}
Now let's turn to the process of offline mode activation. For this it is necessary to select files of generated maps (tiles) using the «Browse» button ( look at the Figure 7). There is any number of files, thus this button must have an attribute «multiple».
input title="Select files for the base map" type="file" id="folder_OfflineMap" multiple
Then there is a need to activate a checkbox with the title «Turn on/off offline mode.». This checkbox has id="id_startOfflineMapMode". An event of changing the state of this checkbox is bound with function OK_OfflineMap() in the following way:
document.getElementById("id_startOfflineMapMode").onchange=function()
{if(document.getElementById("id_startOfflineMapMode").checked==true)
{ OK_OfflineMap()} 
else{globalVarOfflineMapOn=false}}
Here globalVarOfflineRecMapOn - is a global variable. The value (false or array of Images files) of this variable is considered during every panning and zooming of map. If globalVarOfflineMapOn!==false, i.e. the offline mode is activated and files are selected, then during every panning and zooming of map the function OK_OfflineMap() is triggered.
The files array assigning to this variable is done in the following way:
//event occurs while file selecting:

document.getElementById('folder_OfflineMap').addEventListener('change',readFolderOffMap,false); 
function readFolderOffMap(evt) 
{globalVarOfflineMapOn = evt.target.files; }
About addEventListener read here
The function OK_OfflineMap() runs the loop through all selected files and depending on their names and the current extent of map window, and while every zooming and panning substitutes a suitable image file as an offline map:
function OK_OfflineMap() 
{if(globalVarOfflineMapOn==false){alert("Please, select files"); return;} 
var georef=map.calculateBounds(); 
georef.transform(map.getProjectionObject(),new OpenLayers.Projection("EPSG:4326")); 
var lat1=georef.bottom;var lat2=georef.top;var long1=georef.left;var long2=georef.right; 
window.URL = window.URL || window.webkitURL; 
var mapfile=globalVarOfflineMapOn[0];var absCoord=10000; 
for(var f=0;f<globalVarOfflineMapOn.length;f++) 
{ 
var coords=globalVarOfflineMapOn[f].name.split("_"); 

coords[0]=coords[0].split("p")[0]+"."+coords[0].split("p")[1]; 
coords[1]=coords[1].split("p")[0]+"."+coords[1].split("p")[1]; 
coords[2]=coords[2].split("p")[0]+"."+coords[2].split("p")[1]; 
coords[3]=coords[3].split("p")[0]+"."+coords[3].split("p")[1]; 
var coordL=georef.left-coords[0];v
ar coordB=georef.bottom-coords[1];var coordR=georef.right-coords[2];
var coordT=georef.top-coords[3]; 
var absCoord2=Math.abs(coordL)+Math.abs(coordB)+Math.abs(coordR)+Math.abs(coordT); 
 // Determining a map file that by the coordinates of the lower left 
//and upper right corner has a minimal deviation from the current map extent:
if( absCoord2<parseFloat(absCoord) ) 
{absCoord=absCoord2;mapfile = globalVarOfflineMapOn[f];} } 
   if(parseInt(mapfile.name.split("_")[4])==parseInt(map.zoom)) 
  { var reader = new FileReader(); 
reader.addEventListener("loadend", function() { 
   var  boundImage=new OpenLayers.Bounds();var indl=0;var offLayer=''; 
for (var yi=0;yi<map.layers.length;yi++) 
{if(map.layers[yi].CLASS_NAME=="OpenLayers.Layer.Image"&&
map.layers[yi]==map.baseLayer&&map.layers[yi].name=="Offline_basemap") 
{offLayer=map.layers[yi];indl++; }} 
var coords=mapfile.name.split("_"); 
coords[0]=coords[0].split("p")[0]+"."+coords[0].split("p")[1];
coords[1]=coords[1].split("p")[0]+"."+coords[1].split("p")[1]; 
coords[2]=coords[2].split("p")[0]+"."+coords[2].split("p")[1];
coords[3]=coords[3].split("p")[0]+"."+coords[3].split("p")[1]; 
boundImage.left=parseFloat(coords[0]);boundImage.bottom=parseFloat(coords[1]);
boundImage.right=parseFloat(coords[2]);boundImage.top=parseFloat(coords[3]);
boundImage.transform(new OpenLayers.Projection("EPSG:4326"),map.getProjectionObject()); 
if(indl>0){offLayer.url=reader.result;offLayer.extent=boundImage; 
offLayer.redraw();
if (offLayer.div.childNodes[0].style.left!=="0px"){
var tempvar=globalVarOfflineMapOn; 
globalVarOfflineMapOn=false;map.removeLayer(offLayer);offLayer.destroy();
newOfflineLayer(reader,boundImage);globalVarOfflineMapOn=tempvar; 
}} 
else{ newOfflineLayer(reader,boundImage); } }); 
reader.readAsDataURL(mapfile);} } 

In conclusion it is important to say that while applying the offered methods for offline maps obtaining (creating) and using, you can use data from various projects and organizations, both commercial and non-commercial, so please follow the copyrights and do not violate the licenses getting any data. OpenWebGIS stands for keeping the law when using all software and data. Therefore, please take into consideration the Copyright and License of OpenStreetMap, Google Maps API, Google Maps, also all WMS data sources and others!

No comments:

Post a Comment