Skip to content

Commit

Permalink
added offline tile storage mgmt library & demo app
Browse files Browse the repository at this point in the history
  • Loading branch information
andygup committed Oct 7, 2013
1 parent 957f3bf commit 8f59cd8
Show file tree
Hide file tree
Showing 7 changed files with 768 additions and 5 deletions.
34 changes: 29 additions & 5 deletions README.md
@@ -1,10 +1,18 @@
offline-editor-js
=================

Experimental JavaScript library that auto-detects an offline condition and stores FeatureLayer edit activities until a connection is reestablished. No longer will offline edit be the sole domain of native SDKs!
Experimental JavaScript library that auto-detects an offline condition and stores FeatureLayer edit activities until a connection is reestablished. Works with adds, updates and deletes.

Includes several libraries:

- OfflineStore - overrides applyEdits() method
- OfflineTileStore - stores tiles for offline pan and zoom.
- OfflineFeatureStore - **TBD** (manages features for offline usage)

##How to use?

The easiest approach is to simply use the library to override applyEdits():

**Step 1.** The library provides a constructor that can simply be used in place of the traditional applyEdit() method. It does all the rest of the work for you:

var offlineStore = new OfflineStore(map);
Expand All @@ -20,25 +28,27 @@ While the library works in Chrome, Firefox and Safari with the internet turned o

##Features

* Override the applyEdits() method.
* Can store base map tiles for offline pan and zoom.
* Automatic offline/online detection. Once an offline condition exists the library starts storing the edits. And, as soon as it reconnects it will submit the updates.
* Can store dozens or hundreds of edits.
* Currently works with Points, Polylines and Polygons.
* Indexes edits for successful/unsuccessful update validation as well as for more advanced workflows.
* Monitors available storage and is configured by default to stop edits at a maximum threshold and alert that the threshold has been reached. This is intended to help prevent data loss.

##API
##OfflineStore Library

####OfflineStore(/\* Map \*/ map)
* Constructor. Requires a reference to an ArcGIS API for JavaScript Map.

####applyEdits(/\* Graphic \*/ graphic,/\* FeatureLayer \*/ layer, /\* String \*/ enumValue)
* Method.
* Method. Overrides FeatureLayer.applyEdits().

####getStore()
* Returns an array of Graphics.
* Returns an array of Graphics from localStorage.

####getLocalStoreIndex()
* Returns the index as an array of JSON objects. The objects are constructor like this:
* Returns the index as an array of JSON objects. An internal index is used to keep track of adds, deletes and updates. The objects are constructed like this:

{"id": object610,"type":"add","success":"true"}

Expand Down Expand Up @@ -68,7 +78,21 @@ While the library works in Chrome, Firefox and Safari with the internet turned o
}


##OfflineTileStore Library

####OfflineTileStore()
* Constructor. Stores tiles for offline panning and zoom.


####storeLayer()
* Stores tiled in either localStorage or IndexedDB if it is available. Storage process is initiated by forcing a refresh on the basemap layer.

####useIndexedDB
* Property. Manually sets whether library used localStorage or IndexedDB. Default is false.


####getLocalStorageUsed()
* Returns amount of storage used by the calling domain. Typical browser limit is 5MBs.

##Testing
Run Jasmine's SpecRunner.html in a browser. You can find it in the /test directory.
Expand Down
89 changes: 89 additions & 0 deletions tiles/index.html
@@ -0,0 +1,89 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=7, IE=9, IE=10">
<meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no">
<title>Store images in localStorage</title>

<link rel="stylesheet" href="http://js.arcgis.com/3.7/js/dojo/dijit/themes/claro/claro.css">
<link rel="stylesheet" href="http://js.arcgis.com/3.7/js/esri/css/esri.css">
<style>
html, body { height: 100%; width: 100%; margin: 0; padding: 0; }
#map{
padding:0;
}
</style>

<script src="./src/OfflineTileStore.js"></script>
<!--<script src="./src/dbStore.js"></script>-->
<script src="./src/dbStore2.js"></script>
<script>var dojoConfig = {parseOnLoad: true};</script>
<script src="http://js.arcgis.com/3.7/"></script>
<script>
dojo.require("esri.map");
dojo.require("dojox.encoding.digests._base");
dojo.require("dijit.layout.BorderContainer");
dojo.require("dijit.layout.ContentPane");

var map;
var database;
var dbSupport = false;
var offlineTileStore = null;

require([
"esri/map"
],function(Map){

database = new dbStore();
dbSupport = database.isSupported();
if(dbSupport == true){
database.init(function(evt,err){
console.log("dbstore init: " + evt);
}.bind(this));
}

map = new esri.Map("map", {
basemap: "streets",
center: [2.352, 48.87],
zoom: 12
});

map.on("layer-add-result", function(evt){
try{
offlineTileStore = new OfflineTileStore(map)
console.log("Local storage used: " + offlineTileStore.getlocalStorageUsed())
}
catch(err){
console.log("err " + err.stack)
};
}.bind(this));

})

function deleteStorage(){
localStorage.clear();
}

function getSize(){
//database.testGet();
database.size(function(evt,err){
console.log("GET " + evt + " MBs, err: " + err);
})
}

function testRefresh(){
offlineTileStore.storeLayer();
}

</script>
</head>
<body class="claro">
<button onclick="deleteStorage()">Delete localStore</button>
<button onclick="getSize()">Get db size</button>
<button onclick="testRefresh()">Refresh</button>

<div id="map" data-dojo-type="dijit.layout.ContentPane" data-dojo-props="region:'center'" style="overflow:hidden;">
</div>
</body>
</html>
137 changes: 137 additions & 0 deletions tiles/proxy.php
@@ -0,0 +1,137 @@
<?php
/***************************************************************************
* USAGE
* [1] http://<this-proxy-url>?<arcgis-service-url>
* [2] http://<this-proxy-url>?<arcgis-service-url> (with POST body)
* [3] http://<this-proxy-url>?<arcgis-service-url>?token=ABCDEFGH
*
* note: [3] is used when fetching tiles from a secured service and the
* JavaScript app sends the token instead of being set in this proxy
*
* REQUIREMENTS
* - cURL extension for PHP must be installed and loaded. To load it,
* add the following lines to your php.ini file:
* extension_dir = "<your-php-install-location>/ext"
* extension = php_curl.dll
*
* - Turn OFF magic quotes for incoming GET/POST data: add/modify the
* following line to your php.ini file:
* magic_quotes_gpc = Off
*
***************************************************************************/

/***************************************************************************
* <true> to only proxy to the sites listed in '$serverUrls'
* <false> to proxy to any site (are you sure you want to do this?)
*/
$mustMatch = true;

/***************************************************************************
* ArcGIS Server services this proxy will forward requests to
*
* 'url' = location of the ArcGIS Server, either specific URL or stem
* 'matchAll' = <true> to forward any request beginning with the URL
* <false> to forward only the request that exactly matches the url
* 'token' = token to include for secured service, if any, otherwise leave it
* empty
*/
$serverUrls = array(
array( 'url' => 'http://sampleserver1.arcgisonline.com/ArcGIS/rest/services/', 'matchAll' => true, 'token' => '' ),
array( 'url' => 'http://services.arcgisonline.com/ArcGIS/rest/services/', 'matchAll' => true, 'token' => '' ),
array( 'url' => 'http://sampleserver2.arcgisonline.com/ArcGIS/rest/services/', 'matchAll' => true, 'token' => '' ),
array( 'url' => 'http://sampleserver1a.arcgisonline.com/arcgisoutput/', 'matchAll' => true, 'token' => '' ),
array( 'url' => 'http://sampleserver1b.arcgisonline.com/arcgisoutput/', 'matchAll' => true, 'token' => '' ),
array( 'url' => 'http://sampleserver1c.arcgisonline.com/arcgisoutput/', 'matchAll' => true, 'token' => '' )
);
/***************************************************************************/

function is_url_allowed($allowedServers, $url) {
$isOk = false;
$url = trim($url, "\/");
for ($i = 0, $len = count($allowedServers); $i < $len; $i++) {
$value = $allowedServers[$i];
$allowedUrl = trim($value['url'], "\/");
if ($value['matchAll']) {
if (stripos($url, $allowedUrl) === 0) {
$isOk = $i; // array index that matched
break;
}
}
else {
if ((strcasecmp($url, $allowedUrl) == 0)) {
$isOk = $i; // array index that matched
break;
}
}
}
return $isOk;
}

// check if the curl extension is loaded
if (!extension_loaded("curl")) {
header('Status: 500', true, 500);
echo 'cURL extension for PHP is not loaded! <br/> Add the following lines to your php.ini file: <br/> extension_dir = &quot;&lt;your-php-install-location&gt;/ext&quot; <br/> extension = php_curl.dll';
return;
}

$targetUrl = $_SERVER['QUERY_STRING'];
if (!$targetUrl) {
header('Status: 400', true, 400); // Bad Request
echo 'Target URL is not specified! <br/> Usage: <br/> http://&lt;this-proxy-url&gt;?&lt;target-url&gt;';
return;
}

$parts = preg_split("/\?/", $targetUrl);
$targetPath = $parts[0];

// check if the request URL matches any of the allowed URLs
if ($mustMatch) {
$pos = is_url_allowed($serverUrls, $targetPath);
if ($pos === false) {
header('Status: 403', true, 403); // Forbidden
echo 'Target URL is not allowed! <br/> Consult the documentation for this proxy to add the target URL to its Whitelist.';
return;
}
}

// add token (if any) to the url
$token = $serverUrls[$pos]['token'];
if ($token) {
$targetUrl .= (stripos($targetUrl, "?") !== false ? '&' : '?').'token='.$token;
}

// open the curl session
$session = curl_init();

// set the appropriate options for this request
$options = array(
CURLOPT_URL => $targetUrl,
CURLOPT_HEADER => false,
CURLOPT_HTTPHEADER => array(
'Content-Type: ' . $_SERVER['CONTENT_TYPE'],
'Referer: ' . $_SERVER['HTTP_REFERER']
),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true
);

// put the POST data in the request body
$postData = file_get_contents("php://input");
if (strlen($postData) > 0) {
$options[CURLOPT_POST] = true;
$options[CURLOPT_POSTFIELDS] = $postData;
}
curl_setopt_array($session, $options);

// make the call
$response = curl_exec($session);
$code = curl_getinfo($session, CURLINFO_HTTP_CODE);
$type = curl_getinfo($session, CURLINFO_CONTENT_TYPE);
curl_close($session);

// set the proper Content-Type
header("Status: ".$code, true, $code);
header("Content-Type: ".$type);

echo $response;
?>

0 comments on commit 8f59cd8

Please sign in to comment.