Maps in a browser without a network

Introduction


In my free time, I write an application for finding ATMs in Minsk. And somehow, setting off on vacation, I was left without 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 card offline. Of course, in our time it’s better not to leave your home without connecting to a network, but still the Internet on your favorite mobile device may be absent at the most inopportune time.

Looking at other applications on my phone, I noticed that at best they cache parts of the map that were downloaded before. This might partly help me, but did not solve the problem completely. After that, I wondered if I should be able 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


Once, after all this, I remembered that the google map api allows you to make your own map implementation (for example, as I advise for OSM ). Immediately came the idea of ​​slipping an implementation that will always be available, and this can be done either by downloading cards to the cache if there is a connection, or by supplying the map cache with the application.

At first I thought of using application cache, but I abandoned this venture, since its api does not provide extensive options for managing cache loading.

In the end, I decided to just supply the cache with the application.

I also came up with the idea of ​​storing sprites in localStorage, but this implementation has big disadvantages:
  1. localStorage size limit;
  2. data must be stored in base64, which is approximately 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: is it worth caching a card at all? That is, whether a map of a certain city will be used and whether detailed detailing is needed. In my case, it was enough to have the Minsk cache for a small zoom (10-15).

Second question: how much space will the cache occupy? If the average size of the sprite is 20 kb, then theoretically for zoom 10 (Minsk fits completely) 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 zoom cache 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 many points, translate the polar coordinates into the coordinates of the map sprites and find the sprites in the polygon . After the script was ready, I downloaded the sprites and got the following results (in brackets is the total space for this zoom):
ZoomTheoretical number of spritesTheoretical sprite sizeThe actual number of spritesActual size of sprites
9120 kb (20 kb)252 kb (52 kb)
10120 kb (40 kb)372 kb (124 kb)
eleven480 kb (100 kb)7204 kb (328 kb)
1216320 kb (420 kb)17348 kb (676 kb)
thirteen641.3 mb (1.7 mb) 48820 kb (1.5 mb)
142565.1 mb (6.8 mb)1582.2 mb (3.7 mb)
fifteen102420.5 mb (27 mb)5865.5 mb (9.3 mb)
16409682 mb (109 mb)226415 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 was possible 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 card to work with cached sprites, you need to tell it where to get the data from, it can be done simply:
  1. map.mapTypes.set ("LocalGmap", new google.maps.ImageMapType ({
  2.     getTileUrl: function (coord, zoom) {
  3.        return "cache /" + zoom + "/" + coord.x + "_" + coord.y + ".png"
  4.     },
  5.     tileSize: new google.maps.Size (256, 256),
  6.     name: "LocalGmap",
  7.     maxZoom: 15
  8. }));

The getTileUrl function returns the value that is substituted into the src attribute of the image, therefore, if we have base64 image representations stored in localStorage, we can implement the map cache like this:
  1. map.mapTypes.set ("WebStorageGmap", new google.maps.ImageMapType ({
  2.     getTileUrl: function (coord, zoom) {
  3.        return localStorage.getItem ([zoom, coord.x, coord.y] .join ('_'));
  4.     },
  5.     tileSize: new google.maps.Size (256, 256),
  6.     name: "WebStorageGmap",
  7.     maxZoom: 15
  8. }));

But for now, we’re still tied to google maps api scripts, images, and cursors.

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 will call gmapapi.js. This script mentions many references to some data.

We start again and see which scripts are loading. 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 are additional components. Download main.js and since there is no mention of additional components in gmapapi.js, quickly looking at main.js we get all the components we are interested in, which 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 will replace loading main.js of the external file with a local one, and we will also load components.js so that it is not necessary to load the necessary components after the map is initialized.

We look further: from http://maps.googleapis.com some scripts are loaded that make strange magic:
  • AuthenticationService.Authenticate
  • QuotaService.RecordEvent
  • StaticMapService.GetMapImage
  • ViewportInfoService.GetViewportInfo
  • gen_204
  • ft

We are looking for where they are mentioned, and this, as it turned out, gmapapi.js. We replace the links to these scripts with local ones (to make fallback work in the application cache). We add the empty.js script that will not do anything and add the bundle “magic_ script empty.js” to the fallback section of our manifest file.
FALLBACK:
gmapcache / googleapis / maps / api / js / AuthenticationService.Authenticate gmapcache / empty.js
gmapcache / googleapis / maps / api / js / QuotaService.RecordEvent gmapcache / empty.js
gmapcache / googleiaps / maps GetMapImage gmapcache / empty.js
gmapcache / googleapis / maps / api / js / ViewportInfoService.GetViewportInfo gmapcache / empty.js
gmapcache / googleapis / maps / gen_204 gmapcache / empty.js
gmapcache / googleapis / maps

With scripts everything, now pictures.

All images that are not sprites are loaded from http://maps.gstatic.com/mapfiles/ . A quick search of all files says that this line is mentioned in one place in gmapapi.js. We download all the pictures locally, replace the found link with a local one.

Now for the 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 / cbsc mod /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 / gstatic2
map
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, a fully working offline map is ready!

I will give all the changes in gmapapi.js:
It wasHas become
[
    " 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 the settings for maps, and you can also specify links to sprites there.

What's next?


Actually that's all.

So what I did:
  1. downloaded sprites locally;
  2. downloaded all used resources locally (scripts, pictures, cursors);
  3. replaced the mention of external resources with local ones and added files to the cache section of the manifest file;
  4. made it so that the necessary scripts are not loaded after initialization;
  5. replaced unnecessary files with dummies and added a file manifest section to fallback.

This project is on the github https://github.com/tbicr/OfflineMap , it consists of a parser and a site . Lazy people can see the map here http://offline-map.appspot.com .

To see the work offline immediately in the browser, 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 WebStorageGmap and enjoy.

The “Prepare Web Storage” button uploads sprites to localStorage, and “Clear Web Storage” accordingly clears it.

Card Types:
  • Map - standard google map map.
  • Satellite - standard google map map from the satellite.
  • OSM - implementation of an OSM card with google api.
  • MyGmap - implementation of the 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 (first you need to run “Prepare Web Storage”).
  • LocalMyGmap is a hybrid implementation of LocalGmap and MyGmap, if the data is in a certain area on the map, it works like LocalGmap, otherwise like MyGmap.
  • WebStorageMyGmap is a hybrid implementation of WebStorageGmap and MyGmap, if the data is in localStorage, it works like WebStorageGmap, otherwise like MyGmap.

Also popular now: