- PVSM.RU - https://www.pvsm.ru -
В свободное время я пишу приложение по поиску банкоматов в Минске. И как-то отправляясь в отпуск я остался без интернета на телефоне. Все бы хорошо, но мне нужно было найти банкомат, снять деньги и не опоздать на поезд. Я открыл свое приложение и сильно разочаровался, что не могу использовать карту офлайн. Конечно, без подключения к сети в наше время лучше из дома не выходить, но все же интернет на любимом мобильном устройстве может отсутствовать в самое не подходящее время.
Посмотрев на другие приложения на моем телефоне, я заметил, что они в лучшем случае кэшируют части карты, которые были загружены до этого. Это могло бы отчасти помочь мне, но не решало проблему полностью. После этого я задумался, стоит ли иметь возможность просматривать карту офлайн. Так как мое приложение не родное, а основанное на phonegap, те браузерное, то и рассказ будет о том, как можно кэшировать карту для браузерных приложений в частности используя google map api v3.
Как-то после всего этого я вспомнил, что google map api позволяет сделать свою реализацию карты (например как это советую для OSM [1]). Сразу пришла идея подсунуть реализацию, которая будет доступна всегда, а это можно сделать либо скачивая карты в кэш при наличии соединения, либо поставляя кэш карты с приложением.
Сначала я думал использовать application cache, но я отказался от этой затеи, так как его api не предоставляет широких возможностей управлением загрузкой кэша.
В итоге решил просто поставлять кэш с приложением.
Также мне в голову пришла идея хранить спрайты в localStorage, но у этой реализации есть большие недостатки:
От идеи хранения спрайтов в 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 казался достаточным.
Я решил взять реальную карту и реальные спрайты, чтобы узнать, сколько места займет кэш на самом деле. Для этого потребовалось решить несколько школьных задач: создать многоугольник с минимальным периметром из множества точек, перевести полярные координаты в координаты спрайтов карты [2] и найти спрайты находящиеся в многоугольнике [3]. После того, как скрипт [4] был готов я скачал спрайты и получил следующие результаты (в скобках общее занимаемое место для данного зума):
Зум | Теоретическое количество спрайтов | Теоретический размер спрайтов | Реальное количество спрайтов | Реальный размер спрайтов |
---|---|---|---|---|
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 [5]. Скачиваем и заменяем скрипт на его локальную версию, которую назовем gmapapi.js. В этом скрипте упоминается много ссылок на какие-то данные.
Запускаем еще раз и смотрим, какие скрипты загружаются. Это http://maps.gstatic.com/cat_js/intl/en_us/mapfiles/api-3/8/5/main.js [6] и еще много скриптов похожих на http://maps.gstatic.com/cat_js/intl/en_us/mapfiles/api-3/8/5/{map,marker}.js [7].
Первый скрипт содержит ядро, остальные — дополнительные компоненты. Качаем 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 [8] грузятся какие-то скрипты, которые делают непонятную магию:
Ищем где они упоминаются, а это, как оказалось, 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/ [9]. Быстрый поиск по всем файлам говорит, что данная строка упоминается в одном месте в 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 хранятся все настройки для карт и там также можно указать и ссылки на спрайты.
Собственно на этом все.
Итак, что я сделал:
Этот проект есть на гитхабе https://github.com/tbicr/OfflineMap [15], он состоит из парсера [4] и сайта [16]. Ленивые могут посмотреть карту здесь http://offline-map.appspot.com [17].
Чтобы увидеть работу офлайн сразу в браузере, идем http://offline-map.appspot.com [17], жмем “Prepare Web Storage”, ждем несколько секунд пока спрайты загрузятся, после этого отключаем интернет, жмем WebStorageGmap и наслаждаемся.
Кнопка “Prepare Web Storage” закачивает спрайты в localStorage, а “Clear Web Storage” соответственно и его очищает.
Типы карт:
Автор: tbicr
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/html5/5014
Ссылки в тексте:
[1] OSM: http://wiki.openstreetmap.org/wiki/Google_Maps_Example
[2] перевести полярные координаты в координаты спрайтов карты: http://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%BE%D0%B5%D0%BA%D1%86%D0%B8%D1%8F_%D0%9C%D0%B5%D1%80%D0%BA%D0%B0%D1%82%D0%BE%D1%80%D0%B0
[3] найти спрайты находящиеся в многоугольнике: http://ru.wikipedia.org/wiki/%D0%97%D0%B0%D0%B4%D0%B0%D1%87%D0%B0_%D0%BE_%D0%BF%D1%80%D0%B8%D0%BD%D0%B0%D0%B4%D0%BB%D0%B5%D0%B6%D0%BD%D0%BE%D1%81%D1%82%D0%B8_%D1%82%D0%BE%D1%87%D0%BA%D0%B8_%D0%BC%D0%BD%D0%BE%D0%B3%D0%BE%D1%83%D0%B3%D0%BE%D0%BB%D1%8C%D0%BD%D0%B8%D0%BA%D1%83#.D0.9C.D0.B5.D1.82.D0.BE.D0.B4_.D1.81.D1.83.D0.BC.D0.BC.D0.B8.D1.80.D0.BE.D0.B2.D0.B0.D0.BD.D0.B8.D1.8F_.D1.83.D0.B3.D0.BB.D0.BE.D0.B2
[4] скрипт: https://github.com/tbicr/OfflineMap/blob/master/parse.py
[5] http://maps.googleapis.com/maps/api/js?sensor=false: http://maps.googleapis.com/maps/api/js?sensor=false
[6] 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/main.js
[7] http://maps.gstatic.com/cat_js/intl/en_us/mapfiles/api-3/8/5/{map,marker}.js: http://maps.gstatic.com/cat_js/intl/en_us/mapfiles/api-3/8/5/%7Bmap,marker%7D.js
[8] http://maps.googleapis.com: http://maps.googleapis.com
[9] http://maps.gstatic.com/mapfiles/: http://maps.gstatic.com/mapfiles/
[10] http://mt0.googleapis.com/mapslt/ft?hl=en-USu0026: http://mt0.googleapis.com/mapslt/ft?hl=en-USu0026
[11] http://mt1.googleapis.com/mapslt/ft?hl=en-USu0026: http://mt1.googleapis.com/mapslt/ft?hl=en-USu0026
[12] http://csi.gstatic.com: http://csi.gstatic.com
[13] https://maps.googleapis.com: https://maps.googleapis.com
[14] http://maps.gstatic.com/intl/en_us/mapfiles/api-3/8/5/main.js: http://maps.gstatic.com/intl/en_us/mapfiles/api-3/8/5/main.js
[15] https://github.com/tbicr/OfflineMap: https://github.com/tbicr/OfflineMap
[16] сайта: https://github.com/tbicr/OfflineMap/tree/master/site
[17] http://offline-map.appspot.com: http://offline-map.appspot.com
Нажмите здесь для печати.