Pull to refresh

Карты в браузере без сети

Reading time 8 min
Views 28K

Вступление


В свободное время я пишу приложение по поиску банкоматов в Минске. И как-то отправляясь в отпуск я остался без интернета на телефоне. Все бы хорошо, но мне нужно было найти банкомат, снять деньги и не опоздать на поезд. Я открыл свое приложение и сильно разочаровался, что не могу использовать карту офлайн. Конечно, без подключения к сети в наше время лучше из дома не выходить, но все же интернет на любимом мобильном устройстве может отсутствовать в самое не подходящее время.

Посмотрев на другие приложения на моем телефоне, я заметил, что они в лучшем случае кэшируют части карты, которые были загружены до этого. Это могло бы отчасти помочь мне, но не решало проблему полностью. После этого я задумался, стоит ли иметь возможность просматривать карту офлайн. Так как мое приложение не родное, а основанное на phonegap, те браузерное, то и рассказ будет о том, как можно кэшировать карту для браузерных приложений в частности используя google map api v3.

Идея


Как-то после всего этого я вспомнил, что google map api позволяет сделать свою реализацию карты (например как это советую для OSM). Сразу пришла идея подсунуть реализацию, которая будет доступна всегда, а это можно сделать либо скачивая карты в кэш при наличии соединения, либо поставляя кэш карты с приложением.

Сначала я думал использовать application cache, но я отказался от этой затеи, так как его api не предоставляет широких возможностей управлением загрузкой кэша.

В итоге решил просто поставлять кэш с приложением.

Также мне в голову пришла идея хранить спрайты в localStorage, но у этой реализации есть большие недостатки:
  1. ограничение размера localStorage;
  2. данные нужно хранить в base64, что примерно на 30% больше реального размера.

От идеи хранения спрайтов в indexedDB или webSQL пришлось отказаться из-за отсутствия синхронных реализаций api.

Быть или не быть


Первый вопрос, который я себе задал: стоит ли вообще кэшировать карту? То есть будет ли использоваться карта определенного города и нужна ли подробная детализация. В моем случае достаточно было иметь кэш Минска для небольшого зума (10-15).

Второй вопрос: сколько места будет занимать кэш? Если средний размер спрайта брать 20 кб, то теоретически для зума 10 (полностью вмещается Минск) нужен 1 спрайт (20 кб), для 11 — 4 (100 кб), для 12 — 16 (420 кб), для 13 — 64 (1.7 мб), для 14 — 256 (6.8 мб), 15 — 1024 (27 мб). Кэш с зумом 14 казался достаточным.

Скачивание


Я решил взять реальную карту и реальные спрайты, чтобы узнать, сколько места займет кэш на самом деле. Для этого потребовалось решить несколько школьных задач: создать многоугольник с минимальным периметром из множества точек, перевести полярные координаты в координаты спрайтов карты и найти спрайты находящиеся в многоугольнике. После того, как скрипт был готов я скачал спрайты и получил следующие результаты (в скобках общее занимаемое место для данного зума):
Зум Теоретическое количество спрайтов Теоретический размер спрайтов Реальное количество спрайтов Реальный размер спрайтов
9 1 20 кб (20 кб) 2 52 кб (52 кб)
10 1 20 кб (40 кб) 3 72 кб (124 кб)
11 4 80 кб (100 кб) 7 204 кб (328 кб)
12 16 320 кб (420 кб) 17 348 кб (676 кб)
13 64 1.3 мб (1.7 мб) 48 820 кб (1.5 мб)
14 256 5.1 мб (6.8 мб) 158 2.2 мб (3.7 мб)
15 1024 20.5 мб (27 мб) 586 5.5 мб (9.3 мб)
16 4096 82 мб (109 мб) 2264 15 мб (24.3 мб)

Спрайты скачивались в том случае, если они находились внутри Минской кольцевой дороги или если на этих спрайтах находились нужные мне объекты. Таким образом получилось значительно сократить занимаемое спрайтами место.

Так как у меня были спрайты, оставалось заставить карту работать офлайн.

Без сети


Для того, чтобы карта работала с кэшируемыми спрайтами, нужно указать ей откуда брать данные, сделать это можно просто:
  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. }));

Функция getTileUrl возвращает значение, которое подставляется в атрибут src картинки, следовательно, если у нас в localStorage будут храниться base64 представления картинок, то можно реализовать кэш карты так:
  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. }));

Но пока что мы по-прежнему привязаны к скриптам, картинкам и курсорам google maps api.

Начнем с самого главного скрипта: http://maps.googleapis.com/maps/api/js?sensor=false. Скачиваем и заменяем скрипт на его локальную версию, которую назовем gmapapi.js. В этом скрипте упоминается много ссылок на какие-то данные.

Запускаем еще раз и смотрим, какие скрипты загружаются. Это http://maps.gstatic.com/cat_js/intl/en_us/mapfiles/api-3/8/5/main.js и еще много скриптов похожих на http://maps.gstatic.com/cat_js/intl/en_us/mapfiles/api-3/8/5/{map,marker}.js.

Первый скрипт содержит ядро, остальные — дополнительные компоненты. Качаем main.js и так как в gmapapi.js нет никакого упоминания о дополнительных компонентах, то быстро просмотрев main.js получаем все интересующие нас компоненты, которые качаем в 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', …

Теперь в gmapapi.js заменим загрузку main.js внешнего файла на локальный, а также будем загружать components.js, чтобы не требовалось подгружать нужные компоненты после инициализации карты.

Смотрим дальше: с http://maps.googleapis.com грузятся какие-то скрипты, которые делают непонятную магию:
  • AuthenticationService.Authenticate
  • QuotaService.RecordEvent
  • StaticMapService.GetMapImage
  • ViewportInfoService.GetViewportInfo
  • gen_204
  • ft

Ищем где они упоминаются, а это, как оказалось, gmapapi.js. Подменяем ссылки на эти скрипты локальными (чтобы заработал fallback в application cache). Добавляем скрипт empty.js, который ничего не будет делать и добавляем связку “магический_скрипт empty.js” в fallback секцию нашего манифест файла.
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

Со скриптами всё, теперь картинки.

Все картинки, не являющиеся спрайтами грузятся с http://maps.gstatic.com/mapfiles/. Быстрый поиск по всем файлам говорит, что данная строка упоминается в одном месте в gmapapi.js. Качаем локально все картинки, заменяем найденную ссылку на локальную.

Теперь для полной работы офлайн запихиваем все в манифест файл.
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

Все, полностью рабочая офлайн карта готова!

Приведу все изменений в gmapapi.js:
Было Стало
[
    "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");

По сути в gmapapi.js хранятся все настройки для карт и там также можно указать и ссылки на спрайты.

Что дальше?


Собственно на этом все.

Итак, что я сделал:
  1. скачал спрайты локально;
  2. скачал все используемые ресурсы локально (скрипты, картинки, курсоры);
  3. заменил упоминание внешних ресурсов на локальные и добавил файлы в cache секцию манифест файла;
  4. сделал так, чтобы нужные скрипты не подгружались после инициализации;
  5. заменил ненужные файлы пустышками и добавил в fallback секцию манифест файла.

Этот проект есть на гитхабе https://github.com/tbicr/OfflineMap, он состоит из парсера и сайта. Ленивые могут посмотреть карту здесь http://offline-map.appspot.com.

Чтобы увидеть работу офлайн сразу в браузере, идем http://offline-map.appspot.com, жмем “Prepare Web Storage”, ждем несколько секунд пока спрайты загрузятся, после этого отключаем интернет, жмем WebStorageGmap и наслаждаемся.

Кнопка “Prepare Web Storage” закачивает спрайты в localStorage, а “Clear Web Storage” соответственно и его очищает.

Типы карт:
  • Map — стандартная google map карта.
  • Satellite — стандартная google map карта со спутника.
  • OSM — реализация OSM карты с google api.
  • MyGmap — реализация стандартной google map карты.
  • LocalGmap — реализация карты хранящейся локально (на одном хосте с сайтом).
  • WebStorageGmap — реализация карты хранящейся в localStorage (сначала надо запустить “Prepare Web Storage”).
  • LocalMyGmap — гибридная реализация LocalGmap и MyGmap, если данные находятся в определенной области на карте, работает как LocalGmap, иначе как MyGmap.
  • WebStorageMyGmap — гибридная реализация WebStorageGmap и MyGmap, если данные находятся в localStorage, работает как WebStorageGmap, иначе как MyGmap.
Tags:
Hubs:
+30
Comments 12
Comments Comments 12

Articles