/* Copyright (c) 2006-2010 by OpenLayers Contributors (see authors.txt for * full list of contributors). 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); }, /** * APIMethod: 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) { OpenLayers.Protocol.prototype.read.apply(this, arguments); 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; }, /** * APIMethod: 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; }, /** * APIMethod: 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; }, /** * APIMethod: 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))); }, /** * APIMethod: 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" });