More trip planner testing with colors
[busui.git] / labs / openlayers / lib / OpenLayers / Protocol / SQL / Gears.js
blob:a/labs/openlayers/lib/OpenLayers/Protocol/SQL/Gears.js -> blob:b/labs/openlayers/lib/OpenLayers/Protocol/SQL/Gears.js
--- a/labs/openlayers/lib/OpenLayers/Protocol/SQL/Gears.js
+++ b/labs/openlayers/lib/OpenLayers/Protocol/SQL/Gears.js
@@ -1,1 +1,562 @@
-
+/* 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 
+ * <http://code.google.com/apis/gears/api_database.html>.
+ *
+ * The main advantage is that all the read, create, update and delete operations 
+ * can be done offline.
+ *
+ * Inherits from:
+ *  - <OpenLayers.Protocol.SQL>
+ */
+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
+     * {<OpenLayers.Format.JSON>}
+     */
+    jsonParser: null,
+
+    /**
+     * Property: wktParser
+     * {<OpenLayers.Format.WKT>}
+     */
+    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 (<OpenLayers.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
+     * <OpenLayers.Protocol.Response> 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:
+     * {<OpenLayers.Protocol.Response>} An <OpenLayers.Protocol.Response>
+     *      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:
+     * {<OpenLayers.Feature.Vector>}
+     */
+    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({<OpenLayers.Feature.Vector>})} or
+     *            {<OpenLayers.Feature.Vector>} The features to create in
+     *            the database.
+     * options - {Object} Optional object for configuring the request.
+     *
+     * Returns:
+     *  {<OpenLayers.Protocol.Response>} An <OpenLayers.Protocol.Response>
+     *          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({<OpenLayers.Feature.Vector>})} or
+     *            {<OpenLayers.Feature.Vector>} The features to update in
+     *            the database.
+     * options - {Object} Optional object for configuring the request.
+     *
+     * Returns:
+     *  {<OpenLayers.Protocol.Response>} An <OpenLayers.Protocol.Response>
+     *          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({<OpenLayers.Feature.Vector>})} or
+     *      {<OpenLayers.Feature.Vector>} The feature to create or update
+     *      in the database.
+     *
+     * Returns:
+     *  {<OpenLayers.Protocol.Response>} An <OpenLayers.Protocol.Response>
+     *          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 - {<OpenLayers.Feature.Vector>}
+     * 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 - {<OpenLayers.Feature.Vector>} 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({<OpenLayers.Feature.Vector>})} or
+     *            {<OpenLayers.Feature.Vector>}
+     * options - {Object} Optional object for configuring the request.
+     *       This object is modified and should not be reused.
+     *
+     * Returns:
+     *  {<OpenLayers.Protocol.Response>} An <OpenLayers.Protocol.Response>
+     *          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 - {<OpenLayers.Feature.Vector>}
+     *
+     * 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({<OpenLayers.Feature.Vector>})}
+     * 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({<OpenLayers.Protocol.Response>})} An array of
+     *       <OpenLayers.Protocol.Response> 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 - {<OpenLayers.Protocol.Response>}
+     */
+    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"
+});
+