Карты в браузере без сети: open source наносит ответный удар

    Как-то давно я писал о том как можно в вебе использовать карты без сети и пытался сделать это с помощью гугло карт. К сожалению условия использования запрещали модифицировать ресурсы, а написанный мною код работал только с localStorage, поэтому я решил перейти на светлую сторону силы, где код открыт, прост и понятен.

    Чего же я хочу?


    Я хочу сделать кэширование карты для полной работы без сети, те в первый раз загружаешь карту, просматриваешь интересующие тайлы (которые при этом будут кэшироваться) и в следующий раз карта с просмотренными тайлами будет полностью доступна без сети.

    В принципе кэшировать на лету не обязательно и можно делать то же самое отдельно и для определенного региона. Но я всего лишь хочу показать подход.

    Что же у нас есть?


    В современном вебе для хранения наших данных могут подойти:
    Application Cache — для статики, но не для тайлов.
    Local Storage — с использованием base64 data uri, синхронно, поддерживается везде, но очень мало места.
    Indexed DB — с использование base64 data uri, асинхронно, поддерживается в полноценных и мобильных хроме, ff, ie10.
    Web SQL — с использование base64 data uri, асинхронно, обозначен как устаревший, поддерживается в полноценных и мобильных хроме, сафари, опере, браузере андроида.
    File Writer — только хром.

    Также можно попробовать использовать блобы и блоб урлы для уменьшения занимаемого тайлами места, но это может работать только вместе с Indexed DB. Эту затею я пока оставлю.

    Итак, если комбинировать Application Cache, Indexed DB и Web SQL, то можно решить задачу хранения тайлов достаточную для нормального использования в современных браузерах, в том числе и мобильных.

    Теория


    В теории нам нужно:
    1. взять API;
    2. добавить всю статику в Application Cache;
    3. переопределить слой тайлов так, чтобы он загружал данные из наших асинхронных хранилищ;
    4. добавить логику по загрузке тайлов в хранилища.

    Хранилище


    Для начала организуем key-value хранилище с базовыми операциями add, delete, get для Indexed DB и Web SQL. Здесь есть одна магическая конструкция emr.fire('storageLoaded', storage);, которая будет вызываться после того как хранилище проинициализировано и готово к использованию, чтобы карта не падала при обращении к хранилищу.

    Реализация хранилища с помощью Indexed DB
    var getIndexedDBStorage = function () {
        var indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
    
        var IndexedDBImpl = function () {
            var self = this;
            var db = null;
            var request = indexedDB.open('TileStorage');
    
            request.onsuccess = function() {
                db = this.result;
                emr.fire('storageLoaded', self);
            };
    
            request.onerror = function (error) {
                console.log(error);
            };
    
            request.onupgradeneeded = function () {
                var store = this.result.createObjectStore('tile', { keyPath: 'key'});
                store.createIndex('key', 'key', { unique: true });
            };
    
            this.add = function (key, value) {
                var transaction = db.transaction(['tile'], 'readwrite');
                var objectStore = transaction.objectStore('tile');
                objectStore.put({key: key, value: value});
            };
    
            this.delete = function (key) {
                var transaction = db.transaction(['tile'], 'readwrite');
                var objectStore = transaction.objectStore('tile');
                objectStore.delete(key);
            };
    
            this.get = function (key, successCallback, errorCallback) {
                var transaction = db.transaction(['tile'], 'readonly');
                var objectStore = transaction.objectStore('tile');
                var result = objectStore.get(key);
                result.onsuccess = function () {
                    successCallback(this.result ? this.result.value : undefined);
                };
                result.onerror = errorCallback;
            };
        };
    
        return indexedDB ? new IndexedDBImpl() : null;
    };
    


    Реализация хранилища с помощью Web SQL
    var getWebSqlStorage = function () {
        var openDatabase = window.openDatabase;
    
        var WebSqlImpl = function () {
            var self = this;
            var db = openDatabase('TileStorage', '1.0', 'Tile Storage', 5 * 1024 * 1024);
            db.transaction(function (tx) {
                tx.executeSql('CREATE TABLE IF NOT EXISTS tile (key TEXT PRIMARY KEY, value TEXT)', [], function () {
                    emr.fire('storageLoaded', self);
                });
            });
    
            this.add = function (key, value) {
                db.transaction(function (tx) {
                    tx.executeSql('INSERT INTO tile (key, value) VALUES (?, ?)', [key, value]);
                });
            };
    
            this.delete = function (key) {
                db.transaction(function (tx) {
                    tx.executeSql('DELETE FROM tile WHERE key = ?', [key]);
                });
            };
    
            this.get = function (key, successCallback, errorCallback) {
                db.transaction(function (tx) {
                    tx.executeSql('SELECT value FROM tile WHERE key = ?', [key], function (tx, result) {
                        successCallback(result.rows.length ? result.rows.item(0).value : undefined);
                    }, errorCallback);
                });
            };
        };
    
        return openDatabase ? new WebSqlImpl() : null;
    };
    


    Создание хранилища
    var storage =  getIndexedDBStorage() || getWebSqlStorage() || null;
    if (!storage) {
        emr.fire('storageLoaded', null);
    }
    


    Предлагаю считать данную реализацию очень схематичной, думаю здесь есть над чем подумать, например, чтобы не блокировать инициализацию карты, пока инициализируется хранилище; запоминать какие тайлы есть в хранилище без непосредственного обращения к API; попытаться объединить несколько операций сохранения в одну транзакцию, чтобы уменьшить количество записей на диск; попробовать использовать блобы там где они поддерживаются. Возможно реализация Indexed DB в старых браузерах будет падать, тк в них может быть не реализовано событие onupgradeneeded.

    IMG to data URI & CORS


    Для того чтобы хранить тайлы нам нужно преобразовать их в data URI, то есть base64 представление. Для этого воспользуемся canvas и его методами toDataURL или getImageData:

    _imageToDataUri: function (image) {
        var canvas = window.document.createElement('canvas');
        canvas.width = image.width;
        canvas.height = image.height;
    
        var context = canvas.getContext('2d');
        context.drawImage(image, 0, 0);
    
        return canvas.toDataURL('image/png');
    }
    

    Так как html элемент img может принимать в качестве картинки любой доступный ресурс, в том числе и на авторизированных сервисах и локальной файловой системе, то возможность отправлять это содержимое третьей стороне представляет собой угрозу безопасности, поэтому картинки не разрешающие Access-Control-Allow-Origing для Вашего домена сохранить будет нельзя. Благо тайлы mapnik или же tile.openstreetmap.org имеют заголовок Access-Control-Allow-Origing: *, но для того чтобы все работало нужно установить флаг элемента img.crossOrigin в значение Anonymous.

    Работа CORS в данной реализации во всех мобильных браузерах не гарантируется, поэтому проще всего настроить для своего сайта прокси на своем домене или отключить проверку CORS например для Phoengap адептов. Лично у меня данный код не взлетел в андроидовском браузере по умолчанию (androin 4.0.4 sony xperia active), а в опере некоторые тайлы сохранялись странным образом (сравни что иногда получается и то что должно быть на самом деле, но это похоже на баг оперы).

    Здесь можно попробовать использовать WebWorkers + AJAX вместо canvas.

    Leaflet


    Итак нам понадобится популярное JS API карт с открытым кодом, одним из таких кандидатов является Leaflet.

    Немного посмотрев исходники можно найти метод тайлового слоя, который отвечает за непосредственное указание src для тайлов:

    _loadTile: function (tile, tilePoint) {
        tile._layer = this;
        tile.onload = this._tileOnLoad;
        tile.onerror = this._tileOnError;
    
        tile.src = this.getTileUrl(tilePoint);
    }
    

    То есть если переопределить этот класс и непосредственно данный метод для загрузки данных в src из хранилища, то мы сделаем то что нужно. Реализуем также добавление данных в хранилище, если они были загружены из сети и получим полноценное кэширование.

    Реализация для Leaflet
    var StorageTileLayer = L.TileLayer.extend({
        _imageToDataUri: function (image) {
            var canvas = window.document.createElement('canvas');
            canvas.width = image.width;
            canvas.height = image.height;
    
            var context = canvas.getContext('2d');
            context.drawImage(image, 0, 0);
    
            return canvas.toDataURL('image/png');
        },
    
        _tileOnLoadWithCache: function () {
            var storage = this._layer.options.storage;
            if (storage) {
                storage.add(this._storageKey, this._layer._imageToDataUri(this));
            }
            L.TileLayer.prototype._tileOnLoad.apply(this, arguments);
        },
    
        _setUpTile: function (tile, key, value, cache) {
            tile._layer = this;
            if (cache) {
                tile._storageKey = key;
                tile.onload = this._tileOnLoadWithCache;
                tile.crossOrigin = 'Anonymous';
            } else {
                tile.onload = this._tileOnLoad;
            }
            tile.onerror = this._tileOnError;
            tile.src = value;
        },
    
        _loadTile: function (tile, tilePoint) {
            this._adjustTilePoint(tilePoint);
            var key = tilePoint.z + ',' + tilePoint.y + ',' + tilePoint.x;
    
            var self = this;
            if (this.options.storage) {
                this.options.storage.get(key, function (value) {
                    if (value) {
                        self._setUpTile(tile, key, value, false);
                    } else {
                        self._setUpTile(tile, key, self.getTileUrl(tilePoint), true);
                    }
                }, function () {
                    self._setUpTile(tile, key, self.getTileUrl(tilePoint), true);
                });
            } else {
                self._setUpTile(tile, key, self.getTileUrl(tilePoint), false);
            }
        }
    });
    


    Сама же карта в данном случае будет инициализироваться следующим образом:

    var map = L.map('map').setView([53.902254, 27.561850], 13);
    new StorageTileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {storage: storage}).addTo(map);
    

    Также добавим наши ресурсы в Application Cache, чтобы карта могла полноценно работать без сети с закэшированными тайлами:

    Application Cache manifest для Leaflet
    CACHE MANIFEST
    
    NETWORK:
    *
    
    CACHE:
    index.html
    style.css
    event.js
    storage.js
    map.js
    run.js
    
    leaflet.css
    leaflet.js
    images/layers.png
    images/marker-icon.png
    images/marker-icon@2x.png
    images/marker-shadow.png
    


    Пример и его код на гитхабе.

    Mapbox (modesmaps)


    Еще одним кандидатом открытого JS API карт является mapbox основанного на modesmaps.

    Посмотрев исходники mapbox мы не найдем для нас ничего интересного, поэтому перейдем к исходникам modestmaps. Начнем с TemplatedLayer, который является обычным слоем карты с шаблонным провайдером, те код который нам нужен будет находится в классе слоя:

    MM.TemplatedLayer = function(template, subdomains, name) {
        return new MM.Layer(new MM.Template(template, subdomains), null, name);
    };
    

    Найдя использования шаблонного провайдера в слое карты можно заметить, что наш провайдер может возвращать либо URL тайла, либо готовый DOM элемент, причем DOM элемент сразу позиционируется, а URL тайла пересылается в requestManager:

    if (!this.requestManager.hasRequest(tile_key)) {
        var tileToRequest = this.provider.getTile(tile_coord);
        if (typeof tileToRequest == 'string') {
            this.addTileImage(tile_key, tile_coord, tileToRequest);
        } else if (tileToRequest) {
            this.addTileElement(tile_key, tile_coord, tileToRequest);
        }
    }
    

    addTileImage: function(key, coord, url) {
        this.requestManager.requestTile(key, coord, url);
    }
    

    addTileElement: function(key, coordinate, element) {
        element.id = key;
        element.coord = coordinate.copy();
        this.positionTile(element);
    }
    

    Сам же requestManager инициализируется в конструкторе слоя карты. Создание DOM элемента img и установка его src происходит в методе processQueue, который также дергается из слоя карты:

    processQueue: function(sortFunc) {
        if (sortFunc && this.requestQueue.length > 8) {
            this.requestQueue.sort(sortFunc);
        }
        while (this.openRequestCount < this.maxOpenRequests && this.requestQueue.length > 0) {
            var request = this.requestQueue.pop();
            if (request) {
                this.openRequestCount++;
                var img = document.createElement('img');
                img.id = request.id;
                img.style.position = 'absolute';
                img.coord = request.coord;
                this.loadingBay.appendChild(img);
                img.onload = img.onerror = this.getLoadComplete();
                img.src = request.url;
                request = request.id = request.coord = request.url = null;
            }
        }
    }
    

    То есть если мы переопределим данный метод, то также получим желаемый результат.

    Реализация для mapbox (modestmaps)
    var StorageRequestManager = function (storage) {
        MM.RequestManager.apply(this, []);
        this._storage = storage;
    };
    
    StorageRequestManager.prototype._imageToDataUri = function (image) {
        var canvas = window.document.createElement('canvas');
        canvas.width = image.width;
        canvas.height = image.height;
    
        var context = canvas.getContext('2d');
        context.drawImage(image, 0, 0);
    
        return canvas.toDataURL('image/png');
    };
    
    StorageRequestManager.prototype._createTileImage = function (id, coord, value, cache) {
        var img = window.document.createElement('img');
        img.id = id;
        img.style.position = 'absolute';
        img.coord = coord;
        this.loadingBay.appendChild(img);
        if (cache) {
            img.onload = this.getLoadCompleteWithCache();
            img.crossOrigin = 'Anonymous';
        } else {
            img.onload = this.getLoadComplete();
        }
        img.onerror = this.getLoadComplete();
        img.src = value;
    };
    
    StorageRequestManager.prototype._loadTile = function (id, coord, url) {
        var self = this;
        if (this._storage) {
            this._storage.get(id, function (value) {
                if (value) {
                    self._createTileImage(id, coord, value, false);
                } else {
                    self._createTileImage(id, coord, url, true);
                }
            }, function () {
                self._createTileImage(id, coord, url, true);
            });
        } else {
            self._createTileImage(id, coord, url, false);
        }
    };
    
    StorageRequestManager.prototype.processQueue = function (sortFunc) {
        if (sortFunc && this.requestQueue.length > 8) {
            this.requestQueue.sort(sortFunc);
        }
        while (this.openRequestCount < this.maxOpenRequests && this.requestQueue.length > 0) {
            var request = this.requestQueue.pop();
            if (request) {
                this.openRequestCount++;
                this._loadTile(request.id, request.coord, request.url);
                request = request.id = request.coord = request.url = null;
            }
        }
    };
    
    StorageRequestManager.prototype.getLoadCompleteWithCache = function () {
        if (!this._loadComplete) {
            var theManager = this;
            this._loadComplete = function(e) {
                e = e || window.event;
    
                var img = e.srcElement || e.target;
                img.onload = img.onerror = null;
    
                if (theManager._storage) {
                    theManager._storage.add(this.id, theManager._imageToDataUri(this));
                }
    
                theManager.loadingBay.removeChild(img);
                theManager.openRequestCount--;
                delete theManager.requestsById[img.id];
    
                if (e.type === 'load' && (img.complete ||
                    (img.readyState && img.readyState === 'complete'))) {
                    theManager.dispatchCallback('requestcomplete', img);
                } else {
                    theManager.dispatchCallback('requesterror', {
                        element: img,
                        url: ('' + img.src)
                    });
                    img.src = null;
                }
    
                setTimeout(theManager.getProcessQueue(), 0);
            };
        }
        return this._loadComplete;
    };
    
    MM.extend(StorageRequestManager, MM.RequestManager);
    
    var StorageLayer = function(provider, parent, name, storage) {
        this.parent = parent || document.createElement('div');
        this.parent.style.cssText = 'position: absolute; top: 0px; left: 0px;' +
            'width: 100%; height: 100%; margin: 0; padding: 0; z-index: 0';
        this.name = name;
        this.levels = {};
        this.requestManager = new StorageRequestManager(storage);
        this.requestManager.addCallback('requestcomplete', this.getTileComplete());
        this.requestManager.addCallback('requesterror', this.getTileError());
        if (provider) {
            this.setProvider(provider);
        }
    };
    
    MM.extend(StorageLayer, MM.Layer);
    
    var StorageTemplatedLayer = function(template, subdomains, name, storage) {
        return new StorageLayer(new MM.Template(template, subdomains), null, name, storage);
    };
    


    Сама же карта в данном случае будет инициализироваться следующим образом:

    var map = mapbox.map('map');
    map.addLayer(new StorageTemplatedLayer('http://{S}.tile.osm.org/{Z}/{X}/{Y}.png', ['a', 'b', 'c'], undefined, storage));
    map.ui.zoomer.add();
    map.ui.zoombox.add();
    map.centerzoom({lat: 53.902254, lon: 27.561850}, 13);
    

    Также добавим наши ресурсы в Application Cache, чтобы карта могла полноценно работать без сети с закэшированными тайлами:

    Application Cache manifest для Mapbox (modestmaps)
    CACHE MANIFEST
    
    NETWORK:
    *
    
    CACHE:
    index.html
    style.css
    event.js
    storage.js
    map.js
    run.js
    
    mapbox.css
    mapbox.js
    map-controls.png
    


    Пример и его код на гитхабе.

    OpenLayers


    И последним кандидатом открытого JS API карт является OpenLayers.

    Мне пришлось потратить какое-то время чтобы разобраться как запустить минимальный вид, в итоге мой файл для сборки приобрел следующий вид:

    [first]
    
    [last]
    
    [include]
    OpenLayers/Map.js
    OpenLayers/Layer/OSM.js
    OpenLayers/Control/Zoom.js
    OpenLayers/Control/Navigation.js
    OpenLayers/Control/TouchNavigation.js
    
    [exclude]
    

    Я буду использовать OpenLayers.Layer.OSM, поэтому начну поиск с него:

    url: [
        'http://a.tile.openstreetmap.org/${z}/${x}/${y}.png',
        'http://b.tile.openstreetmap.org/${z}/${x}/${y}.png',
        'http://c.tile.openstreetmap.org/${z}/${x}/${y}.png'
    ]
    

    OpenLayers.Layer.OSM наследуется от OpenLayers.Layer.XYZ с переопределенными URL. Здесь интересен метод getURL:

    getURL: function (bounds) {
        var xyz = this.getXYZ(bounds);
        var url = this.url;
        if (OpenLayers.Util.isArray(url)) {
            var s = '' + xyz.x + xyz.y + xyz.z;
            url = this.selectUrl(s, url);
        }
    
        return OpenLayers.String.format(url, xyz);
    }
    

    Также интересен метод getXYZ, который можно использовать для создания ключа:

    getXYZ: function(bounds) {
        var res = this.getServerResolution();
        var x = Math.round((bounds.left - this.maxExtent.left) /
            (res * this.tileSize.w));
        var y = Math.round((this.maxExtent.top - bounds.top) /
            (res * this.tileSize.h));
        var z = this.getServerZoom();
    
        if (this.wrapDateLine) {
            var limit = Math.pow(2, z);
            x = ((x % limit) + limit) % limit;
        }
    
        return {'x': x, 'y': y, 'z': z};
    }
    

    Сам OpenLayers.Layer.XYZ наследуется от OpenLayers.Layer.Grid, у которого есть метод addTile и который внутри себя создает тайлы с помощью tileClass, которым является OpenLayers.Tile.Image:

    addTile: function(bounds, position) {
        var tile = new this.tileClass(
            this, position, bounds, null, this.tileSize, this.tileOptions
        );
        this.events.triggerEvent("addtile", {tile: tile});
        return tile;
    }
    

    В OpenLayers.Tile.Image src задается в методе setImgSrc:

    setImgSrc: function(url) {
        var img = this.imgDiv;
        if (url) {
            img.style.visibility = 'hidden';
            img.style.opacity = 0;
            if (this.crossOriginKeyword) {
                if (url.substr(0, 5) !== 'data:') {
                    img.setAttribute("crossorigin", this.crossOriginKeyword);
                } else {
                    img.removeAttribute("crossorigin");
                }
            }
            img.src = url;
        } else {
            this.stopLoading();
            this.imgDiv = null;
            if (img.parentNode) {
                img.parentNode.removeChild(img);
            }
        }
    }
    

    Но в нем не задаются обработчики onload и onerror. Сам метод дергается из initImage, где эти обработчики и вешаются:

    initImage: function() {
        this.events.triggerEvent('beforeload');
        this.layer.div.appendChild(this.getTile());
        this.events.triggerEvent(this._loadEvent);
        var img = this.getImage();
        if (this.url && img.getAttribute("src") == this.url) {
            this._loadTimeout = window.setTimeout(
                OpenLayers.Function.bind(this.onImageLoad, this), 0
            );
        } else {
            this.stopLoading();
            if (this.crossOriginKeyword) {
                img.removeAttribute("crossorigin");
            }
            OpenLayers.Event.observe(img, "load",
                OpenLayers.Function.bind(this.onImageLoad, this)
            );
            OpenLayers.Event.observe(img, "error",
                OpenLayers.Function.bind(this.onImageError, this)
            );
            this.imageReloadAttempts = 0;
            this.setImgSrc(this.url);
        }
    }
    

    Можно заметить, что метод класса слоя getURL, а также initImage, дергаются из renderTile:

    renderTile: function() {
        if (this.layer.async) {
            var id = this.asyncRequestId = (this.asyncRequestId || 0) + 1;
            this.layer.getURLasync(this.bounds, function(url) {
                if (id == this.asyncRequestId) {
                    this.url = url;
                    this.initImage();
                }
            }, this);
        } else {
            this.url = this.layer.getURL(this.bounds);
            this.initImage();
        }
    }
    

    Итак если мы переопределим данный класс, то также получим желаемый результат.

    Реализация для OpenLayers
    var StorageImageTile = OpenLayers.Class(OpenLayers.Tile.Image, {
        _imageToDataUri: function (image) {
            var canvas = window.document.createElement('canvas');
            canvas.width = image.width;
            canvas.height = image.height;
    
            var context = canvas.getContext('2d');
            context.drawImage(image, 0, 0);
    
            return canvas.toDataURL('image/png');
        },
    
        onImageLoadWithCache: function() {
            if (this.storage) {
                this.storage.add(this._storageKey, this._imageToDataUri(this.imgDiv));
            }
            this.onImageLoad.apply(this, arguments);
        },
    
        renderTile: function() {
            var self = this;
            var xyz = this.layer.getXYZ(this.bounds);
            var key = xyz.z + ',' + xyz.y + ',' + xyz.x;
            var url = this.layer.getURL(this.bounds);
            if (this.storage) {
                this.storage.get(key, function (value) {
                    if (value) {
                        self.initImage(key, value, false);
                    } else {
                        self.initImage(key, url, true);
                    }
                }, function () {
                    self.initImage(key, url, true);
                });
            } else {
                self.initImage(key, url, false);
            }
        },
    
        initImage: function(key, url, cache) {
            this.events.triggerEvent('beforeload');
            this.layer.div.appendChild(this.getTile());
            this.events.triggerEvent(this._loadEvent);
            var img = this.getImage();
    
            this.stopLoading();
            if (cache) {
                OpenLayers.Event.observe(img, 'load',
                    OpenLayers.Function.bind(this.onImageLoadWithCache, this)
                );
                this._storageKey = key;
            } else {
                OpenLayers.Event.observe(img, 'load',
                    OpenLayers.Function.bind(this.onImageLoad, this)
                );
            }
            OpenLayers.Event.observe(img, 'error',
                OpenLayers.Function.bind(this.onImageError, this)
            );
            this.imageReloadAttempts = 0;
            this.setImgSrc(url);
        }
    });
    
    var StorageOSMLayer = OpenLayers.Class(OpenLayers.Layer.OSM, {
        async: true,
        tileClass: StorageImageTile,
    
        initialize: function(name, url, options) {
            OpenLayers.Layer.OSM.prototype.initialize.apply(this, arguments);
            this.tileOptions = OpenLayers.Util.extend({
                storage: options.storage
            }, this.options && this.options.tileOptions);
        },
    
        clone: function (obj) {
            if (obj == null) {
                obj = new StorageOSMLayer(this.name,
                    this.url,
                    this.getOptions());
            }
    
            obj = OpenLayers.Layer.Grid.prototype.clone.apply(this, [obj]);
            return obj;
        }
    });
    


    Сама же карта в данном случае будет инициализироваться следующим образом:

    var map = new OpenLayers.Map('map');
    map.addLayer(new StorageOSMLayer(undefined, undefined, {storage: storage}));
    var fromProjection = new OpenLayers.Projection('EPSG:4326');
    var toProjection   = new OpenLayers.Projection('EPSG:900913');
    var center = new OpenLayers.LonLat(27.561850, 53.902254).transform(fromProjection, toProjection);
    map.setCenter(center, 13);
    

    Также добавим наши ресурсы в Application Cache, чтобы карта могла полноценно работать без сети с закэшированными тайлами:

    Application Cache manifest для OpenLayers
    CACHE MANIFEST
    
    NETWORK:
    *
    
    CACHE:
    index.html
    style.css
    event.js
    storage.js
    map.js
    run.js
    
    theme/default/style.css
    OpenLayers.js
    


    Пример и его код на гитхабе.

    Еще я нашел уже готовую реализацию кэша OpenLayers.Control.CacheWrite, но только с использованием localStorage, что не очень интересно.

    Заключение


    Собственно я получил что хотел. Примеры шустро работают в хроме или если у Вас SSD, в лисе и опере наблюдаются тормаза при сохранении на диск, осла под рукой не оказалось. Тормоза проявляются и в мобильных браузерах, причем даже при чтении, что немного меня расстроило, тк для мобильных устройств данная задача более актуальна.

    Стандартного размера хранилища IndexedDB или WebSQL вполне хватит чтобы закэшировать город или больше, что делает применение подхода более интересным чем в варианте с localStorage.

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

    Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

    Какими JS API карт пользуетесь Вы?

    Поделиться публикацией

    Комментарии 21

      +2
      Тайлы имеют свойство обновляться. Тогда надо с каждым тайлом тянуть номер ревизии или сбрасывать весь кэш, если прилетело обновление.
        +2
        или поступить еще проще — использовать кэш только в случае отсутствия сети.
          0
          Если неизвесно какие именно тайлы, те пользователь может выбирать любую облать, то да, если же тайлы определенные, например город_1, город_2 и тд, то можно реализовать сервис, который будет говорить, какие именно тайлы обновились с определенной даты. Или же раз в месяц обновлять все. В любом случае немного устаревшие тайлы лучше, чем их отсутсвие или медленная загрузка (например в метро или за городом).
      +2
      по поводу localstorage — можно использовать хак с поддоменами, но если делаете для себя, т.к. работает не везде, и могут пофиксить
        0
        Интересный подход.
        0
        Ipad и Iphone и так запоминают в кэше последние просмотры (не знаю как остальные ОС), а если вам нужно гарантированно сохранить схему проезда — то и для этого есть инструменты. А чаще интернета нет как раз тогда, когда хочется новый адрес посмотреть :-)
          +3
          Пост именно о веб, а для телефона есть Osmand+, которым можно хотя бы и весь мир загрузить с навигацией и поиском адресов.
            0
            Т.е. пост о «если дома отключат интернет»?
              0
              Веб это не только интернет дома. Вообщем еще есть ситуации, когда медленное соединение, плохой сигнал или даже его отсутсвие может играть отрицательную роль и не является редкостью. По этим причинам мне лично нужна возможность иметь кэш в своем приложении. А статья о том как можно сделать этот самый кэш на веб технологиях.

              Хотя да, дома это врядли нужно, а чтобы просто смотреть карту на мобильном устройстве существует много готовых решений.
                0
                Веб это не только интернет дома. Вообщем еще есть ситуации, когда медленное соединение, плохой сигнал или даже его отсутсвие может играть отрицательную роль и не является редкостью. По этим причинам мне лично нужна возможность иметь кэш в своем приложении. А статья о том как можно сделать этот самый кэш на веб технологиях.


                Просто я с трудом представляю ситуацию чтобы у меня был доступ к какому-то не мобильному устройству, я не был дома, не было интернета, а он бы был нужен)

                ИМХО, статья клевая для развития, но на практике я не думаю, что это может пригодиться.
                  0
                  Как фича все же больше для мобильных устройств. Как вариант карта городского транспорта или платежных терминалов. В первом случае Вы можете куда-нибудь спешить когда едете в метро, второй у Вас закончились деньги на мобиле когда они нужны. К тому же я бы не сказал, что мобильный интернет самый быстрый, по крайней мере не там где я живу.
          0
          Странно, что Вы незнакомы с проектом SAS.Планета. Цитата с сайта: «SAS.Планета — свободная программа, предназначенная для просмотра и загрузки спутниковых снимков высокого разрешения и обычных карт, представляемых такими сервисами, как Google Earth, Google Maps, Яндекс.карты и т.д.».

          Для мобильных платформ есть программы, которые умеют пользоваться скачанными наборами тайлов: SAS4WinCE и SAS4Android. Прошлым летом успешно использовал SAS4WinCE на навигаторе во время поездки на Северный Сахалин.
            +1
            Это отдельное програмное обеспечение, код которого я не могу вставить на свою веб страничку.
              –2
              Странно, что ваш коммент не первый :-) SAS — это первое, что приходит в голову при разговоре об оффлайновых картах. Поправка для топикстартера — да, это не для веб-страничек, разумеется.

              Печально, что у SAS4Android есть косяк — ограничение памяти на отображаемые тайлы из-за того, что код не нативный.
                +1
                Да есть SAS.Планета, есть GMapCatcher, для андроида есть Google Maps, Yandex Maps, MapsWithMe, OsmAnd, Navitel и тд. Альтернатив решающую ту или иную задачу можно найти достаточное количество. Просто скачать и просмотреть тайлы может конечно интересно, но есть готовые решения.
                0
                SAS.Планета, кроме всего прочего, еще (1) не то, чтобы сильно open source, (2) нарушает terms of use Google Maps, за что скачивающий IP регулярно банят.
                  +1
                  Откройте для себя bitbucket.org/sas_team
                    0
                    О, познавательно, спасибо! А, если не секрет, чего эту ссылку так прячут? На sasgis.ru о том, что исходники доступны — ни слова. На download отдают только готовые сборки, даже в FAQ есть куча неких дополнений для SAS.Планета в исходниках, а о том, что основная программа тоже доступна — опять же, ни слова.

                    А вообще — внушает. Это, пожалуй, вторая столь массивная и полновесная свободная программа на Delphi/Object Pascal, которую я знаю, не считая самих FreePascal и Lazarus.
                      0
                      Их бы портировать на FPC/LCL, чтобы под другие ОС собрать… Всё-таки Viking глюковат малость, но SAS через вайн ещё хуже.
                +1
                Я пользуюсь API карт от ТопПлан, topplan.ru/js/tpmap-5.0.0.js оно сырое, но работа идет.

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое