Merge pull request #195 from andygup/fix_manifest

Update to support ArcGIS JS API v3.9
This commit is contained in:
Andy 2014-06-11 15:35:04 -06:00
commit 30f858287c
21 changed files with 2477 additions and 675 deletions

View File

@ -42,7 +42,7 @@ Extends and overrides a feature layer. This library allows you to extend esri.la
##offlineTilesEnabler
Extends and overrides a tiled map service. Provides the ability to customize the extent used to cut the tiles. See the detailed description of basemap.prepareForOffline() in the "How To Use" section below to learn different options.
Extends and overrides a tiled map service. Provides the ability to customize the extent used to cut the tiles. See the detailed description of basemap.prepareForOffline() in the "How To Use" section to learn different options.
* __Click [here](doc/offlinetilesenabler.md) to see the full API doc for `offlineTilesEnabler`__
@ -57,6 +57,7 @@ Extends TileMapServiceLayer. You can display TPK files with this library.
* [Learn more about using the `tile` library](doc/howtousetiles.md)
* [Learn more about using the `edit` library](doc/howtouseeditlibrary.md)
* [Learn more about using the `tpk` library](doc/howtousetpklibrary.md)
* [Learn more abuut using an application cache with this library](doc/howtouseappcache.md)
##Setup Instructions
@ -68,7 +69,7 @@ Extends TileMapServiceLayer. You can display TPK files with this library.
##Samples
* `appcache-features.html` - shows how to work with the application manifest and features.
* `appcache-features.html` - shows how to work with the application manifest, tiles and features.
* `appcache-tiles.html` - shows how to work with the application manifest and map tiles.
* `attachments-editor.html` - demonstrates how to work with this library and feature attachments.
* `military-offline.html` - shows working with points, lines and polygons locally.

68
doc/howtouseappcache.md Normal file
View File

@ -0,0 +1,68 @@
Tips on using application cache
===============================
If you have a requirement to reload your application or restart the browser while offline then you will need to use the [application cache](http://appcachefacts.info/). Some developers also use application caches to speed up page reload performance. For example, Google uses an application cache when load their main web page.
The application cache will allow you to store any file that is required for offline use. The list includes html files, JavaScript libraries, CSS and images. Any file that your application requires to run normally will have to be referenced in the application cache.
Once an application is stored in the application cache it will be available the next time an application restarts.
## Using AppCaches with your ArcGIS web app
**Step 1** Make sure you are using an optimized build of the ArcGIS API for JavaScript. You can create an optimized build at [http://jso.arcgis.com/](http://jso.arcgis.com/). This will create a single file that contains all the necessary modules for your app. There are options to host the build via CDN or locally. Either approach will work.
NOTE: You cannot use the regular CDN for the ArcGIS API for JavaScript because the URL contains a redirect. Redirects are not allowed in an application cache and it will fail to load.
**Step 2** Create the application cache file. We have a [Grunt.js](http://gruntjs.com/) task included in the /samples directory to assist with this step. You will need to make some adjustments to the package.json file. It acts as the configuration file for the Grunt task.
**Step 3** Reference the application cache file from within your application. Here's an example of the syntax:
```html
<html manifest="appcache-features.appcache">
```
**Step 4** Be sure to include and use the `/utils/appCacheManager.js` library as a module in your application. This will enable you to monitor what's going on in the application cache and capture specific events. Here is a psuedo code example of how to instantiate it:
```js
appCacheManager = new AppCacheManager(true,true);
appCacheManager.on(appCacheManager.CACHE_EVENT,cacheEventHandler);
appCacheManager.on(appCacheManager.CACHE_ERROR,cacheErrorHandler);
appCacheManager.on(appCacheManager.CACHE_LOADED,cacheLoadedHandler);
```
In the `/samples` directory there are two examples, `appcache-features.html` and `appcache-tiles.html` that demonstrate how to use tiles, features and the appCacheManager with the application cache.
###Configuring your web server
Your web server must be able to serve up the MIME TYPE `TEXT/cache-manifest`. If this is missing there's a really good chance that the application cache file won't be served up to your app.
### Clearing the application cache in a browser
When you do testing with an application cache, any time you make a change to your application HTML, CSS or JS you will need to delete the existing application cache. Otherwise, any changes you make will not be reflected in the app.
**Simply deleting your web cache the normal way won't clear an application cache!**
In Chrome you can navigate to chrome://appcache-internals/ then select the appropriate cache and delete it. If you are testing on an Android device you can remotely debug from your laptop's Chrome instance.
In Safari iPhone and iPad go to settings and select "Clear Cookies and Data."
If you want to test on Firefox then try Tools > Options > Advanced > Network > Offline data > Clear Now. More info is available [here](https://developer.mozilla.org/en-US/docs/Web/HTML/Using_the_application_cache#Storage_location_and_clearing_the_offline_cache).
As for IE, this library doesn't currently support any versions.
### Where to place the file
The application cache file can live anywhere in your web directory. It's common to see it to be placed in the root.
###Support
Most modern browsers support application cache including IE v10 and v11, Firefox v28+, Chrome v33+, Safari v7+, iOS Safari v3.2+, and Android browser 2.1+. For more detailed info refer to [caniuse.com](http://caniuse.com/#search=appcache).
### References
[Support for application cache](http://caniuse.com/#search=appcache)
[Appcache Facts](http://appcachefacts.info/)
[Using the application cache - Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/HTML/Using_the_application_cache)

View File

@ -3,9 +3,14 @@ How to use the tiles library
##`tiles` library
The `tiles` library allows a developer to extend a tiled layer with offline support.
There are two approaches to using this set of libraries. The first approach is if you are using an ArcGIS.com Web Map, and the second approach is if you need to be able to restart or reload your application offline.
## Approach 1 - ArcGIS.com Map
Approach #1 is best for partial offline use cases and it uses the `offlineTilesEnabler.js` library. This approach will not allow you to reload or restart the application.
**Step 1** Configure paths for dojo loader to find the tiles and vendor modules (you need to set paths relative to the location of your html document), before loading ArcGIS JavaScript API
```html
@ -19,7 +24,7 @@ The `tiles` library allows a developer to extend a tiled layer with offline supp
}
}
</script>
<script src="//js.arcgis.com/3.7compact"></script>
<script src="//js.arcgis.com/3.8compact"></script>
```
@ -124,3 +129,72 @@ It calculates the geographic boundary of each of the tiles stored in the indexed
}
}
```
## Approach #2 - Custom TileLayer
This approach is best if you have requirements for restarting or reloading your application while offline. For this approach use the `OfflineTilesEnablerLayer.js` library. This library extends TileMapServiceLayer and you can use it with any Esri tiled basemap layer.
**Step 1** Configure paths for dojo loader to find the tiles and vendor modules (you need to set paths relative to the location of your html document), before loading ArcGIS JavaScript API
```html
<script>
// configure paths BEFORE loading arcgis or dojo libs
var locationPath = location.pathname.replace(/\/[^/]+$/, "");
var dojoConfig = {
paths: {
tiles: locationPath + "/../../lib/tiles",
vendor: locationPath + "/../../vendor"
}
}
</script>
<script src="//js.arcgis.com/3.8compact"></script>
```
**Step 2** Include the `tiles/offlineTilesEnabler` library in your app.
```js
require([
"esri/map",
"tiles/OfflineTilesEnablerLayer"],
function(Map,OfflineTilesEnablerLayer)
{
...
});
```
**Step 3** Create a new instance of `OfflineTilesEnablerLayer`. Note, when you instantiate the `Map` leave off the `basemap` property because we are adding a customer tile layer as our basemap. `OfflineTilesEnablerLayer` has three properties in the constructor. The first is the REST endpoint of the basemap you want to use, the second is the callback and the last is an optional parameter to preset the layer as online or offline. This will help with with drawing tiles correctly during offline restarts or reloads.
```js
tileLayer = new OfflineTilesEnablerLayer("http://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer",function(evt){
console.log("Tile Layer Loaded.");
},_isOnline);
var map = new Map("map",{
center: [-104.98,39.74], // long, lat
zoom: 8,
sliderStyle: "small"
});
map.addLayer(tileLayer);
```
All map events will continue to work normally. Although some methods that are typically available will now have to be accessed through the OfflineTilesEnablerLayer such as `getLevel()`, `getMaxZoom()`, and `getMinZoom()`.
To get the current extent you will need to monitor the `zoom-end` and `pan-end` events like this:
```js
map.on("zoom-end",function(evt){
_currentExtent = evt.extent;
});
map.on("pan-end",function(evt){
_currentExtent = evt.extent;
});
```

View File

@ -1,30 +1,25 @@
API Doc for offlineTilesEnabler
===============================
There are two different libraries for taking tiles offline: offlineTilesEnabler.js and OfflineTilesEnablerLayer.js. The first one, offlineTilesEnabler.js, is for use with ArcGIS.com web maps and partial offline scenario. You won't be able to restart or reload your app when using this library.
If you have a requirement to allow restarting or reloading then you should use OfflineTilesEnablerLayer.js. The OfflineTilesEnablerLayer.js library lets you create a custom basemap layer that extends TiledMapServiceLayer. To view the docs for this library scroll down on this page.
##offlineTilesEnabler
Extends and overrides a tiled map service. Provides the ability to customize the extent used to cut the tiles. See the detailed description of basemap.prepareForOffline() in the "How To Use" section below to learn different options.
Extends and overrides a tiled map service. For use with ArcGIS.com maps or partial-offline situations that don't require a browser restart or reload.
Provides the ability to customize the extent used to cut the tiles. See the detailed description of basemap.prepareForOffline() in the "How To Use" section below to learn different options.
###Constructor
Constructor | Description
--- | ---
`OfflineTilesEnabler()` | Creates an instance of the offlineTilesEnabler class. This library allows you to extend an ArcGISTiledMapServiceLayer with offline capability as well as manage the online/offline resynchronization process.
`OfflineTilesEnabler()` | Creates an instance of the offlineTilesEnabler class. This library allows you to extend an ArcGISTiledMapServiceLayer with partial offline capability as well as manage the online/offline resynchronization process.
###Methods
Methods | Returns | Description
--- | --- | ---
`extend(layer, callback)`|`callback(boolean, string)` |Overrides an ArcGISTiledMapServiceLayer. Callback is called after indexedDb store is initialized and informs the application whether it is indexedDb is supported or not.
###Properties
Property | Description
--- | ---
`layer.offline.proxyPath`| For CORS enabled servers this can be set to `null`. The default is to use the internal resource-proxy path: `libs/offline-editor-js/resource-proxy/proxy.php.` Don't forget to check your proxy configuration to allow connections for all possible services that you might be using. More information on using proxies with ArcGIS can be found here: [https://developers.arcgis.com/javascript/jshelp/ags_proxy.html](https://developers.arcgis.com/javascript/jshelp/ags_proxy.html).
###ArcGISTiledMapServiceLayer Overrides
Methods | Returns | Description
--- | --- | ---
`getTileUrl(level, row, col)` | Url | Use the tile url's level, row and column. Retrieves tiles as requested by the ArcGIS API for JavaScript. If a tile is in cache it is returned. If it is not in cache then one is retrieved over the internet.
`goOffline()` | nothing | This method puts the layer in offline mode. When in offline mode, the layer will not fetch any tile from the remote server. It will look up the tiles in the indexed db database and display them in the layer. If the tile can't be found in the local database it will show up blank (even if there is actual connectivity). The pair of methods `goOffline()` and `goOnline() `allows the developer to manually control the behaviour of the layer. Used in conjunction with the offline dectection library, you can put the layer in the appropriate mode when the offline condition changes.
`goOnline()` | nothing | This method puts the layer in online mode. When in online mode, the layer will behave as regular layers, fetching all tiles from the remote server. If there is no internet connectivity the tiles may appear thanks to the browsers cache, but no attempt will be made to look up tiles in the local database.
`getLevelEstimation(extent,` `level, tileSize)` | {level, tileCount, sizeBytes} | Returns an object that contains the number of tiles that would need to be downloaded for the specified extent and zoom level, and the estimated byte size of such tiles. This method is useful to give the user an indication of the required time and space before launching the actual download operation. The byte size estimation is very rough.
@ -38,3 +33,44 @@ Methods | Returns | Description
`estimateTileSize(callback)` | `callback(number)` | Retrieves one tile from a layer and then returns its size.
`prepareForOffline(` `minLevel, maxLevel, extent, ` `reportProgress)` | `callback(number)` | Retrieves tiles and stores them in the local cache. See the "How To Use" section below to learn more about customizing the use of this method.
###Properties
Property | Description
--- | ---
`layer.offline.proxyPath`| For CORS enabled servers this can be set to `null`. The default is to use the internal resource-proxy path: `libs/offline-editor-js/resource-proxy/proxy.php.` Don't forget to check your proxy configuration to allow connections for all possible services that you might be using. More information on using proxies with ArcGIS can be found here: [https://developers.arcgis.com/javascript/jshelp/ags_proxy.html](https://developers.arcgis.com/javascript/jshelp/ags_proxy.html).
##OfflineTilesEnablerLayer
Extends and overrides a tiled map service. This library creates a custom tiled map layer. It can be used in situations where a browser restart or reload is required.
###Constructor
Constructor | Description
--- | ---
`OfflineTilesEnablerLayer(url,callback,state)` | Creates an instance of the offlineTilesEnabler class. This library allows you to extend an ArcGISTiledMapServiceLayer with offline capability as well as manage the online/offline resynchronization process. Any Esri basemap REST endpoint should work. The state property is a boolean for specifying if the application is intializing the layer online (true) or offline (false). When you first load the map you should set this property to `true`.
###Methods
Methods | Returns | Description
--- | --- | ---
`goOffline()` | nothing | This method puts the layer in offline mode. When in offline mode, the layer will not fetch any tile from the remote server. It will look up the tiles in the indexed db database and display them in the layer. If the tile can't be found in the local database it will show up blank (even if there is actual connectivity). The pair of methods `goOffline()` and `goOnline() `allows the developer to manually control the behaviour of the layer. Used in conjunction with the offline dectection library, you can put the layer in the appropriate mode when the offline condition changes.
`goOnline()` | nothing | This method puts the layer in online mode. When in online mode, the layer will behave as regular layers, fetching all tiles from the remote server. If there is no internet connectivity the tiles may appear thanks to the browsers cache, but no attempt will be made to look up tiles in the local database.
`getLevelEstimation(extent,` `level, tileSize)` | {level, tileCount, sizeBytes} | Returns an object that contains the number of tiles that would need to be downloaded for the specified extent and zoom level, and the estimated byte size of such tiles. This method is useful to give the user an indication of the required time and space before launching the actual download operation. The byte size estimation is very rough.
`getExtentBuffer(buffer,extent)`| Extent | Returns a new extent buffered by a given measurement that's based on map units. For example, if you are using mercator map projection then the buffer property would be in meters and the new extent would be returned in mercactor.
`getTileUrlsByExtent(extent, level)` | Array | Returns an array of tile urls within a given map extent and zoom level.
`deleteAllTiles(callback)` | `callback(boolean, errors)` | Clears the local cache of tiles.
`getOfflineUsage(callback)` | `callback(size, error)` | Gets the size in bytes of the local tile cache.
`getTilePolygons(callback)` | `callback(polygon, error)` | Gets polygons representing all cached tiles. This is helpful to give users a visual feedback of the current content of the tile cache.
`saveToFile(filename, callback)` | `callback( boolean, error)` | Saves tile cache into a portable csv format.
`loadFromFile(filename, callback)` | `callback( boolean, error)` | Reads a csv file into local tile cache.
`estimateTileSize(callback)` | `callback(number)` | Retrieves one tile from a layer and then returns its size.
`prepareForOffline(` `minLevel, maxLevel, extent, ` `reportProgress)` | `callback(number)` | Retrieves tiles and stores them in the local cache. See the "How To Use" section below to learn more about customizing the use of this method.
`getMaxZoom(callback)` | `callback(number)` | Returns the maximum zoom level of the layer.
`getMinZoom(callback)` | `callback(number)` | Returns the minimum zoom level of the layer.
###Properties
Property | Description
--- | ---
`layer.offline.proxyPath`| For CORS enabled servers this can be set to `null`. The default is to use the internal resource-proxy path: `libs/offline-editor-js/resource-proxy/proxy.php.` Don't forget to check your proxy configuration to allow connections for all possible services that you might be using. More information on using proxies with ArcGIS can be found here: [https://developers.arcgis.com/javascript/jshelp/ags_proxy.html](https://developers.arcgis.com/javascript/jshelp/ags_proxy.html).

View File

@ -487,7 +487,7 @@ define([
*/
goOffline: function()
{
console.log("going offline");
console.log("offlineFeatureManager going offline");
this._onlineStatus = this.OFFLINE;
},
@ -498,7 +498,7 @@ define([
*/
goOnline: function(callback)
{
console.log("going online");
console.log("offlineFeaturesManager going online");
this._onlineStatus = this.RECONNECTING;
this._replayStoredEdits(function(success,responses)
{

View File

@ -0,0 +1,58 @@
/**
* Helper library for handling features during browser restarts or reloads.
*/
define(["esri/graphic"], function(Graphic) {
"use strict";
return {
/**
* Converts an array of graphics/features into JSON
* @param features
* @param updateEndEvent
* @param callback
*/
convertGraphicLayerToJSON: function(features,updateEndEvent,callback){
var layerDefinition = {};
layerDefinition.objectIdFieldName = updateEndEvent.target.objectIdField;
layerDefinition.globalIdFieldName = updateEndEvent.target.globalIdField;
layerDefinition.geometryType = updateEndEvent.target.geometryType;
layerDefinition.spatialReference = updateEndEvent.target.spatialReference;
layerDefinition.fields = updateEndEvent.target.fields;
var length = features.length;
var jsonArray = [];
for(var i=0; i < length; i++){
var jsonGraphic = features[i].toJson();
jsonArray.push(jsonGraphic);
if(i == (length - 1)) {
var featureJSON = JSON.stringify(jsonArray);
var layerDefJSON = JSON.stringify(layerDefinition);
callback(featureJSON,layerDefJSON);
break;
}
}
},
/**
* Create a featureDefinition
* @param featureLayer
* @param featuresArr
* @param geometryType
* @param callback
*/
getFeatureDefinition: function(/* Object */ featureLayer,/* Array */ featuresArr,/* String */ geometryType,callback){
var featureDefinition = {
"layerDefinition":featureLayer,
"featureSet":{
"features": featuresArr,
"geometryType": geometryType
}
}
callback(featureDefinition);
}
}
})

View File

@ -21,6 +21,7 @@
<serverUrl url="http://sampleserver1c.arcgisonline.com/arcgisoutput/" matchAll="true"/>),
<serverUrl url="http://services2.arcgis.com/" matchAll="true"/>)
<serverUrl url="http://andygup.github.io" matchAll="true"/>
<serverUrl url="http://esri.github.io" matchAll="true"/>
</serverUrls>
</ProxyConfig>

File diff suppressed because one or more lines are too long

View File

@ -26,7 +26,7 @@ define([],function()
*/
this.isSupported = function(){
if(!window.indexedDB){
if(!window.indexedDB && !window.openDatabase){
return false;
}

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
define([
"esri/geometry"
], function (geometry) {
"esri/geometry/Polygon"
], function (Polygon) {
"use strict";
var TilingScheme = function (layer) {
this.tileInfo = layer.tileInfo;
@ -24,14 +24,14 @@ define([
var x2 = this.tileInfo.origin.x + (col2 * this.tileInfo.cols * this.tileInfo.lods[level].resolution);
var y2 = this.tileInfo.origin.y - (row2 * this.tileInfo.rows * this.tileInfo.lods[level].resolution);
var polygon = new geometry.Polygon(this.tileInfo.spatialReference);
var polygon = new Polygon(this.tileInfo.spatialReference);
polygon.addRing([
[x1, y1], // clockwise
[x2, y1],
[x2, y2],
[x1, y2],
[x1, y1]
]);
[x1, y1], // clockwise
[x2, y1],
[x2, y2],
[x1, y2],
[x1, y1]
]);
return polygon;
},

View File

@ -26,13 +26,18 @@ module.exports = function(grunt) {
"#",
"# ArcGIS API for JavaScript files",
"<%= pkg.optimizedApiURL %>/dojo/dojo.js",
"<%= pkg.optimizedApiURL %>/dojo/nls/dojo_en-us.js",
"<%= pkg.optimizedApiURL %>/dojo/selector/acme.js",
"#",
"<%= pkg.arcGISBaseURL %>/js/esri/dijit/images/popup-sprite.png",
"<%= pkg.arcGISBaseURL %>/js/esri/dijit/images/attribute_inspector_sprite.png",
"<%= pkg.arcGISBaseURL %>/js/dojo/dojox/gfx/svg.js",
"<%= pkg.arcGISBaseURL %>/js/dojo/dojo/resources/blank.gif",
"<%= pkg.arcGISBaseURL %>/js/esri/dijit/images/ajax-loader.gif",
"<%= pkg.arcGISBaseURL %>/js/esri/images/map/logo-sm.png",
"<%= pkg.arcGISBaseURL %>/js/esri/images/map/logo-med.png",
"<%= pkg.arcGISBaseURL %>/js/esri/css/esri.css",
"<%= pkg.arcGISBaseURL %>/js/dojo/dijit/themes/claro/claro.css",
"<%= pkg.arcGISBaseURL %>/js/esri/nls/jsapi_en-us.js",
"#",
"//services.arcgisonline.com/ArcGIS/rest/info?f=json",
@ -42,7 +47,7 @@ module.exports = function(grunt) {
"# required for web maps",
"<%= pkg.arcGISBaseURL %>/js/esri/dijit/images/ajax-loader.gif",
"#",
"# required custom libs",
"# required local html",
"# /xyz/style.css",
"# /img/1.png"],
network: [
@ -56,18 +61,19 @@ module.exports = function(grunt) {
},
src: [
"*.html",
/*"js/*.min.js",*/
"samples/images/*.png",
"vendor/IndexedDBShim/dist/*.min.js",
"vendor/offline/offline.min.js",
"lib/tiles/*.js",
"lib/tiles/*.png",
"lib/tiles/*.psd",
"utils/*.js"
/*
"images/*",
"css/*.css"
*/
"../samples/images/*.png",
"../samples/css/*.css",
"../vendor/IndexedDBShim/dist/*.js",
"../vendor/offline/offline.min.js",
"../lib/tiles/*.js",
"../lib/tiles/*.png",
"../lib/tiles/*.psd",
"../lib/edit/*.js",
"../utils/*.js"
/*
"images/*",
"css/*.css"
*/
],
dest: "<%= pkg.manifestName %>"
}

View File

@ -1,6 +1,6 @@
CACHE MANIFEST
# This manifest was generated by grunt-manifest HTML5 Cache Manifest Generator
# Time: Mon May 19 2014 13:37:55 GMT-0600 (MDT)
# Time: Wed Jun 11 2014 13:42:14 GMT-0600 (MDT)
CACHE:
# manifest-generator, version: 0.0.1
@ -9,14 +9,19 @@ CACHE:
appcache-features.html
#
# ArcGIS API for JavaScript files
http://js.arcgis.com/o/agup_hack4co/appcache2/dojo/dojo.js
http://js.arcgis.com/o/agup_hack4co/appcacheFeatures/dojo/dojo.js
http://js.arcgis.com/o/agup_hack4co/appcacheFeatures/dojo/nls/dojo_en-us.js
http://js.arcgis.com/o/agup_hack4co/appcacheFeatures/dojo/selector/acme.js
#
http://js.arcgis.com/3.9/js/esri/dijit/images/popup-sprite.png
http://js.arcgis.com/3.9/js/esri/dijit/images/attribute_inspector_sprite.png
http://js.arcgis.com/3.9/js/dojo/dojox/gfx/svg.js
http://js.arcgis.com/3.9/js/dojo/dojo/resources/blank.gif
http://js.arcgis.com/3.9/js/esri/dijit/images/ajax-loader.gif
http://js.arcgis.com/3.9/js/esri/images/map/logo-sm.png
http://js.arcgis.com/3.9/js/esri/images/map/logo-med.png
http://js.arcgis.com/3.9/js/esri/css/esri.css
http://js.arcgis.com/3.9/js/dojo/dijit/themes/claro/claro.css
http://js.arcgis.com/3.9/js/esri/nls/jsapi_en-us.js
#
//services.arcgisonline.com/ArcGIS/rest/info?f=json
@ -26,7 +31,7 @@ http://js.arcgis.com/3.9/js/esri/nls/jsapi_en-us.js
# required for web maps
http://js.arcgis.com/3.9/js/esri/dijit/images/ajax-loader.gif
#
# required custom libs
# required local html
# /xyz/style.css
# /img/1.png
appcache-features.html
@ -36,6 +41,27 @@ attachments-editor.html
military-offline.html
service-inspector.html
tiles-indexed-db.html
tpk-layer.html
../samples/images/blue-pin.png
../samples/images/red-pin.png
../samples/css/modular-popup.css
../vendor/IndexedDBShim/dist/IndexedDBShim.js
../vendor/IndexedDBShim/dist/IndexedDBShim.min.js
../vendor/offline/offline.min.js
../lib/tiles/FileSaver.js
../lib/tiles/OfflineTilesEnablerLayer.js
../lib/tiles/TilesStore.js
../lib/tiles/base64utils.js
../lib/tiles/offlineTilesEnabler.js
../lib/tiles/tilingScheme.js
../lib/tiles/notile.png
../lib/tiles/notile.psd
../lib/edit/attachmentsStore.js
../lib/edit/editsStore.js
../lib/edit/offlineFeaturesManager.js
../lib/edit/restartOfflineFeaturesManager.js
../utils/appCacheManager.js
../utils/debouncer.js
NETWORK:
*

View File

@ -1,31 +1,17 @@
<!DOCTYPE html>
<!--<html>-->
<html manifest="appcache-features.appcache">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no"/>
<title>Cache Features Sample</title>
<!--
This sample demonstrates using an application manifest to store tiles locally.
The use cases for using this sample are to ensure you can reload and restart you application
one it is offline.
Use the included Grunt task to help generate the manifest file. There is manual
work involved in determining which files need to go into the manifest. The included manifest
file should work with this sample to give you an idea of what goes into the manifest.
A few things to know about manifest files:
- You cannot load an online url that has a redirect.
- If there is an error encountered, the manifest file will stop loading
- The /utils/appCacheManager.js library will auto detect if the manifest changed and
ask if you want to reload the application.
-->
<!--The viewport meta tag is used to improve the presentation and behavior of the samples
on iOS devices-->
<meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no">
<title>AppCache Tiles and Features</title>
<link rel="stylesheet" href="../samples/css/modular-popup.css">
<link rel="stylesheet" href="http://js.arcgis.com/3.9/js/dojo/dijit/themes/claro/claro.css">
<link rel="stylesheet" href="http://js.arcgis.com/3.9/js/esri/css/esri.css">
<style>
html, body, #map {
height: 100%;
@ -38,61 +24,16 @@
overflow: hidden;
font-family: "Trebuchet MS";
}
.dj_ie .infowindow .window .top .right .user .content { position: relative; }
.dj_ie .simpleInfoWindow .content {position: relative;}
.esriPopup .contentPane {
color: white;
background-color: black;
height: 110px;
}
.dijitTextBox{ color: #000000; background-color: white;}
/* This is an editable text field */
.dijitSelect input, .dijitTextBox input {
color: black;
/*background-color: #ffffff !important;*/
}
.dijitTextBoxDisabled{
background-color: lightgray;
}
.esriAttributeInspector {height:100px;}
/* this is a non-editable text field */
.esriAttributeInspector .dijitTextBoxDisabled input {
color: #000000;
background-color: lightgray;
}
.dijitButtonNode{
width: 60px;
padding: 2px;
border-radius: 3px;
color: yellow;
}
.esriAttributeInspector .atiLayerName {display:none;}
.saveButton {
margin:0px;
width:16px;
height:16px;
}
.msgTextArea{
width:16px;
height:16px;
margin-left: 60px;
width: auto;
background-color: #000000;
border-color: #000000;
color: white;
font-family: "Trebuchet MS";
}
.basic-btn{
background-color: #000000;
border-color: #ffffff 1px;
color: #ffffff;
padding: 8px;
position: relative; float: left;
#map {
z-index: 1;
position: absolute;
top: 50px;
left: 0;
}
#button-div1{
position: relative;
position: absolute;
top: 0;
z-index: 2;
background: #000000;
color: #ffffff;
width: 100%;
@ -103,15 +44,55 @@
position: relative; float: right;
}
#img-offline-indicator{
padding: 8px;
/*padding: 8px;*/
position: relative; float: right;
position: relative; float: right;
}
#popup {
height: 100%;
width: 100%;
z-index: 98;
font-size: x-large;
position: absolute;
top: -9999px !important;
left: -9999px !important;
}
#popup-body {
position: relative;
top: 10%;
left: 10%;
z-index: 100;
height: 80%;
width: 80%;
border-radius: 10px;
background-color: black;
display: table;
opacity: 0.7;
}
.basic-btn{
background-color: #000000;
border-color: #ffffff 1px;
color: #ffffff;
padding: 8px;
position: relative; float: left;
}
#pending-edits{
padding: 8px;
position: relative; float: right;
}
@media (max-width: 500px) {
#pending-edits {
font-size: small;
}
}
@media (max-width: 450px) {
#pending-edits {
font-size: x-small;
}
}
</style>
<script src="../vendor/offline/offline.min.js"></script>
<script>
Offline.options = {
@ -130,159 +111,263 @@
paths: {
edit: locationPath + "/../lib/edit",
vendor: locationPath + "/../vendor",
utils: locationPath + "/../utils"
utils: locationPath + "/../utils",
tiles: locationPath + "/../lib/tiles"
}
}
</script>
<script src="http://js.arcgis.com/3.9/"></script>
<script>
<!-- Required when using the offline-editor-js library with Safari -->
<script src="../vendor/IndexedDBShim/dist/IndexedDBShim.js"></script>
<script src="http://js.arcgis.com/o/agup_hack4co/appcacheFeatures/dojo/dojo.js" data-dojo-config="async: true"></script>
<!-- Use this tag below if you are hosting your ArcGIS API for JavaScript files locally -->
<!--<script src="libs/dojo/dojo/dojo.js" data-dojo-config="async: true"></script>-->
</head>
<body>
<div id="button-div1">
<button class="basic-btn" id="btn-get-tiles">1. Download Tiles</button>
<button class="basic-btn" data-dojo-type="dijit/form/ToggleButton" id="btn-online-offline">2. Go Offline</button>
<div id="right-div">
<img id="img-offline-indicator" src="../samples/images/blue-pin.png"/>
<div id="pending-edits">Pending edits: 0</div>
</div>
</div>
require([
"esri/map",
"esri/tasks/query",
"esri/layers/FeatureLayer",
"edit/offlineFeaturesManager",
"edit/editsStore",
"utils/appCacheManager",
"esri/dijit/AttributeInspector",
"dojo/dom-construct",
"dojo/on",
"dojo/dom",
"dijit/form/Button",
"dijit/form/SimpleTextarea",
"dojo/domReady!"],
function(Map,Query,FeatureLayer,OfflineFeaturesManager,editsStore,AppCacheManager,AttributeInspector,domConstruct,on,dom,Button,SimpleTextArea) {
<div id="popup">
<div id="popup-body">
<div id="row1" class="mod-popup-table-row">
<div class="mod-popup-label-top-left">ID</div>
<div class="mod-popup-input">
<input id="stop-main-id" type="text" disabled class="mod-popup-stop-input-disabled" value="test"/>
</div>
</div>
<div id="row2" class="mod-popup-table-row">
<div class="mod-popup-label">Bustop ID</div>
<div class="mod-popup-input">
<input id="stop-id" disabled class="mod-popup-stop-input-disabled" value="test"/>
</div>
</div>
<div id="row3" class="mod-popup-table-row">
<div class="mod-popup-label">Routes</div>
<div class="mod-popup-input">
<input id="stop-routes" class="mod-popup-stop-input" value="test"/>
</div>
</div>
<div id="row4" class="mod-popup-table-row">
<div class="mod-popup-label">Stopnames</div>
<div class="mod-popup-input">
<input id="stop-names" class="mod-popup-stop-input" value="test"/>
</div>
</div>
<div id="row5" class="mod-popup-table-row">
<div class="mod-popup-button-div-bottom-left">
<button id="mod-popup-close-btn" class="mod-popup-button-cancel" >Close</button>
</div>
<div class="mod-popup-button-div">
<button id="mod-popup-save-btn" class="mod-popup-button" style="margin-right: 10px;">Save</button>
<button id="mod-popup-delete-btn" disabled class="mod-popup-button" style="background-color: grey; text-decoration:line-through;">Delete</button>
</div>
</div>
</div>
<div id="modal-background" class="mod-popup-modal-background"></div>
</div>
var textTimer;
<div id="map"></div>
<script>
require(["esri/map","esri/layers/FeatureLayer","tiles/OfflineTilesEnablerLayer",
"edit/restartOfflineFeaturesManager", "utils/appCacheManager",
"esri/renderers/SimpleRenderer","esri/symbols/SimpleMarkerSymbol","esri/Color","esri/tasks/query",
"edit/offlineFeaturesManager", "edit/editsStore",
"dojo/on",
"dojo/domReady!"],
function(Map,FeatureLayer,OfflineTilesEnablerLayer,restartOfflineFeaturesMgr,AppCacheManager,
SimpleRenderer,SimpleMarkerSymbol,Color,Query,
OfflineFeaturesManager,editsStore,on) {
initAppCacheManager();
var _isOnline = true;
var defaultSymbol;
// Variables for editing handling
var currentFeature;
var busStopFeatureLayer;
var offlineFeaturesManager;
var map,busStopsFeatureLayer,currentFeature;
var imgOfflineIndicator,btnOnlineOffline;
var modPopup = document.getElementById("popup");
var pendingEdits = document.getElementById("pending-edits");
var imgOfflineIndicator = document.getElementById("img-offline-indicator");
// Variables for handling tiles
var tileLayer;
var _currentExtent = null;
var _wantToCancel;
var globalState = {};
var btnGetTiles = document.getElementById("btn-get-tiles");
// Important settings for determining which tile layers gets stored for offline use.
var minZoomAdjust = -1, maxZoomAdjust = 1, resetZoom = 18;
var EXTENT_BUFFER = 0; //buffers the map extent in meters
// Misc.
var appCacheManager;
var btnOnlineOffline = document.getElementById("btn-online-offline");
// Variables for modal popup
var modPopup = document.getElementById("popup");
var closeBtn = document.getElementById("mod-popup-close-btn");
var saveBtn = document.getElementById("mod-popup-save-btn");
var deleteBtn = document.getElementById("mod-popup-delete-btn");
var stopMainID = document.getElementById("stop-main-id");
var stopID = document.getElementById("stop-id");
var stopRoutes = document.getElementById("stop-routes");
var stopNames = document.getElementById("stop-names");
// Symbols and images
var redPinPath = "../samples/images/red-pin.png";
var bluePinPath = "../samples/images/blue-pin.png"
var redPinImage = new Image().src = redPinPath;
var bluePinPath = "../samples/images/blue-pin.png";
initOffline();
map = new Map("map", {
basemap: "topo",
center: [-104.98,39.74], // long, lat
zoom: 10,
sliderStyle: "small"
});
busStopsFeatureLayer = new FeatureLayer("http://services.arcgis.com/IZtlGBUe4KTzLOl4/arcgis/rest/services/BPX_RTD_BusStops2/FeatureServer/0",{
mode: FeatureLayer.MODE_SNAPSHOT,
outFields: ["OBJECTID","BSID","ROUTES","STOPNAME"]
})
map.on("load",function(evt){
initAppCacheManager();
})
map.on("layers-add-result",initEditing);
map.addLayers([busStopsFeatureLayer]);
function initAppCacheManager(){
appCacheManager = new AppCacheManager(true,true);
appCacheManager.on(appCacheManager.CACHE_EVENT,cacheEventHandler);
appCacheManager.on(appCacheManager.CACHE_ERROR,cacheErrorHandler);
// Modify symbol size based on screen size.
// Bigger screens get smaller symbols. Smaller screens get larger symbols.
if (document.documentElement.clientHeight > 768 || document.documentElement.clientWidth > 1024) {
defaultSymbol= new SimpleMarkerSymbol().setStyle(
SimpleMarkerSymbol.STYLE_DIAMOND).setColor(
new Color([255,0,0,0.5])).setSize(20); // scripts
}
else{
defaultSymbol= new SimpleMarkerSymbol().setStyle(
SimpleMarkerSymbol.STYLE_DIAMOND).setColor(
new Color([255,0,0,0.5])).setSize(35);
}
function initOffline(){
/**
* There have been a few bugs in the offline detection library (offline.min.js)
* This is a utility check to 100% validate if the application is online or
* offline prior to launching any map functionality.
*/
verifyOnline(function(result){ console.log("VERIFY ONLINE " + result)
result == true ? _isOnline = true : _isOnline = false;
startMap();
})
function startMap(){
//Make sure map shows up after a browser refresh
Offline.check();
Offline.state === 'up' ? resetZoom = 18 : resetZoom = 17;
tileLayer = new OfflineTilesEnablerLayer("http://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer",function(evt){
console.log("Tile Layer Loaded.");
},_isOnline);
var map = new Map("map",{
center: [-104.98,39.74], // long, lat
zoom: 8,
sliderStyle: "small"
});
map.addLayer(tileLayer);
busStopFeatureLayer = new FeatureLayer("http://services.arcgis.com/IZtlGBUe4KTzLOl4/arcgis/rest/services/BPX_RTD_BusStops2/FeatureServer/0", {
mode: FeatureLayer.MODE_SNAPSHOT,
outFields: ["OBJECTID","BSID","ROUTES","STOPNAME"]
});
//Set the graphics to red boxes to make it easy to click on them
//on a mobile device.
busStopFeatureLayer.setRenderer(new SimpleRenderer(defaultSymbol));
busStopFeatureLayer.on("update-end",function(evt){
on(btnGetTiles,"click",downloadTiles);
on(btnOnlineOffline, 'click', goOnlineOffline);
initOfflineFeaturesMgr();
setFeatureLayerClickHandler();
setModalPopupClickListeners();
var features = evt.target.graphics;
// Convert existing feature graphics into JSON.
// These are then stored in localStorage.
// If you want you can store multiple feature layers.
// Just be aware of the localStorage limitations.
restartOfflineFeaturesMgr.convertGraphicLayerToJSON(features,evt,function(features,layerDef){
if(typeof(Storage) !== "undefined") {
localStorage.offlineLayerDef = layerDef;
localStorage.offlineFeature = features;
console.log("Done pushing layerDef and features to localStorage.")
} else {
alert("The offline library is not supported on this browser.")
}
})
});
map.on("zoom-end",function(evt){
_currentExtent = evt.extent;
});
map.on("pan-end",function(evt){
_currentExtent = evt.extent;
});
map.on("load",function(evt){
_currentExtent = evt.map.extent;
updateOfflineUsage();
if(_isOnline == false){
var featureLayer = JSON.parse(localStorage.offlineLayerDef);
var featuresArray = JSON.parse(localStorage.offlineFeature);
var geometryType = "esriGeometryPoint";
restartOfflineFeaturesMgr.getFeatureDefinition(featureLayer,featuresArray,geometryType,function(featureDef){
busStopFeatureLayer = new FeatureLayer(featureDef,{
mode: FeatureLayer.MODE_SNAPSHOT,
outFields: ["OBJECTID","BSID","ROUTES","STOPNAME"]
});
// Set the graphic symbols to red diamond to make it easy to click on them
// on a mobile device.
busStopFeatureLayer.setRenderer(new SimpleRenderer(defaultSymbol));
var mapListen = map.on("update-end",function(evt){
console.log("Feature has been added back to the map while offline.");
on(btnGetTiles,"click",downloadTiles);
on(btnOnlineOffline, 'click', goOnlineOffline);
initOfflineFeaturesMgr();
setFeatureLayerClickHandler();
setModalPopupClickListeners();
mapListen.remove();
})
map.addLayer(busStopFeatureLayer);
});
}
});
map.addLayer(busStopFeatureLayer);
}
/**
* **********************************************
* FEATURE LAYER MANAGEMENT CODE
* **********************************************
*/
function initOfflineFeaturesMgr(){
offlineFeaturesManager = new OfflineFeaturesManager();
offlineFeaturesManager.on(offlineFeaturesManager.events.EDITS_ENQUEUED, updateStatus);
offlineFeaturesManager.on(offlineFeaturesManager.events.EDITS_SENT, updateStatus);
offlineFeaturesManager.on(offlineFeaturesManager.events.ALL_EDITS_SENT, updateStatus);
imgOfflineIndicator = document.getElementById("img-offline-indicator");
imgOfflineIndicator.offlineColor = "blue";
offlineFeaturesManager.extend(busStopFeatureLayer);
console.log("offlineFeaturesManager initialized.");
Offline.check();
Offline.on('up', goOnline);
Offline.on('down', goOffline);
on(dom.byId('btn-online-offline'), 'click', goOnlineOffline);
}
function initEditing(evt){
offlineFeaturesManager.extend(busStopsFeatureLayer);
map.infoWindow.on("hide", function() {
busStopsFeatureLayer.clearSelection();
});
btnOnlineOffline = document.getElementById("btn-online-offline");
var layerInfos = [{
'featureLayer': busStopsFeatureLayer,
'showAttachments': false,
'isEditable': true,
'showDeleteButton': false,
'fieldInfos': [
{'fieldName': 'OBJECTID', 'isEditable':false, 'label':'ID:'},
{'fieldName': 'BSID', 'isEditable':false, 'label':'Bus stop ID:'},
{'fieldName': 'ROUTES', 'isEditable':false,'label':'Routes:'},
{'fieldName': 'STOPNAME', 'isEditable':true, 'label':'Stop name:'}
]
}];
var attInspector = new AttributeInspector({
layerInfos:layerInfos
}, domConstruct.create("div"));
//add a save button next to the delete button
var saveButton = new Button({ label: "Save", "class": "saveButton"});
domConstruct.place(saveButton.domNode, attInspector.deleteBtn.domNode, "after");
var textArea = new SimpleTextArea({name:"msgTextArea",rows:"1",class:"msgTextArea"})
domConstruct.place(textArea.domNode, saveButton.domNode, "after");
saveButton.on("click", function(){
clearTimeout(textTimer);
busStopsFeatureLayer.applyEdits(null, [currentFeature], null,
function(response){
textArea.set("value" , "Item successfully saved.");
textTimer = setTimeout(function(){
textArea.set("value" , " ");
},10000);
},function(err){
alert("There was error applying an edit: " + JSON.stringify(err));
}
);
});
attInspector.on("attribute-change", function(evt) {
//store the updates to apply when the save button is clicked
currentFeature.attributes[evt.fieldName] = evt.fieldValue;
console.log("CHANGE " + evt.fieldValue)
});
attInspector.on("next", function(evt) {
currentFeature = evt.feature;
console.log("Next " + currentFeature.attributes.objectid);
});
busStopsFeatureLayer.on("click", function(evt) {
currentFeature = evt.graphic
var query = new Query();
query.objectIds = [evt.graphic.attributes.OBJECTID];
busStopsFeatureLayer.selectFeatures(query,FeatureLayer.SELECTION_NEW,
function(evt){
console.log("Success: " + JSON.stringify(evt[0].attributes))
},function(err){
console.log("ERROR " + JSON.stringify(err));
});
map.infoWindow.setTitle(currentFeature.attributes.STOPNAME);
map.infoWindow.show(evt.screenPoint,map.getInfoWindowAnchor(evt.screenPoint));
}.bind(this));
map.infoWindow.setContent(attInspector.domNode);
map.infoWindow.resize(350, 270);
}
function updateStatus(){
@ -297,27 +382,189 @@
}
}
function goOnline(){
/**
* Display modal popup when someone clicks on a feature
*/
function setFeatureLayerClickHandler(){
busStopFeatureLayer.on("click", function(evt) {
currentFeature = evt.graphic
var query = new Query();
query.objectIds = [evt.graphic.attributes.OBJECTID];
busStopFeatureLayer.selectFeatures(query,FeatureLayer.SELECTION_NEW,
function(evt){
console.log("Success: " + JSON.stringify(evt[0].attributes));
showModalPopup(evt[0]);
var atts = evt[0].attributes
stopID.value = atts.OBJECTID;
stopMainID.value = atts.BSID;
stopNames.value = atts.STOPNAME;
stopRoutes.value = atts.ROUTES;
},function(err){
console.log("ERROR " + JSON.stringify(err));
});
}.bind(this));
}
function setModalPopupClickListeners(){
closeBtn.onclick = function(evt){
hideModalPopup();
}
saveBtn.onclick = function(evt){
modPopup.graphic.attributes.ROUTES = stopRoutes.value;
modPopup.graphic.attributes.STOPNAME = stopNames.value;
busStopFeatureLayer.applyEdits(null,[modPopup.graphic],null,function(result){
console.log("Successfully saved changes to: " + modPopup.graphic.attributes.STOPNAME);
hideModalPopup();
},
function(err){
alert("There was a problem while trying to save: " + modPopup.graphic.attributes.STOPNAME);
})
}
deleteBtn.onclick = function(evt){
busStopFeatureLayer.applyEdits(null,null,[modPopup.graphic],function(result){
console.log("Successfully deleted: " + modPopup.graphic.attributes.STOPNAME);
hideModalPopup();
},
function(err){
alert("There was a problem while trying to delete: " + modPopup.graphic.attributes.STOPNAME);
})
}
}
function showModalPopup(graphic){
modPopup.style.position = "static";
modPopup.graphic = graphic; // assign graphic to modPopup as a property.
}
function hideModalPopup(){
modPopup.style.position = "absolute";
}
/**
* ************************************
* TILE MANAGEMENT CODE
* ************************************
*/
/**
* Manually starts the process to download and store tiles
* in the local database
*/
function downloadTiles(){
tileLayer.deleteAllTiles(function(success,err){
if(success == false){
alert("There was a problem deleting the tile cache");
}
else{
console.log("success deleting tile cache");
if( globalState.downloadState == 'downloading')
{
console.log("cancel!");
_wantToCancel = true;
btnGetTiles.innerHTML = "cancelling..";
}
else
{
var zoom = getMinMaxZoom();
var extent = tileLayer.getExtentBuffer(EXTENT_BUFFER,_currentExtent);
_wantToCancel = false;
tileLayer.prepareForOffline(zoom.min, zoom.max, extent, reportProgress.bind(this));
globalState.downloadState = 'downloading';
}
}
}.bind(this))
}
/**
* Reports the process while downloading tiles.
*/
function reportProgress(progress)
{
console.log("downloading tiles...");
if(progress.hasOwnProperty("countNow")){
var percent = Math.floor(progress.countNow / progress.countMax * 100);
btnGetTiles.innerHTML = 'Saving to phone ' + percent + "% - Tap to Cancel";
}
if( progress.finishedDownloading )
{
btnGetTiles.innerHTML = "Saving to phone 100% - Tap to Cancel";
updateOfflineUsage();
if( progress.cancelRequested )
{
globalState.downloadState = 'cancelled';
alert("Tile download was cancelled");
}
else
{
globalState.downloadState = 'downloaded';
alert("Tile download complete");
}
btnGetTiles.innerHTML = '1. Download Tiles';
}
return _wantToCancel; //determines if a cancel request has been issued
}
/**
* Utility function to validate min and max zoom settings of the map
*/
function getMinMaxZoom(){
var zoom = {};
var min = tileLayer.getLevel() + minZoomAdjust;
var max = tileLayer.getLevel() + maxZoomAdjust;
var mMaxZoom;
tileLayer.getMaxZoom(function(result){
mMaxZoom = result;
});
var mMinZoom;
tileLayer.getMinZoom(function(result){
mMinZoom = result;
});
zoom.max = Math.min(mMaxZoom, max); //prevent errors by setting the tile layer floor
zoom.min = Math.max(mMinZoom, min); //prevent errors by setting the tile layer ceiling
return zoom;
}
/**
* ************************************
* MISC. CODE
* ************************************
*/
function goOnline(){
console.log("Going online...");
offlineFeaturesManager.goOnline(function(success,error){
if(error === undefined){
btnOnlineOffline.innerHTML = "Go Offline";
btnOnlineOffline.innerHTML = "2. Go Offline";
imgOfflineIndicator.src = bluePinPath;
imgOfflineIndicator.offlineColor = "blue";
console.log("Online.");
console.log("offlineFeatureManager is online.");
}
else{
alert("There was a problem syncing offline edits: " + JSON.stringify(error));
}
});
updateOfflineUsage();
if(typeof baseMapLayer != "undefined") baseMapLayer.goOnline();
}
function goOffline(){
btnOnlineOffline.innerHTML = "Go Online";
console.log("Going offline...");
btnOnlineOffline.innerHTML = "2. Go Online";
imgOfflineIndicator.src = redPinPath;
imgOfflineIndicator.offlineColor = "red";
offlineFeaturesManager.goOffline();
if(typeof tileLayer != "undefined") tileLayer.goOffline();
}
function goOnlineOffline(){
@ -329,28 +576,70 @@
}
}
/**
* For internal use - detecting amount of storage used and number of tiles stored.
*/
function updateOfflineUsage()
{
tileLayer.offline.store.usedSpace(function(result,err){
if(result != null){
console.log( "DB Tile count: " + result.tileCount + ", DB Bytes: " + result.sizeBytes);
}
else{
console.log("DB Tile count: " + count + ", DB Bytes: Error");
}
})
}
function initAppCacheManager(){
appCacheManager = new AppCacheManager(true,true);
appCacheManager.on(appCacheManager.CACHE_EVENT,cacheEventHandler);
appCacheManager.on(appCacheManager.CACHE_ERROR,cacheErrorHandler);
appCacheManager.on(appCacheManager.CACHE_LOADED,cacheLoadedHandler);
}
function cacheLoadedHandler(evt){
if(evt == appCacheManager.CACHE_LOADED) console.log("Application cache successfully loaded!")
}
function cacheEventHandler(evt){
console.log("CACHE EVENT: " + JSON.stringify(evt));
if(evt.hasOwnProperty("total")) console.log("CACHE EVENT loaded #:" + evt.loaded + ", out of " + evt.total);
}
function cacheErrorHandler(evt){
console.log("CACHE ERROR: " + JSON.stringify(evt));
alert("There was a problem loading the cache. Try reloading the app. ")
}
}
);
</script>
</head>
/**
* Attempts an http request to verify if app is online or offline.
* Use this in conjunction with the offline checker library: offline.min.js
* @param callback
*/
function verifyOnline(callback){
var req = new XMLHttpRequest();
req.open("GET", "images/blue-pin.png?" + (Math.floor(Math.random() * 1000000000)), true);
req.onload = function()
{
if( req.status === 200 && req.responseText !== "")
{
callback(true);
}
else
{
console.log("verifyOffline failed");
callback(false);
}
};
req.onerror = function(e)
{
console.log("verifyOffline failed: " + e);
callback(false);
};
req.send(null);
}
});
</script>
<body>
<div id="button-div1">
<button class="basic-btn" data-dojo-type="dijit/form/ToggleButton" id="btn-online-offline">Go Offline</button>
<div id="right-div">
<img id="img-offline-indicator" src="../samples/images/blue-pin.png"/>
<div id="pending-edits">Pending edits: 0</div>
</div>
</div>
<div id="test"></div>
<div id="map"></div>
</body>
</html>

View File

@ -1,6 +1,6 @@
CACHE MANIFEST
# This manifest was generated by grunt-manifest HTML5 Cache Manifest Generator
# Time: Mon May 19 2014 13:19:05 GMT-0600 (MDT)
# Time: Wed Jun 04 2014 17:49:41 GMT-0600 (MDT)
CACHE:
# manifest-generator, version: 0.0.1
@ -10,7 +10,10 @@ appcache-tiles.html
#
# ArcGIS API for JavaScript files
http://js.arcgis.com/o/agup_hack4co/appcache2/dojo/dojo.js
http://js.arcgis.com/o/agup_hack4co/appcache2/dojo/nls/dojo_en-us.js
http://js.arcgis.com/o/agup_hack4co/appcache2/dojo/selector/acme.js
#
http://js.arcgis.com/3.9/js/esri/dijit/images/popup-sprite.png
http://js.arcgis.com/3.9/js/dojo/dojox/gfx/svg.js
http://js.arcgis.com/3.9/js/dojo/dojo/resources/blank.gif
http://js.arcgis.com/3.9/js/esri/dijit/images/ajax-loader.gif
@ -26,7 +29,7 @@ http://js.arcgis.com/3.9/js/esri/nls/jsapi_en-us.js
# required for web maps
http://js.arcgis.com/3.9/js/esri/dijit/images/ajax-loader.gif
#
# required custom libs
# required local html
# /xyz/style.css
# /img/1.png
appcache-features.html
@ -36,6 +39,20 @@ attachments-editor.html
military-offline.html
service-inspector.html
tiles-indexed-db.html
tpk-layer.html
../samples/images/blue-pin.png
../samples/images/red-pin.png
../vendor/IndexedDBShim/dist/IndexedDBShim.min.js
../vendor/offline/offline.min.js
../lib/tiles/FileSaver.js
../lib/tiles/TilesStore.js
../lib/tiles/base64utils.js
../lib/tiles/offlineTilesEnabler.js
../lib/tiles/tilingScheme.js
../lib/tiles/notile.png
../lib/tiles/notile.psd
../utils/appCacheManager.js
../utils/debouncer.js
NETWORK:
*

View File

@ -1,343 +1,377 @@
<!DOCTYPE html>
<html manifest="appcache-tiles.appcache">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no"/>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no"/>
<!--
<!--
This sample demonstrates using an application manifest to store tiles locally.
This sample demonstrates using an application manifest to store tiles locally.
The use cases for using this sample are to ensure you can reload and restart you application
one it is offline.
The use cases for using this sample are to ensure you can reload and restart you application
one it is offline.
Use the included Grunt task to help generate the manifest file. There is manual
work involved in determining which files need to go into the manifest. The included manifest
file should work with this sample to give you an idea of what goes into the manifest.
It is strongly recommended that you use your own optimized build of the ArcGIS API for JavaScript
using the Web Optimizer: http://jso.arcgis.com/. You can reference the CDN or host it on your
own web server.
A few things to know about manifest files:
- You cannot load an online url that has a redirect.
- If there is an error encountered, the manifest file will stop loading
- The /utils/appCacheManager.js library will auto detect if the manifest changed and
ask if you want to reload the application.
Use the included Grunt task to help generate the manifest file. There is manual
work involved in determining which files need to go into the manifest. The included manifest
file should work with this sample to give you an idea of what goes into the manifest.
-->
A few things to know about manifest files:
- You cannot load an online url that has a redirect.
- If there is an error encountered, the manifest file will stop loading
- The /utils/appCacheManager.js library will auto detect if the manifest changed and
ask if you want to reload the application.
<title>Cache Tiles</title>
<link rel="stylesheet" href="http://js.arcgis.com/3.9/js/esri/css/esri.css">
<style>
html, body, #map {
height: 100%;
width: 100%;
margin: 0;
padding: 0;
-->
<title>Cache Tiles</title>
<link rel="stylesheet" href="http://js.arcgis.com/3.9/js/esri/css/esri.css">
<style>
html, body, #map {
height: 100%;
width: 100%;
margin: 0;
padding: 0;
}
body {
background-color: #FFF;
overflow: hidden;
font-family: "Trebuchet MS";
}
#button-div1{
position: relative;
background: #000000;
color: #ffffff;
width: 100%;
height: 50px;
}
.basic-btn{
background-color: #000000;
border-color: #ffffff 1px;
color: #ffffff;
padding: 8px;
position: relative; float: left;
}
#img-offline-indicator{
padding: 8px;
position: relative; float: right;
}
#tile-info{
background-color: #000000;
color: white;
padding: 8px;
position: relative; float: right;
}
</style>
<script>
var locationPath = location.pathname.replace(/\/[^/]+$/, "");
var dojoConfig = {
paths: {
tiles: locationPath + "/../lib/tiles",
vendor: locationPath + "/../vendor",
utils: locationPath + "/../utils"
}
body {
background-color: #FFF;
overflow: hidden;
font-family: "Trebuchet MS";
}
#button-div1{
position: relative;
background: #000000;
color: #ffffff;
width: 100%;
height: 50px;
}
.basic-btn{
background-color: #000000;
border-color: #ffffff 1px;
color: #ffffff;
padding: 8px;
position: relative; float: left;
}
#img-offline-indicator{
padding: 8px;
position: relative; float: right;
}
#tile-info{
background-color: #000000;
color: white;
padding: 8px;
position: relative; float: right;
}
</style>
<script>
var locationPath = location.pathname.replace(/\/[^/]+$/, "");
var dojoConfig = {
paths: {
tiles: locationPath + "/../lib/tiles",
vendor: locationPath + "/../vendor",
utils: locationPath + "/../utils"
}
}
</script>
<!-- This is a custom build of the ArcGIS API for JavaScript using the new Web Optimizer Tool -->
<script src="http://js.arcgis.com/3.9/" data-dojo-config="async: true"></script>
<!-- Use this tag below if you are hosting your ArcGIS API for JavaScript files locally -->
<!--<script src="libs/dojo/dojo/dojo.js" data-dojo-config="async: true"></script>-->
<script src="../vendor/IndexedDBShim/dist/IndexedDBShim.min.js"></script>
<script src="../vendor/offline/offline.min.js"></script>
<script>
Offline.options = {
checks: {
image: {
url: function() {
return 'http://esri.github.io/offline-editor-js/tiny-image.png?_=' + (Math.floor(Math.random() * 1000000000));
}
},
active: 'image'
}
}
</script>
<script>
var map,offlineTileEnabler,baseMapLayer,zoom = 18;
var globalState = {};
var _wantToCancel;
var minZoomAdjust = -1, maxZoomAdjust = 1, mMinZoom, mMaxZoom;
var imgOfflineIndicator,btnGetTiles,btnOnlineOffline,btnZoom;
var tiles,appCacheManager,tileInfo;
var redPinPath = "../samples/images/red-pin.png";
var bluePinPath = "../samples/images/blue-pin.png"
var EXTENT_BUFFER = 0; //buffers the map extent in meters
require(["esri/map","utils/appCacheManager","tiles/offlineTilesEnabler","dojo/on","dojo/domReady!"],
function(Map,AppCacheManager,OfflineTileEnabler,on) {
imgOfflineIndicator = document.getElementById("img-offline-indicator");
imgOfflineIndicator.offlineColor = "blue";
Offline.check();
Offline.on('up down', updateState );
//Make sure map shows up after a browser refresh
Offline.state === 'up' ? zoom = 18 : zoom = 17;
map = new Map("map", {
basemap: "topo",
center: [-122.45,37.75], // long, lat
zoom: zoom,
sliderStyle: "small"
});
map.on("load",function(evt){
init();
initOfflineTileEnabler();
console.log("level: " + map.getLevel() + ", maxZoom: " + map.getMaxZoom());
})
function init(){
map.on("extent-change",function(evt){
updateOfflineUsage();
console.log("Zoom level = " + map.getLevel())
})
appCacheManager = new AppCacheManager(true,true);
appCacheManager.on(appCacheManager.CACHE_EVENT,cacheEventHandler);
appCacheManager.on(appCacheManager.CACHE_ERROR,cacheErrorHandler);
btnGetTiles = document.getElementById("btn-get-tiles");
btnOnlineOffline = document.getElementById("btn-online-offline");
btnZoom = document.getElementById("btn-zoom-out");
tileInfo = document.getElementById("tile-info")
tileInfo.innerHTML = "Tile count: 0\r\nBytes: 0";
map.reposition();
map.resize();
}
function initOfflineTileEnabler(){
mMaxZoom = map.getMaxZoom();
mMinZoom = map.getMinZoom();
offlineTileEnabler = new OfflineTileEnabler();
baseMapLayer = offlineTileEnabler.getBasemapLayer(map);
offlineTileEnabler.extend(baseMapLayer,function(success)
{
if( success )
{
console.log("Offline tile lib is enabled");
//using null sets this for CORS
baseMapLayer.offline.proxyPath = null;
updateOfflineUsage();
}
else
{
alert("error initializing storage - browser doesn't support indexeddb or websql")
}
}.bind(this));
}
function cacheEventHandler(evt){
console.log("CACHE EVENT: " + JSON.stringify(evt));
}
function cacheErrorHandler(evt){
console.log("CACHE ERROR: " + JSON.stringify(evt));
}
function updateState(){
if(Offline.state === 'up'){
updateOfflineUsage();
imgOfflineIndicator.src = bluePinPath;
baseMapLayer.goOnline();
}
else{
imgOfflineIndicator.src = redPinPath;
baseMapLayer.goOffline();
}
}
function updateOfflineUsage()
{
var count = getEstimateTileCount(function(info,err){
if(info != null){
console.log("COUNT " + info)
tileInfo.innerHTML = "Tile count: " + info.tileCount + "\r\nBytes: " + info.sizeBytes;
}
else{
console.log("ERROR updateOfflineUsage(): " + JSON.stringify(err));
}
});
}
/**
* Gets tile count and size estimates. Not perfect.
* @param callback
*/
function getEstimateTileCount(callback)
{
var extent = baseMapLayer.getExtentBuffer(EXTENT_BUFFER,map.extent);
var level = map.getLevel();
var url = baseMapLayer.getTileUrlsByExtent(extent,level)[0];
baseMapLayer._lastTileUrl = url;
baseMapLayer.estimateTileSize(function(tileSize,err){
if(tileSize != null){
var totalEstimation = {tileCount:0,sizeBytes:0};
var zoom = getMinMaxZoom();
for(var level = zoom.min; level<= zoom.max; level++)
{
var levelEstimation = baseMapLayer.getLevelEstimation(baseMapLayer.getExtentBuffer(EXTENT_BUFFER,map.extent),level,tileSize);
totalEstimation.tileCount += levelEstimation.tileCount;
totalEstimation.sizeBytes += levelEstimation.sizeBytes;
}
console.log("Size estimate: " + totalEstimation.sizeBytes + ", tile count: " + totalEstimation.tileCount)
callback(totalEstimation,null);
}
else{
callback(null,err);
}
}.bind(this))
}
}
</script>
<!-- This is a custom build of the ArcGIS API for JavaScript using the new Web Optimizer Tool -->
<script src="http://js.arcgis.com/o/agup_hack4co/appcache2/dojo/dojo.js" data-dojo-config="async: true"></script>
<!-- Use this tag below if you are hosting your ArcGIS API for JavaScript files locally -->
<!--<script src="libs/dojo/dojo/dojo.js" data-dojo-config="async: true"></script>-->
<script src="../vendor/IndexedDBShim/dist/IndexedDBShim.min.js"></script>
<script src="../vendor/offline/offline.min.js"></script>
<script>
Offline.options = {
checks: {
image: {
url: function() {
return 'http://esri.github.io/offline-editor-js/tiny-image.png?_=' + (Math.floor(Math.random() * 1000000000));
}
);
function getMinMaxZoom(){
var zoom = {};
var min = map.getLevel() + minZoomAdjust;
var max = map.getLevel() + maxZoomAdjust;
zoom.max = Math.min(mMaxZoom, max); //prevent errors by setting the tile layer floor
zoom.min = Math.max(mMinZoom, min); //prevent errors by setting the tile layer ceiling
return zoom;
},
active: 'image'
}
function reportProgress(progress)
{
console.log("downloading tiles...");
if(progress.hasOwnProperty("countNow")){
var percent = Math.floor(progress.countNow / progress.countMax * 100);
btnGetTiles.innerHTML = 'Saving to phone ' + percent + "% - Tap to Cancel";
}
if( progress.finishedDownloading )
{
btnGetTiles.innerHTML = "Saving to phone 100% - Tap to Cancel";
if( progress.cancelRequested )
{
globalState.downloadState = 'cancelled';
alert("Tile download was cancelled");
}
else
{
globalState.downloadState = 'downloaded';
alert("Tile download complete");
}
btnGetTiles.innerHTML = '1. Download Tiles';
}
return _wantToCancel; //determines if a cancel request has been issued
}
function downloadTiles(){
baseMapLayer.deleteAllTiles(function(success,err){
if(success == false){
alert("There was a problem deleting the tile cache");
}
else{
console.log("success deleting tile cache");
var self = this.data;
if( globalState.downloadState == 'downloading')
{
console.log("cancel!");
_wantToCancel = true;
btnGetTiles.innerHTML = "cancelling..";
}
else
{
var zoom = getMinMaxZoom();
var extent = baseMapLayer.getExtentBuffer(EXTENT_BUFFER,map.extent);
_wantToCancel = false;
baseMapLayer.prepareForOffline(zoom.min, zoom.max, extent, reportProgress.bind(this));
globalState.downloadState = 'downloading';
}
}
}.bind(this))
}
function goOnlineOffline(){
if(imgOfflineIndicator.offlineColor == "blue"){
btnOnlineOffline.innerHTML = "2. Go Online";
imgOfflineIndicator.src = redPinPath;
imgOfflineIndicator.offlineColor = "red";
baseMapLayer.goOffline();
}
else{
btnOnlineOffline.innerHTML = "2. Go Offline";
imgOfflineIndicator.src = bluePinPath;
imgOfflineIndicator.offlineColor = "blue";
baseMapLayer.goOnline();
}
}
function panLeft(){
map.panLeft();
}
</script>
}
</script>
</head>
<body>
<div id="button-div1">
<img id="img-offline-indicator" onclick="goOnlineOffline()" src="../samples/images/blue-pin.png"/>
<img id="img-offline-indicator" src="../samples/images/blue-pin.png"/>
<textarea contenteditable="false" id="tile-info"></textarea>
<button class="basic-btn" id="btn-get-tiles" onclick="downloadTiles()">1. Download Tiles</button>
<button class="basic-btn" onclick="goOnlineOffline()" id="btn-online-offline">2. Go Offline</button>
<button class="basic-btn" id="btn-get-tiles">1. Download Tiles</button>
<button class="basic-btn" id="btn-online-offline">2. Go Offline</button>
<button class="basic-btn" id="btn-zoom-out" onclick="panLeft()">3. Pan left</button>
</div>
<div id="map"></div>
</body>
</html>
<script>
var map;
require(["esri/map","utils/appCacheManager","tiles/OfflineTilesEnablerLayer","dojo/on","dojo/domReady!"],
function(Map,AppCacheManager,OfflineTileEnablerLayer,on) {
var tileLayer = null;
var zoom = 18;
var globalState = {};
var _wantToCancel;
var _isOnline = true;
var minZoomAdjust = -1, maxZoomAdjust = 1, mMinZoom, mMaxZoom;
var imgOfflineIndicator,btnGetTiles,btnOnlineOffline,btnZoom;
var tiles,appCacheManager,tileInfo;
var redPinPath = "../samples/images/red-pin.png";
var bluePinPath = "../samples/images/blue-pin.png"
var EXTENT_BUFFER = 0; //buffers the map extent in meters
imgOfflineIndicator = document.getElementById("img-offline-indicator");
imgOfflineIndicator.offlineColor = "blue";
Offline.check();
Offline.on('up down', updateState );
/**
* There have been a few bugs in the offline detection library (offline.min.js)
* This is a utility check to 100% validate if the application is online or
* offline prior to launching any map functionality.
*/
verifyOnline(function(result){ console.log("VERIFY ONLINE " + result)
result == true ? _isOnline = true : _isOnline = false;
startMap();
})
function startMap(){
//Make sure map shows up after a browser refresh
Offline.state === 'up' ? zoom = 18 : zoom = 17;
map = new Map("map", {
center: [-122.45,37.75], // long, lat
zoom: zoom,
sliderStyle: "small"
});
tileLayer = new OfflineTileEnablerLayer("http://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer",function(evt){
console.log("Offline tile lib is enabled. Application state is: " + Offline.state);
},_isOnline);
map.on("load",function(evt){
init();
Offline.check();
//using null sets this for CORS
tileLayer.offline.proxyPath = null;
on(btnOnlineOffline,"click",goOnlineOffline);
on(btnGetTiles,"click",downloadTiles);
updateOfflineUsage();
tileLayer.getMaxZoom(function(result){
mMaxZoom = result;
});
tileLayer.getMinZoom(function(result){
mMinZoom = result;
});
console.log("level: " + tileLayer.getLevel() + ", maxZoom: " + mMaxZoom);
})
map.addLayer(tileLayer);
}
function init(){
map.on("extent-change",function(evt){
updateOfflineUsage();
console.log("Zoom level = " + tileLayer.getLevel())
});
appCacheManager = new AppCacheManager(true,true);
appCacheManager.on(appCacheManager.CACHE_EVENT,cacheEventHandler);
appCacheManager.on(appCacheManager.CACHE_ERROR,cacheErrorHandler);
appCacheManager.on(appCacheManager.CACHE_LOADED,cacheLoaderHandler);
btnGetTiles = document.getElementById("btn-get-tiles");
btnOnlineOffline = document.getElementById("btn-online-offline");
btnZoom = document.getElementById("btn-zoom-out");
tileInfo = document.getElementById("tile-info")
tileInfo.innerHTML = "Tile count: 0\r\nBytes: 0";
map.reposition();
map.resize();
}
function updateState(){
if(Offline.state === 'up'){
updateOfflineUsage();
imgOfflineIndicator.src = bluePinPath;
if(typeof tileLayer != "undefined") tileLayer.goOnline();
}
else{
imgOfflineIndicator.src = redPinPath;
if(typeof tileLayer != "undefined") tileLayer.goOffline();
}
}
function updateOfflineUsage()
{
tileLayer.offline.store.usedSpace(function(result,err){
if(result != null){
tileInfo.innerHTML = "DB Tile count: " + result.tileCount + "\r\nDB Bytes: " + result.sizeBytes;
}
else{
tileInfo.innerHTML = "DB Tile count: " + count + "\r\nDB Bytes: Error";
}
})
}
/**
* Utility function to validate min and max zoom settings of the map
*/
function getMinMaxZoom(){
var zoom = {};
var min = tileLayer.getLevel() + minZoomAdjust;
var max = tileLayer.getLevel() + maxZoomAdjust;
zoom.max = Math.min(mMaxZoom, max); //prevent errors by setting the tile layer floor
zoom.min = Math.max(mMinZoom, min); //prevent errors by setting the tile layer ceiling
return zoom;
}
/**
* Reports the process while downloading tiles.
*/
function reportProgress(progress)
{
console.log("downloading tiles...");
if(progress.hasOwnProperty("countNow")){
var percent = Math.floor(progress.countNow / progress.countMax * 100);
btnGetTiles.innerHTML = 'Saving to phone ' + percent + "% - Tap to Cancel";
}
if( progress.finishedDownloading )
{
btnGetTiles.innerHTML = "Saving to phone 100% - Tap to Cancel";
updateOfflineUsage();
if( progress.cancelRequested )
{
globalState.downloadState = 'cancelled';
alert("Tile download was cancelled");
}
else
{
globalState.downloadState = 'downloaded';
alert("Tile download complete");
}
btnGetTiles.innerHTML = '1. Download Tiles';
}
return _wantToCancel; //determines if a cancel request has been issued
}
/**
* Forces offlineTileEnabler to go online or offline.
* If it is offline it will try to find a tile in the local database.
*/
function goOnlineOffline(){
if(imgOfflineIndicator.offlineColor == "blue"){
btnOnlineOffline.innerHTML = "2. Go Online";
imgOfflineIndicator.src = redPinPath;
imgOfflineIndicator.offlineColor = "red";
tileLayer.goOffline();
}
else{
btnOnlineOffline.innerHTML = "2. Go Offline";
imgOfflineIndicator.src = bluePinPath;
imgOfflineIndicator.offlineColor = "blue";
tileLayer.goOnline();
}
}
/**
* Manually starts the process to download and store tiles
* in the local database
*/
function downloadTiles(){
tileLayer.deleteAllTiles(function(success,err){
if(success == false){
alert("There was a problem deleting the tile cache");
}
else{
console.log("success deleting tile cache");
var self = this.data;
if( globalState.downloadState == 'downloading')
{
console.log("cancel!");
_wantToCancel = true;
btnGetTiles.innerHTML = "cancelling..";
}
else
{
var zoom = getMinMaxZoom();
var extent = tileLayer.getExtentBuffer(EXTENT_BUFFER,map.extent);
_wantToCancel = false;
tileLayer.prepareForOffline(zoom.min, zoom.max, extent, reportProgress.bind(this));
globalState.downloadState = 'downloading';
}
}
}.bind(this))
}
/**
* Attempts an http request to verify if app is online or offline.
* Use this in conjunction with the offline checker library: offline.min.js
* @param callback
*/
function verifyOnline(callback){
var req = new XMLHttpRequest();
req.open("GET", "images/blue-pin.png?" + (Math.floor(Math.random() * 1000000000)), true);
req.onload = function()
{
if( req.status === 200 && req.responseText !== "")
{
callback(true);
}
else
{
console.log("verifyOffline failed");
callback(false);
}
};
req.onerror = function(e)
{
console.log("verifyOffline failed: " + e);
callback(false);
};
req.send(null);
}
function cacheLoaderHandler(evt){
if(evt == appCacheManager.CACHE_LOADED) console.log("Application cache successfully loaded!")
}
function cacheEventHandler(evt){
console.log("CACHE EVENT: " + JSON.stringify(evt));
}
function cacheErrorHandler(evt){
console.log("CACHE ERROR: " + JSON.stringify(evt));
}
}
);
function panLeft(){
map.panLeft();
}
</script>
</body>

View File

@ -0,0 +1,138 @@
.mod-popup-modal-background {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
opacity: 0.5;
background-color: black;
z-index: 99;
display: table;
}
.mod-popup-input {
border-bottom: solid #ffffff 1px;
width: 80%;
display: table-cell;
vertical-align: middle;
padding: 12px;
}
.mod-popup-label {
padding: 12px;
width: 20%;
display: table-cell;
vertical-align: middle;
background-color: dimgrey;
}
.mod-popup-label-top-left {
padding: 12px;
width: 20%;
display: table-cell;
vertical-align: middle;
background-color: dimgray;
border-top-left-radius: 10px;
}
.mod-popup-table-row {
color: white;
border-radius: 10px;
display: table-row;
}
.mod-popup-stop-input {
border-radius: 5px;
font-size: x-large;
width: 90%;
height: 75%;
padding: 5px;
}
.mod-popup-stop-input-disabled {
color: white;
border-radius: 5px;
font-size: x-large;
background-color: dimgray;
opacity: 1.0; /* For safari display bug */
padding: 5px;
width: 90%;
height: 75%;
}
.mod-popup-button {
position: relative;
float: left;
color: #ffffff;
font-size: x-large;
border-radius: 10px;
width: 45%;
padding: 8px;
background-color: #000000;
border: solid #ffffff 2px;
}
.mod-popup-button:active {
position: relative;
float: left;
color: #000000;
font-size: x-large;
border-radius: 10px;
width: 45%;
padding: 8px;
background-color: lightyellow;
border: solid #ffffff 2px;
}
.mod-popup-button-cancel {
position: relative;
float: left;
color: lightgray;
font-size: x-large;
border-radius: 10px;
width: 100%;
padding: 8px;
background-color: #000000;
border: solid lightgray 1px;
}
.mod-popup-button-cancel:active {
position: relative;
float: left;
color: #000000;
font-size: x-large;
border-radius: 10px;
width: 100%;
padding: 8px;
background-color: lightyellow;
border: solid lightgray 1px;
}
.mod-popup-button-div {
padding: 12px;
display: table-cell;
background-color: #000000;
}
.mod-popup-button-div-bottom-left {
padding: 12px;
display: table-cell;
background-color: #000000;
border-bottom-left-radius: 8px;
}
@media (max-width: 500px) {
.mod-popup-button {
font-size: small;
}
.mod-popup-button:active{
font-size: small;
}
.mod-popup-button-cancel {
font-size: small;
}
.mod-popup-button-cancel:active {
font-size: small;
}
}
@media (max-width: 450px) {
.mod-popup-button {
font-size: x-small;
}
.mod-popup-button:active{
font-size: x-small;
}
.mod-popup-button-cancel {
font-size: x-small;
}
.mod-popup-button-cancel:active {
font-size: x-small;
}
}

View File

@ -5,9 +5,9 @@
"https://www.npmjs.org/doc/cli/npm-init.html"
],
"name": "manifest-generator",
"manifestName": "appcache-tiles.appcache",
"appHomePage": "appcache-tiles.html",
"optimizedApiURL": "http://js.arcgis.com/o/agup_hack4co/appcache2",
"manifestName": "appcache-features.appcache",
"appHomePage": "appcache-features.html",
"optimizedApiURL": "http://js.arcgis.com/o/agup_hack4co/appcacheFeatures",
"arcGISBaseURL": "http://js.arcgis.com/3.9",
"version": "0.0.1",
"private": true,
@ -16,7 +16,7 @@
"type": "git",
"url": "https://github.com/Esri/offline-editor-js.git"
},
"author": "Lloyd Heberlie",
"author": "Andy Gup",
"license": "Apache 2",
"bugs": {
"url": "https://github.com/Esri/offline-editor-js/issues"

View File

@ -0,0 +1,87 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>Jasmine Spec Runner - Tiles</title>
<link rel="shortcut icon" type="image/png" href="../vendor/jasmine-1.3.1/jasmine_favicon.png">
<link rel="stylesheet" type="text/css" href="../vendor/jasmine-1.3.1/jasmine.css">
<script type="text/javascript" src="../vendor/jasmine-1.3.1/jasmine.js"></script>
<script type="text/javascript" src="../vendor/jasmine-1.3.1/jasmine-html.js"></script>
<script type="text/javascript" src="../vendor/jasmine.async/lib/jasmine.async.js"></script>
<script>
var dojoConfig = {
paths: { tiles: location.pathname.replace(/\/[^/]+$/, "") + "../../lib/tiles" }
}
</script>
<link rel="stylesheet" href="http://js.arcgis.com/3.9/js/dojo/dijit/themes/claro/claro.css">
<link rel="stylesheet" href="http://js.arcgis.com/3.9/js/esri/css/esri.css">
<script src="http://js.arcgis.com/3.9/"></script>
<!-- include spec files here... -->
<script type="text/javascript" src="spec/offlineTilesEnablerLayerSpec.js"></script>
<script type="text/javascript">
var g_map;
var g_basemapLayer;
require(["esri/map",
"esri/layers/GraphicsLayer", "esri/graphic", "esri/symbols/SimpleFillSymbol",
"esri/dijit/Scalebar", "esri/arcgis/utils", "esri/geometry",
"dojo/dom", "dojo/on", "dojo/query",
"esri/urlUtils", "esri/geometry/webMercatorUtils",
"tiles/OfflineTilesEnablerLayer",
"dojo/dom-construct", "dojo/domReady!"],
function(Map,
GraphicsLayer, Graphic, SimpleFillSymbol,
Scalebar, esriUtils, geometry,
dom, on, query,
urlUtils, webMercatorUtils,
OfflineTilesEnablerLayer,
domConstruct)
{
g_basemapLayer = new OfflineTilesEnablerLayer("http://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer",function(evt){
console.log("Tile Layer Loaded.");
},true);
g_map = new Map("map", {
center: [-3.695, 40.412], // Madrid center
zoom: 14,
sliderStyle: "small"
});
g_map.on('load', test);
g_map.addLayer(g_basemapLayer);
function test()
{
var jasmineEnv = jasmine.getEnv();
jasmineEnv.updateInterval = 1000;
jasmineEnv.defaultTimeoutInterval = 20000; // 20 sec
var htmlReporter = new jasmine.HtmlReporter();
jasmineEnv.addReporter(htmlReporter);
jasmineEnv.specFilter = function(spec) {
return htmlReporter.specFilter(spec);
};
var currentWindowOnload = window.onload;
jasmineEnv.execute();
}; // test()
}); // require()
</script>
</head>
<body>
<div id="map" style="position: absolute; bottom: 0; right: 0; height:200px; width: 200px;"></div>
<img src="" alt="" id="fakeTile" style="display:none;">
</body>
</html>

View File

@ -0,0 +1,239 @@
"use strict"
describe("offline enabler custom layer library", function()
{
var async = new AsyncSpec(this);
async.it("validate map", function(done)
{ console.log("VALIDATE !!!!!")
expect(g_map).toEqual(jasmine.any(Object));
expect(g_map.id).toEqual("map");
done();
});
async.it("validate tiled layer", function(done)
{
expect(g_basemapLayer).toEqual(jasmine.any(Object));
expect(g_basemapLayer.tileInfo).toEqual(jasmine.any(Object));
done();
});
// async.it("extends the tiled layer object", function(done)
// {
// expect(g_basemapLayer.goOffline).toBeUndefined();
// g_offlineTilesEnabler.extend(g_basemapLayer,function(success)
// {
// expect(success).toEqual(true);
// expect(g_basemapLayer.goOffline).toEqual(jasmine.any(Function));
// expect(g_basemapLayer.goOnline).toEqual(jasmine.any(Function));
// expect(g_basemapLayer.getTileUrl).toEqual(jasmine.any(Function));
// expect(g_basemapLayer._getTileUrl).toEqual(jasmine.any(Function));
// expect(g_basemapLayer.prepareForOffline).toEqual(jasmine.any(Function));
// expect(g_basemapLayer._storeTile).toEqual(jasmine.any(Function));
// expect(g_basemapLayer.deleteAllTiles).toEqual(jasmine.any(Function));
// expect(g_basemapLayer.offline).toEqual(jasmine.any(Object));
// expect(g_basemapLayer.offline.store).toEqual(jasmine.any(Object));
//
// g_basemapLayer.offline.proxyPath = "../lib/resource-proxy/proxy.php";
// done();
// });
// });
//
async.it("can go offline", function(done)
{
expect(g_basemapLayer.goOffline).toEqual(jasmine.any(Function));
expect(g_basemapLayer.offline.online).toEqual(true);
g_basemapLayer.goOffline();
expect(g_basemapLayer.offline.online).toEqual(false);
done();
});
async.it("can go online", function(done)
{
expect(g_basemapLayer.goOffline).toEqual(jasmine.any(Function));
expect(g_basemapLayer.offline.online).toEqual(false);
g_basemapLayer.goOnline();
expect(g_basemapLayer.offline.online).toEqual(true);
done();
})
async.it("delete all tiles", function(done)
{
g_basemapLayer.deleteAllTiles(function(success)
{
expect(success).toEqual(true);
setTimeout(function()
{
g_basemapLayer.getOfflineUsage(function(usage)
{
expect(usage.tileCount).toEqual(0);
done();
});
},1);
});
});
async.it("stores one tile", function(done)
{
g_basemapLayer.getOfflineUsage(function(usage)
{
expect(usage.tileCount).toEqual(0);
g_basemapLayer._storeTile(14,6177,8023, function(success)
{
expect(success).toEqual(true);
g_basemapLayer.getOfflineUsage(function(usage)
{
expect(usage.tileCount).toEqual(1);
done();
});
});
});
});
async.it("stores one tile again", function(done)
{
g_basemapLayer.getOfflineUsage(function(usage)
{
expect(usage.tileCount).toEqual(1);
g_basemapLayer._storeTile(14,6177,8023, function(success)
{
expect(success).toEqual(true);
g_basemapLayer.getOfflineUsage(function(usage)
{
expect(usage.tileCount).toEqual(1);
done();
});
});
});
});
async.it("gets level estimation", function(done)
{
require(["esri/geometry/Extent"],function(Extent)
{
var extent = new Extent({"xmin":-822542.2830377579,"ymin":4580841.761960262,"xmax":94702.05638410954,"ymax":5131188.365613382,"spatialReference":{"wkid":102100}});
g_basemapLayer.estimateTileSize(function(tileSize){
var estimation = g_basemapLayer.getLevelEstimation(extent,10,tileSize);
expect(estimation.tileCount).toEqual(375);
expect(estimation.sizeBytes).toEqual(estimation.tileCount * tileSize);
var estimation = g_basemapLayer.getLevelEstimation(extent,8,tileSize);
expect(estimation.tileCount).toEqual(28);
expect(estimation.sizeBytes).toEqual(estimation.tileCount * tileSize);
var estimation = g_basemapLayer.getLevelEstimation(extent,2,tileSize);
expect(estimation.tileCount).toEqual(2);
expect(estimation.sizeBytes).toEqual(estimation.tileCount * tileSize);
done();
}.bind(this));
});
});
async.it("prepares the layer for offline usage", function(done)
{
require(["esri/geometry/Extent"], function(Extent)
{
g_basemapLayer.deleteAllTiles(function(success)
{
expect(success).toEqual(true);
var extent = new Extent({"xmin":-822542.2830377579,"ymin":4580841.761960262,"xmax":94702.05638410954,"ymax":5131188.365613382,"spatialReference":{"wkid":102100}});
var callCount = 0;
var reportProgress = function(progress)
{
callCount += 1;
expect(progress.error).not.toBeDefined();
if( progress.finishedDownloading )
{
g_basemapLayer.getOfflineUsage(function(usage)
{
expect(usage.tileCount).toEqual(28);
expect(callCount).toEqual(29);
done();
});
}
return false; // cancelRequested = false;
}
g_basemapLayer.prepareForOffline(8,8,extent,reportProgress);
});
});
});
async.it("get tile urls",function(done)
{
require(["esri/geometry/Extent"],function(Extent){
var extent = new Extent({"xmin":-822542.2830377579,"ymin":4580841.761960262,"xmax":94702.05638410954,"ymax":5131188.365613382,"spatialReference":{"wkid":102100}});
var cells = g_basemapLayer.getTileUrlsByExtent(extent,3);
var regexp = /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/;
for(var i = 0; i < cells.length; i++){
var isUrl = regexp.test(cells[i]);
expect(isUrl).toBe(true);
}
expect(cells.length).toBeGreaterThan(0);
done();
});
});
async.it("get extent buffer",function(done)
{
require(["esri/geometry/Extent"],function(Extent){
var extent = new Extent({"xmin":-822542.2830377579,"ymin":4580841.761960262,"xmax":94702.05638410954,"ymax":5131188.365613382,"spatialReference":{"wkid":102100}});
var newExtent = g_basemapLayer.getExtentBuffer(1000,extent);
expect(newExtent.xmin).toBe(-823542.2830377579);
done();
});
});
async.it("returns placeholder urls when offline", function(done)
{
require(["dojo/dom"], function(dom)
{
var fakeTile = dom.byId('fakeTile');
g_basemapLayer.goOnline();
var onlineUrl = g_basemapLayer.getTileUrl(14,6178,8023);
//NOTE: We are getting new attributes at ArcGIS JS API v3.8 : blankTile=false&_ts=1393031666639 <last part is a random number>
// http://services.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/14/6178/8023?blankTile=false&_ts=1393031666639"
var tempUrl = onlineUrl.slice( 0, onlineUrl.indexOf('?'))
expect(tempUrl).toEqual('http://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/14/6178/802');
g_basemapLayer.goOffline();
var offlineUrl = fakeTile.src = g_basemapLayer.getTileUrl(14,6178,8023);
expect(offlineUrl).toEqual('void:/14/6178/8023');
done();
})
});
async.it("getMaxZoom", function(done){
g_basemapLayer.getMaxZoom(function(result){
expect(result).toBe(19);
done();
})
});
async.it("getMinZoom", function(done){
g_basemapLayer.getMinZoom(function(result){
expect(result).toBe(0);
done();
})
});
it("verifies ability to retrieve layer info",function(done){
g_basemapLayer._getTileInfoPrivate("http://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer",function(result){
var fixedResponse = result.replace(/\\'/g, "'");
var resultObj = JSON.parse(fixedResponse);
expect(resultObj).toEqual(jasmine.any(Object));
})
});
it("verifies ability to parse layer info",function(done){
g_basemapLayer._getTileInfoPrivate("http://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer",function(result){
g_basemapLayer.parseGetTileInfo(result,function(result){
expect(result).toEqual(jasmine.any(Object));
})
})
});
});

View File

@ -1,5 +1,12 @@
/**
* Helper Class for working with the application cache.
*
* Listen for the following events:
* UPDATE_READY - an update to the cache is ready
* UPDATE_NONE - the cache hasn't changed since the last app load.
* CACHE_LOADED - the cache has finished loading.
* CACHE_ERROR - an error was thrown by the browser while attempting to load the cache.
*
* For more information on application cache:
* https://developer.mozilla.org/en-US/docs/HTML/Using_the_application_cache
* Many thanks and all kudos go to the following blog posts:
@ -14,6 +21,7 @@ define([
UPDATE_READY: "update-ready",
UPDATE_NONE: "no-update",
CACHE_EVENT: "cache-event",
CACHE_LOADED: "cache-loaded",
CACHE_ERROR: "cache-error",
appCache: window.applicationCache,
@ -101,6 +109,12 @@ define([
},
_handleCacheEvents:function(evt){
if(evt.hasOwnProperty("total") && evt.hasOwnProperty("loaded")){
if(evt.total == evt.loaded){
console.log("appCacheManager: cache has finished loading.")
this.emit(this.CACHE_LOADED,"cache-loaded");
}
}
this.emit(this.CACHE_EVENT,evt);
},