Introduction
In my free time I am writing an application for finding ATMs in Minsk. And somehow going on vacation, I was left without the Internet on the phone. Everything would be fine, but I needed to find an ATM, withdraw money and not be late for the train. I opened my application and was very disappointed that I could not use the map offline. Of course, without connecting to the network in our time, it’s better not to go out of your home, but still the Internet on your favorite mobile device may be out at the wrong time.
Looking at the other applications on my phone, I noticed that, at best, they cache parts of the map that were loaded before. This could partly help me, but did not solve the problem completely. After that, I wondered if it was worth having the opportunity to view the map offline. Since my application is not native, but based on phonegap, those are browser based, then the story will be about how you can cache the map for browser applications in particular using google map api v3.
Idea
Somehow after all this, I remembered that google map api allows you to make your map implementation (for example, as I advise
OSM ). Immediately I got the idea to slip an implementation that will always be available, and this can be done either by downloading maps to the cache if there is a connection, or by supplying the cache cards with the application.
At first I was thinking of using application cache, but I refused this idea, since its api does not provide wide possibilities for managing the loading of the cache.
')
In the end, I decided to just deliver the cache with the application.
I also had the idea to store sprites in localStorage, but this implementation has major flaws:
- localStorage size limit;
- data must be stored in base64, which is about 30% larger than the actual size.
The idea of storing sprites in indexedDB or webSQL had to be abandoned due to the lack of synchronous api implementations.
To be or not to be
The first question I asked myself was it worth caching a card at all? That is, whether a specific city map will be used and whether detailed detailing is necessary. In my case, it was enough to have a cache of Minsk for a small zoom (10-15).
The second question: how much space will the cache take? If the average size of the sprite is 20 kb, then theoretically for a zoom of 10 (Minsk fully accommodates) you need 1 sprite (20 kb), for 11 - 4 (100 kb), for 12 - 16 (420 kb), for 13 - 64 (1.7 mb), for 14 - 256 (6.8 mb), 15 - 1024 (27 mb). A cache with a zoom of 14 seemed sufficient.
Download
I decided to take a real map and real sprites to find out how much space the cache actually takes. To do this, it was necessary to solve several school problems: create a polygon with a minimum perimeter from a set of points,
translate the polar coordinates into the coordinates of the sprites of the map and
find the sprites located in the polygon . After the
script was ready I downloaded the sprites and got the following results (in brackets the total space occupied for this zoom):
Zoom | Theoretical number of sprites | Theoretical sprites size | The actual number of sprites | Real size sprites |
---|
9 | one | 20 kb (20 kb) | 2 | 52 kb (52 kb) |
ten | one | 20 kb (40 kb) | 3 | 72 kb (124 kb) |
eleven | four | 80 kb (100 kb) | 7 | 204 kb (328 kb) |
12 | sixteen | 320 kb (420 kb) | 17 | 348 kb (676 kb) |
13 | 64 | 1.3 mb (1.7 mb) | 48 | 820 kb (1.5 mb) |
14 | 256 | 5.1 mb (6.8 mb) | 158 | 2.2 mb (3.7 mb) |
15 | 1024 | 20.5 mb (27 mb) | 586 | 5.5 mb (9.3 mb) |
sixteen | 4096 | 82 mb (109 mb) | 2264 | 15 mb (24.3 mb) |
Sprites were downloaded if they were inside the Minsk ring road or if the objects I needed were on these sprites. Thus it turned out to significantly reduce the space occupied by sprites.
Since I had sprites, it remained to make the map work offline.
Without network
In order for the map to work with cached sprites, you need to tell it where to get the data, you can do it simply:
- map. mapTypes . set ( "LocalGmap" , new google. maps . ImageMapType ( {
- getTileUrl : function ( coord , zoom ) {
- return "cache /" + zoom + "/" + coord. x + "_" + coord. y + ".png"
- } ,
- tileSize : new google. maps . Size ( 256 , 256 ) ,
- name : "LocalGmap" ,
- maxZoom : 15
- } ) ) ;
The getTileUrl function returns a value that is inserted into the src attribute of the image, therefore, if we have a base64 view of images stored in localStorage, we can implement the map cache like this:
- map. mapTypes . set ( "WebStorageGmap" , new google. maps . ImageMapType ( {
- getTileUrl : function ( coord , zoom ) {
- return localStorage. getItem ( [ zoom , coord. x , coord. y ] . join ( '_' ) ) ;
- } ,
- tileSize : new google. maps . Size ( 256 , 256 ) ,
- name : "WebStorageGmap" ,
- maxZoom : 15
- } ) ) ;
But for now we are still tied to scripts, pictures and cursors google maps api.
Let's start with the most important script:
http://maps.googleapis.com/maps/api/js?sensor=false . Download and replace the script with its local version, which we call gmapapi.js. This script mentions many references to some data.
Run again and see which scripts are loaded. This is
http://maps.gstatic.com/cat_js/intl/en_us/mapfiles/api-3/8/5/main.js and many more scripts similar to
http://maps.gstatic.com/cat_js/intl/ en_us / mapfiles / api-3/8/5 / {map, marker} .js .
The first script contains the kernel, the rest - additional components. Downloading main.js and since there is no mention of additional components in gmapapi.js, then quickly looking at main.js we get all the components of interest that we download in components.js:
google. maps .__ gjsload__ ( 'common' , ...
google. maps .__ gjsload__ ( 'controls' , ...
google. maps .__ gjsload__ ( 'directions' , ...
google. maps .__ gjsload__ ( 'distance_matrix' , ...
google. maps .__ gjsload__ ( 'drawing_impl' , ...
google. maps .__ gjsload__ ( 'elevation' , ...
google. maps .__ gjsload__ ( 'geocoder' , ...
google. maps .__ gjsload__ ( 'geometry' , ...
google. maps .__ gjsload__ ( 'infowindow' , ...
google. maps .__ gjsload__ ( 'kml' , ...
google. maps .__ gjsload__ ( 'layers' , ...
google. maps .__ gjsload__ ( 'map' , ...
google. maps .__ gjsload__ ( 'marker' , ...
google. maps .__ gjsload__ ( 'maxzoom' , ...
google. maps .__ gjsload__ ( 'onion' , ...
google. maps .__ gjsload__ ( 'overlay' , ...
google. maps .__ gjsload__ ( 'places_impl' , ...
google. maps .__ gjsload__ ( 'poly' , ...
google. maps .__ gjsload__ ( 'search_impl' , ...
google. maps .__ gjsload__ ( 'stats' , ...
google. maps .__ gjsload__ ( 'streetview' , ...
google. maps .__ gjsload__ ( 'usage' , ...
google. maps .__ gjsload__ ( 'util' , ...
Now, in gmapapi.js, we replace the load of main.js of an external file with a local file, and also we will load components.js, in order not to load the necessary components after initializing the map.
Look further: some scripts are loaded from
http://maps.googleapis.com , which make incomprehensible magic:
- AuthenticationService.Authenticate
- QuotaService.RecordEvent
- StaticMapService.GetMapImage
- ViewportInfoService.GetViewportInfo
- gen_204
- ft
We look for where they are mentioned, and this, as it turned out, is gmapapi.js. We substitute links to these scripts with local ones (for the fallback in the application cache to work). Add an empty.js script that will do nothing and add a bunch of “magic_ script empty.js” to the fallback section of our file manifest.
FALLBACK:
gmapcache / googleapis / maps / api / js / AuthenticationService.Authenticate gmapcache / empty.js
gmapcache / googleapis / maps / api / js / QuotaService.RecordEvent gmapcache / empty.js
gmapcache / googleapis / maps / api / js / StaticMapService.GetMapImage gmapcache / empty.js
gmapcache / googleapis / maps / api / js / ViewportInfoService.GetViewportInfo gmapcache / empty.js
gmapcache / googleapis / maps / gen_204 gmapcache / empty.js
gmapcache / googleapis / mapslt / ft gmapcache / empty.js
With all the scripts, now the pictures.
All images that are not sprites are loaded from
http://maps.gstatic.com/mapfiles/ . A quick search across all files says that the string is mentioned in one place in gmapapi.js. We swing locally all pictures, we replace the found link with the local one.
Now for full work offline we push everything into the manifest file.
CACHE MANIFEST
NETWORK:
*
CACHE:
index.html
style.css
script.js
gmapapi.js
gmapcache / main.js
gmapcache / components.js
gmapcache / empty.js
gmapcache / gstatic / arrow-down.png
gmapcache / gstatic / cb / mod_cb_scout / cb_scout_sprite_api_003.png
gmapcache / gstatic / cb / target_locking.gif
gmapcache / gstatic / google_white.png
gmapcache / gstatic / iw3.png
gmapcache / gstatic / iws3.png
gmapcache / gstatic / mapcontrols3d7.png
gmapcache / gstatic / markers2 / marker_sprite.png
gmapcache / gstatic / mv / imgs8.png
gmapcache / gstatic / rotate2.png
gmapcache / gstatic / szc4.png
gmapcache / gstatic / transparent.png
gmapcache / gstatic / openhand_8_8.cur
gmapcache / gstatic / closedhand_8_8.cur
Everything, fully working offline map is ready!
Here are all the changes in gmapapi.js:
It was | It became |
---|
[ " http://mt0.googleapis.com/mapslt/ft?hl=en-USu0026 " , " http://mt1.googleapis.com/mapslt/ft?hl=en-USu0026 " ] | [ "gmapcache / googleapis / mapslt / ft? hl = en-USu0026" ] |
[ "en-US" , "US" , null , 0 , null , null , " http://maps.gstatic.com/mapfiles/ " , " http://csi.gstatic.com " , " https://maps.googleapis.com " , " http://maps.googleapis.com " ] | [ "en-US" , "US" , null , 0 , null , null , "gmapcache / gstatic /" , " http://csi.gstatic.com " , " https://maps.googleapis.com " , "gmapcache / googleapis" ] |
getScript ( " http://maps.gstatic.com/intl/en_us/mapfiles/api-3/8/5/main.js " ) ; | getScript ( "gmapcache / main.js" ) ; getScript ( "gmapcache / components.js" ) ; |
In fact, gmapapi.js stores all settings for maps and there you can also specify links to sprites.
What's next?
Actually that's all.
So what I did:
- downloaded sprites locally;
- downloaded all the resources used locally (scripts, pictures, cursors);
- replaced the mention of external resources with local ones and added files to the cache section of the file manifest;
- made it so that the necessary scripts are not loaded after initialization;
- Replaced unnecessary files with dummies and added a file manifest section to the fallback.
This project is on github
https://github.com/tbicr/OfflineMap , it consists of a
parser and a
site . Lazy can see the map here
http://offline-map.appspot.com .
To see the work offline in the browser right away, go to
http://offline-map.appspot.com , click “Prepare Web Storage”, wait a few seconds for the sprites to load, then turn off the Internet, click on WebStorageGmap and enjoy.
The “Prepare Web Storage” button uploads sprites to localStorage, and “Clear Web Storage” respectively clears it.
Card types:
- Map - standard google map map.
- Satellite is a standard google map map from a satellite.
- OSM - implementation of OSM maps with google api.
- MyGmap - implementation of a standard google map map.
- LocalGmap - implementation of a map stored locally (on the same host as the site).
- WebStorageGmap - implementation of the map stored in localStorage (you must first run “Prepare Web Storage”).
- LocalMyGmap is a hybrid implementation of LocalGmap and MyGmap, if the data is in a specific area on the map, works as LocalGmap, except as MyGmap.
- WebStorageMyGmap - a hybrid implementation of WebStorageGmap and MyGmap, if the data is in localStorage, works like WebStorageGmap, other than MyGmap.