diff --git a/build/license.txt b/build/license.txt index dfc0d1dfa7..b74e2a79e7 100644 --- a/build/license.txt +++ b/build/license.txt @@ -52,3 +52,41 @@ * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 */ + +/** + * Contains portions of Gears + * + * Copyright 2007, Google Inc. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of Google Inc. nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * Sets up google.gears.*, which is *the only* supported way to access Gears. + * + * Circumvent this file at your own risk! + * + * In the future, Gears may automatically define google.gears.* without this + * file. Gears may use these objects to transparently fix bugs and compatibility + * issues. Applications that use the code below will continue to work seamlessly + * when that happens. + */ diff --git a/examples/protocol-gears.html b/examples/protocol-gears.html new file mode 100644 index 0000000000..6bbfb5f030 --- /dev/null +++ b/examples/protocol-gears.html @@ -0,0 +1,261 @@ + + + + + + + + + +

Gears Protocol Example

+ +
+
+

+ Shows the usage of the Gears protocol. +

+ +
+
+
+ +
+ Sync +

The Sync link destroys the features currently in the layer, reads + features from the Gears database, and adds them to the layer. + Uncommitted features will be lost.

+ + Commit +

The Commit link commits to the Gears database the features that are + marked as INSERT, UPDATE or DELETE.

+ + Delete +

The Delete link marks the selected feature as DELETE. To select a feature + click choose the navigation control in the editing toolbar.

+
+ +
+

Status:

+

Result:

+
+ +
+

This example demonstrates the usage of OpenLayers Gears protocol to + read/create/update/delete features from/to the Gears database. + Gears must obviously be installed + in your browser for this example to work.

+
+ + diff --git a/lib/Gears/gears_init.js b/lib/Gears/gears_init.js new file mode 100644 index 0000000000..d531a3f5bb --- /dev/null +++ b/lib/Gears/gears_init.js @@ -0,0 +1,88 @@ +/* + * Copyright 2007, Google Inc. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of Google Inc. nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * Sets up google.gears.*, which is *the only* supported way to access Gears. + * + * Circumvent this file at your own risk! + * + * In the future, Gears may automatically define google.gears.* without this + * file. Gears may use these objects to transparently fix bugs and compatibility + * issues. Applications that use the code below will continue to work seamlessly + * when that happens. + */ + +(function() { + // We are already defined. Hooray! + if (window.google && google.gears) { + return; + } + + var factory = null; + + // Firefox + if (typeof GearsFactory != 'undefined') { + factory = new GearsFactory(); + } else { + // IE + try { + factory = new ActiveXObject('Gears.Factory'); + // privateSetGlobalObject is only required and supported on WinCE. + if (factory.getBuildInfo().indexOf('ie_mobile') != -1) { + factory.privateSetGlobalObject(this); + } + } catch (e) { + // Safari + if ((typeof navigator.mimeTypes != 'undefined') + && navigator.mimeTypes["application/x-googlegears"]) { + factory = document.createElement("object"); + factory.style.display = "none"; + factory.width = 0; + factory.height = 0; + factory.type = "application/x-googlegears"; + document.documentElement.appendChild(factory); + } + } + } + + // *Do not* define any objects if Gears is not installed. This mimics the + // behavior of Gears defining the objects in the future. + if (!factory) { + return; + } + + // Now set up the objects, being careful not to overwrite anything. + // + // Note: In Internet Explorer for Windows Mobile, you can't add properties to + // the window object. However, global objects are automatically added as + // properties of the window object in all browsers. + if (!window.google) { + google = {}; + } + + if (!google.gears) { + google.gears = {factory: factory}; + } +})(); diff --git a/lib/OpenLayers.js b/lib/OpenLayers.js index 6b3383e4d1..483281b3e0 100644 --- a/lib/OpenLayers.js +++ b/lib/OpenLayers.js @@ -83,6 +83,7 @@ "OpenLayers/Tween.js", "Rico/Corner.js", "Rico/Color.js", + "Gears/gears_init.js", "OpenLayers/Ajax.js", "OpenLayers/Request.js", "OpenLayers/Request/XMLHttpRequest.js", @@ -190,6 +191,8 @@ "OpenLayers/Strategy/BBOX.js", "OpenLayers/Protocol.js", "OpenLayers/Protocol/HTTP.js", + "OpenLayers/Protocol/SQL.js", + "OpenLayers/Protocol/SQL/Gears.js", "OpenLayers/Layer/PointTrack.js", "OpenLayers/Layer/GML.js", "OpenLayers/Style.js", diff --git a/lib/OpenLayers/Protocol/SQL/Gears.js b/lib/OpenLayers/Protocol/SQL/Gears.js new file mode 100644 index 0000000000..0c76b3d4ba --- /dev/null +++ b/lib/OpenLayers/Protocol/SQL/Gears.js @@ -0,0 +1,559 @@ +/* Copyright (c) 2006-2008 MetaCarta, Inc., published under the Clear BSD + * license. See http://svn.openlayers.org/trunk/openlayers/license.txt for the + * full text of the license. */ + +/** + * @requires Gears/gears_init.js + * @requires OpenLayers/Protocol/SQL.js + * @requires OpenLayers/Format/JSON.js + * @requires OpenLayers/Format/WKT.js + */ + +/** + * Class: OpenLayers.Protocol.SQL.Gears + * This Protocol stores feature in the browser via the Gears Database module + * . + * + * The main advantage is that all the read, create, update and delete operations + * can be done offline. + * + * Inherits from: + * - + */ +OpenLayers.Protocol.SQL.Gears = OpenLayers.Class(OpenLayers.Protocol.SQL, { + + /** + * Property: FID_PREFIX + * {String} + */ + FID_PREFIX: '__gears_fid__', + + /** + * Property: NULL_GEOMETRY + * {String} + */ + NULL_GEOMETRY: '__gears_null_geometry__', + + /** + * Property: NULL_FEATURE_STATE + * {String} + */ + NULL_FEATURE_STATE: '__gears_null_feature_state__', + + /** + * Property: jsonParser + * {} + */ + jsonParser: null, + + /** + * Property: wktParser + * {} + */ + wktParser: null, + + /** + * Property: fidRegExp + * {RegExp} Regular expression to know whether a feature was + * created in offline mode. + */ + fidRegExp: null, + + /** + * Property: saveFeatureState + * {Boolean} Whether to save the feature state () + * into the database, defaults to true. + */ + saveFeatureState: true, + + /** + * Property: typeOfFid + * {String} The type of the feature identifier, either "number" or + * "string", defaults to "string". + */ + typeOfFid: "string", + + /** + * Property: db + * {GearsDatabase} + */ + db: null, + + /** + * Constructor: OpenLayers.Protocol.SQL.Gears + */ + initialize: function(options) { + if (!this.supported()) { + return; + } + OpenLayers.Protocol.SQL.prototype.initialize.apply(this, [options]); + this.jsonParser = new OpenLayers.Format.JSON(); + this.wktParser = new OpenLayers.Format.WKT(); + + this.fidRegExp = new RegExp('^' + this.FID_PREFIX); + this.initializeDatabase(); + + + }, + + /** + * Method: initializeDatabase + */ + initializeDatabase: function() { + this.db = google.gears.factory.create('beta.database'); + this.db.open(this.databaseName); + this.db.execute( + "CREATE TABLE IF NOT EXISTS " + this.tableName + + " (fid TEXT UNIQUE, geometry TEXT, properties TEXT," + + " state TEXT)"); + }, + + /** + * APIMethod: destroy + * Clean up the protocol. + */ + destroy: function() { + this.db.close(); + this.db = null; + + this.jsonParser = null; + this.wktParser = null; + + OpenLayers.Protocol.SQL.prototype.destroy.apply(this); + }, + + /** + * APIMethod: supported + * Determine whether a browser supports Gears + * + * Returns: + * {Boolean} The browser supports Gears + */ + supported: function() { + return !!(window.google && google.gears); + }, + + /** + * Method: read + * Read all features from the database and return a + * instance. If the options parameter + * contains a callback attribute, the function is called with the response + * as a parameter. + * + * Parameters: + * options - {Object} Optional object for configuring the request; it + * can have the {Boolean} property "noFeatureStateReset" which + * specifies if the state of features read from the Gears + * database must be reset to null, if "noFeatureStateReset" + * is undefined or false then each feature's state is reset + * to null, if "noFeatureStateReset" is true the feature state + * is preserved. + * + * Returns: + * {} An + * object. + */ + read: function(options) { + options = OpenLayers.Util.applyDefaults(options, this.options); + + var feature, features = []; + var rs = this.db.execute("SELECT * FROM " + this.tableName); + while (rs.isValidRow()) { + feature = this.unfreezeFeature(rs); + if (this.evaluateFilter(feature, options.filter)) { + if (!options.noFeatureStateReset) { + feature.state = null; + } + features.push(feature); + } + rs.next(); + } + rs.close(); + + var resp = new OpenLayers.Protocol.Response({ + code: OpenLayers.Protocol.Response.SUCCESS, + requestType: "read", + features: features + }); + + if (options && options.callback) { + options.callback.call(options.scope, resp); + } + + return resp; + }, + + /** + * Method: unfreezeFeature + * + * Parameters: + * row - {ResultSet} + * + * Returns: + * {} + */ + unfreezeFeature: function(row) { + var feature; + var wkt = row.fieldByName('geometry'); + if (wkt == this.NULL_GEOMETRY) { + feature = new OpenLayers.Feature.Vector(); + } else { + feature = this.wktParser.read(wkt); + } + + feature.attributes = this.jsonParser.read( + row.fieldByName('properties')); + + feature.fid = this.extractFidFromField(row.fieldByName('fid')); + + var state = row.fieldByName('state'); + if (state == this.NULL_FEATURE_STATE) { + state = null; + } + feature.state = state; + + return feature; + }, + + /** + * Method: extractFidFromField + * + * Parameters: + * field - {String} + * + * Returns + * {String} or {Number} The fid. + */ + extractFidFromField: function(field) { + if (!field.match(this.fidRegExp) && this.typeOfFid == "number") { + field = parseFloat(field); + } + return field; + }, + + /** + * Method: create + * Create new features into the database. + * + * Parameters: + * features - {Array({})} or + * {} The features to create in + * the database. + * options - {Object} Optional object for configuring the request. + * + * Returns: + * {} An + * object. + */ + create: function(features, options) { + options = OpenLayers.Util.applyDefaults(options, this.options); + + var resp = this.createOrUpdate(features); + resp.requestType = "create"; + + if (options && options.callback) { + options.callback.call(options.scope, resp); + } + + return resp; + }, + + /** + * Method: update + * Construct a request updating modified feature. + * + * Parameters: + * features - {Array({})} or + * {} The features to update in + * the database. + * options - {Object} Optional object for configuring the request. + * + * Returns: + * {} An + * object. + */ + update: function(features, options) { + options = OpenLayers.Util.applyDefaults(options, this.options); + + var resp = this.createOrUpdate(features); + resp.requestType = "update"; + + if (options && options.callback) { + options.callback.call(options.scope, resp); + } + + return resp; + }, + + /** + * Method: createOrUpdate + * Construct a request for updating or creating features in the + * database. + * + * Parameters: + * features - {Array({})} or + * {} The feature to create or update + * in the database. + * + * Returns: + * {} An + * object. + */ + createOrUpdate: function(features) { + if (!(features instanceof Array)) { + features = [features]; + } + + var i, len = features.length, feature; + var insertedFeatures = new Array(len); + + for (i = 0; i < len; i++) { + feature = features[i]; + var params = this.freezeFeature(feature); + this.db.execute( + "REPLACE INTO " + this.tableName + + " (fid, geometry, properties, state)" + + " VALUES (?, ?, ?, ?)", + params); + + var clone = feature.clone(); + clone.fid = this.extractFidFromField(params[0]); + insertedFeatures[i] = clone; + } + + return new OpenLayers.Protocol.Response({ + code: OpenLayers.Protocol.Response.SUCCESS, + features: insertedFeatures, + reqFeatures: features + }); + }, + + /** + * Method: freezeFeature + * + * Parameters: + * feature - {} + * state - {String} The feature state to store in the database. + * + * Returns: + * {Array} + */ + freezeFeature: function(feature) { + // 2 notes: + // - fid might not be a string + // - getFeatureStateForFreeze needs the feature fid to it's stored + // in the feature here + feature.fid = feature.fid != null ? + "" + feature.fid : OpenLayers.Util.createUniqueID(this.FID_PREFIX); + + var geometry = feature.geometry != null ? + feature.geometry.toString() : this.NULL_GEOMETRY; + + var properties = this.jsonParser.write(feature.attributes); + + var state = this.getFeatureStateForFreeze(feature); + + return [feature.fid, geometry, properties, state]; + }, + + /** + * Method: getFeatureStateForFreeze + * Get the state of the feature to store into the database. + * + * Parameters: + * feature - {} The feature. + * + * Returns + * {String} The state + */ + getFeatureStateForFreeze: function(feature) { + var state; + if (!this.saveFeatureState) { + state = this.NULL_FEATURE_STATE; + } else if (this.createdOffline(feature)) { + // if the feature was created in offline mode, its + // state must remain INSERT + state = OpenLayers.State.INSERT; + } else { + state = feature.state; + } + return state; + }, + + /** + * Method: delete + * Delete features from the database. + * + * Parameters: + * features - {Array({})} or + * {} + * options - {Object} Optional object for configuring the request. + * This object is modified and should not be reused. + * + * Returns: + * {} An + * object. + */ + "delete": function(features, options) { + if (!(features instanceof Array)) { + features = [features]; + } + + options = OpenLayers.Util.applyDefaults(options, this.options); + + var i, len, feature; + for (i = 0, len = features.length; i < len; i++) { + feature = features[i]; + + // if saveFeatureState is set to true and if the feature wasn't created + // in offline mode we don't delete it in the database but just update + // it state column + if (this.saveFeatureState && !this.createdOffline(feature)) { + var toDelete = feature.clone(); + toDelete.fid = feature.fid; + if (toDelete.geometry) { + toDelete.geometry.destroy(); + toDelete.geometry = null; + } + toDelete.state = feature.state; + this.createOrUpdate(toDelete); + } else { + this.db.execute( + "DELETE FROM " + this.tableName + + " WHERE fid = ?", [feature.fid]); + } + } + + var resp = new OpenLayers.Protocol.Response({ + code: OpenLayers.Protocol.Response.SUCCESS, + requestType: "delete", + reqFeatures: features + }); + + if (options && options.callback) { + options.callback.call(options.scope, resp); + } + + return resp; + }, + + /** + * Method: createdOffline + * Returns true if the feature had a feature id when it was created in + * the Gears database, false otherwise; this is determined by + * checking the form of the feature's fid value. + * + * Parameters: + * feature - {} + * + * Returns: + * {Boolean} + */ + createdOffline: function(feature) { + return (typeof feature.fid == "string" && + !!(feature.fid.match(this.fidRegExp))); + }, + + /** + * Method: commit + * Go over the features and for each take action + * based on the feature state. Possible actions are create, + * update and delete. + * + * Parameters: + * features - {Array({})} + * options - {Object} Object whose possible keys are "create", "update", + * "delete", "callback" and "scope", the values referenced by the + * first three are objects as passed to the "create", "update", and + * "delete" methods, the value referenced by the "callback" key is + * a function which is called when the commit operation is complete + * using the scope referenced by the "scope" key. + * + * Returns: + * {Array({})} An array of + * objects, one per request made + * to the database. + */ + commit: function(features, options) { + var opt, resp = [], nRequests = 0, nResponses = 0; + + function callback(resp) { + if (++nResponses < nRequests) { + resp.last = false; + } + this.callUserCallback(options, resp); + } + + var feature, toCreate = [], toUpdate = [], toDelete = []; + for (var i = features.length - 1; i >= 0; i--) { + feature = features[i]; + switch (feature.state) { + case OpenLayers.State.INSERT: + toCreate.push(feature); + break; + case OpenLayers.State.UPDATE: + toUpdate.push(feature); + break; + case OpenLayers.State.DELETE: + toDelete.push(feature); + break; + } + } + if (toCreate.length > 0) { + nRequests++; + opt = OpenLayers.Util.applyDefaults( + {"callback": callback, "scope": this}, + options.create + ); + resp.push(this.create(toCreate, opt)); + } + if (toUpdate.length > 0) { + nRequests++; + opt = OpenLayers.Util.applyDefaults( + {"callback": callback, "scope": this}, + options.update + ); + resp.push(this.update(toUpdate, opt)); + } + if (toDelete.length > 0) { + nRequests++; + opt = OpenLayers.Util.applyDefaults( + {"callback": callback, "scope": this}, + options["delete"] + ); + resp.push(this["delete"](toDelete, opt)); + } + + return resp; + }, + + /** + * Method: clear + * Removes all rows of the table. + */ + clear: function() { + this.db.execute("DELETE FROM " + this.tableName); + }, + + /** + * Method: callUserCallback + * This method is called from within commit each time a request is made + * to the database, it is responsible for calling the user-supplied + * callbacks. + * + * Parameters: + * options - {Object} The map of options passed to the commit call. + * resp - {} + */ + callUserCallback: function(options, resp) { + var opt = options[resp.requestType]; + if (opt && opt.callback) { + opt.callback.call(opt.scope, resp); + } + if (resp.last && options.callback) { + options.callback.call(options.scope); + } + }, + + CLASS_NAME: "OpenLayers.Protocol.SQL.Gears" +}); diff --git a/tests/Protocol/SQL/Gears.html b/tests/Protocol/SQL/Gears.html new file mode 100644 index 0000000000..6f97f1a0b1 --- /dev/null +++ b/tests/Protocol/SQL/Gears.html @@ -0,0 +1,473 @@ + + + + + + + + diff --git a/tests/list-tests.html b/tests/list-tests.html index 86ab9a0438..9e358f9c93 100644 --- a/tests/list-tests.html +++ b/tests/list-tests.html @@ -115,6 +115,8 @@
  • Projection.html
  • Protocol.html
  • Protocol/HTTP.html
  • +
  • Protocol/SQL.html
  • +
  • Protocol/SQL/Gears.html
  • Renderer.html
  • Renderer/Canvas.html
  • Renderer/Elements.html