Вступление
В свободное время я пишу приложение по поиску банкоматов в Минске. И как-то отправляясь в отпуск я остался без интернета на телефоне. Все бы хорошо, но мне нужно было найти банкомат, снять деньги и не опоздать на поезд. Я открыл свое приложение и сильно разочаровался, что не могу использовать карту офлайн. Конечно, без подключения к сети в наше время лучше из дома не выходить, но все же интернет на любимом мобильном устройстве может отсутствовать в самое не подходящее время.
Посмотрев на другие приложения на моем телефоне, я заметил, что они в лучшем случае кэшируют части карты, которые были загружены до этого. Это могло бы отчасти помочь мне, но не решало проблему полностью. После этого я задумался, стоит ли иметь возможность просматривать карту офлайн. Так как мое приложение не родное, а основанное на phonegap, те браузерное, то и рассказ будет о том, как можно кэшировать карту для браузерных приложений в частности используя google map api v3.
Идея
Как-то после всего этого я вспомнил, что google map api позволяет сделать свою реализацию карты (например как это советую для OSM). Сразу пришла идея подсунуть реализацию, которая будет доступна всегда, а это можно сделать либо скачивая карты в кэш при наличии соединения, либо поставляя кэш карты с приложением.
Сначала я думал использовать application cache, но я отказался от этой затеи, так как его api не предоставляет широких возможностей управлением загрузкой кэша.
В итоге решил просто поставлять кэш с приложением.
Также мне в голову пришла идея хранить спрайты в localStorage, но у этой реализации есть большие недостатки:
- ограничение размера localStorage;
- данные нужно хранить в 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 мб) |
Спрайты скачивались в том случае, если они находились внутри Минской кольцевой дороги или если на этих спрайтах находились нужные мне объекты. Таким образом получилось значительно сократить занимаемое спрайтами место.
Так как у меня были спрайты, оставалось заставить карту работать офлайн.
Без сети
Для того, чтобы карта работала с кэшируемыми спрайтами, нужно указать ей откуда брать данные, сделать это можно просто:
- 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
- }));
Функция getTileUrl возвращает значение, которое подставляется в атрибут src картинки, следовательно, если у нас в localStorage будут храниться base64 представления картинок, то можно реализовать кэш карты так:
- 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
- }));
Но пока что мы по-прежнему привязаны к скриптам, картинкам и курсорам 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:
Было | Стало |
---|---|
|
|
|
|
|
|
По сути в gmapapi.js хранятся все настройки для карт и там также можно указать и ссылки на спрайты.
Что дальше?
Собственно на этом все.
Итак, что я сделал:
- скачал спрайты локально;
- скачал все используемые ресурсы локально (скрипты, картинки, курсоры);
- заменил упоминание внешних ресурсов на локальные и добавил файлы в cache секцию манифест файла;
- сделал так, чтобы нужные скрипты не подгружались после инициализации;
- заменил ненужные файлы пустышками и добавил в 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.
Автор: tbicr
А пользовательское соглашение с гугл Вы не нарушаете?