Skip to content

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also compare across forks.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also compare across forks.
  • 3 commits
  • 17 files changed
  • 0 commit comments
  • 2 contributors
View
1 kanso/.gitignore
@@ -0,0 +1 @@
+packages
View
9 kanso/Makefile
@@ -0,0 +1,9 @@
+SHELL := /bin/bash
+
+build:
+ @interleave src/geocouch-utils.js --path lib --output geocouch-utils.js
+ @interleave src/lists/lists.js --path lib/lists --output lists.js
+ @interleave src/spatial/spatial.js --path lib/spatial --output spatial.js
+ @interleave src/views/all.js --path lib/views --output all.js
+ @interleave src/vendor/clustering.js --path lib/vendor --output clustering.js
+ @interleave src/vendor/geojson-js-utils.js --path vendor/geojson-js-utils --output geojson-utils.js
View
27 kanso/README.md
@@ -0,0 +1,27 @@
+# Helper Functions for GeoCouch
+
+Allows the geocouch-utils to be used with the [Kanso](http://kan.so) tools to make life even easier!
+
+## Using the Kanso Package
+
+If you haven't got Kanso installed, you can install it via NPM:
+
+<code>npm install -g kanso</code>
+
+Once Kanso is installed, stay in this directory (./kanso) and gather the Kanso dependencies:
+
+<code>kanso install</code>
+
+You will now be ready to use Kanso to deploy the geocouch-utils tools to any CouchDB GeoCouch database. To do this, use Kanso to push the package to the Couch database.
+
+<code>kanso push *db url*</code>
+
+And you are done - Kanso will have published to geocouch-utils to [db_url]/_design/geo
+
+## Building the Kanso Package
+
+The packages in lib/ purely wrap the code found in the ../couchapp directory, so there is no duplication. This is done through the use of the [interleave](https://github.com/DamonOehlman/interleave) build tool. In order to build the source, you will need this tool - it can be installed via NPM.
+
+<code>npm install -g interleave</code>
+
+Once interleave is installed, simply build the source by running <code>make</code>
View
12 kanso/kanso.json
@@ -0,0 +1,12 @@
+{
+ "name": "geo",
+ "version": "0.0.1",
+ "description": "Helper functions for GeoCouch - geocouch-utils",
+ "url": "https://github.com/NathanOehlman/geocouch-utils",
+ "modules": ["lib", "vendor"],
+ "load": "lib/geocouch-utils",
+ "dependencies": {
+ "modules": null,
+ "properties": null
+ }
+}
View
10 kanso/lib/geocouch-utils.js
@@ -0,0 +1,10 @@
+module.exports = {
+ lists: require('./lists/lists'),
+ spatial: require('./spatial/spatial'),
+ views: {
+ all: require('./views/all')
+ },
+ vendor: {
+ clustering: require('./vendor/clustering')
+ }
+}
View
143 kanso/lib/lists/lists.js
@@ -0,0 +1,143 @@
+exports.geojson =
+ /**
+ * This function outputs a GeoJSON FeatureCollection (compatible with
+ * OpenLayers). JSONP requests are supported as well.
+ *
+ * @author Volker Mische
+ */
+ function(head, req) {
+ var row, out, sep = '\n';
+
+ // Send the same Content-Type as CouchDB would
+ if (req.headers.Accept.indexOf('application/json')!=-1) {
+ start({"headers":{"Content-Type" : "application/json"}});
+ }
+ else {
+ start({"headers":{"Content-Type" : "text/plain"}});
+ }
+
+ if ('callback' in req.query) {
+ send(req.query['callback'] + "(");
+ }
+
+ send('{"type": "FeatureCollection", "features":[');
+ while (row = getRow()) {
+ out = JSON.stringify({type: "Feature", geometry: row.geometry,
+ properties: row.value});
+ send(sep + out);
+ sep = ',\n';
+ }
+ send("]}");
+
+ if ('callback' in req.query) {
+ send(")");
+ }
+ };
+
+
+exports.kml =
+ /**
+ * A list function that transforms a spatial view result set into a KML feed.
+ *
+ * @author Benjamin Erb
+ */
+ function(head, req) {
+ var row, out, sep = '\n';
+
+ start({"headers":{"Content-Type" : "application/vnd.google-earth.kml+xml"}});
+ send('<?xml version="1.0" encoding="UTF-8"?>\n');
+ send('<kml xmlns="http://www.opengis.net/kml/2.2">\n');
+ send('<Document>\n');
+ send('<name>GeoCouch Result - KML Feed</name>\n');
+ while (row = getRow()) {
+ if(row.geometry){
+ send('\t<Placemark>');
+ send('<name>'+row.id+'</name>');
+ send('<Point><coordinates>'+row.geometry.coordinates[0]+','+row.geometry.coordinates[1]+',0</coordinates></Point>');
+ send('</Placemark>\n');
+ }
+ }
+ send('</Document>\n');
+ send('</kml>\n');
+ };
+
+
+exports['knn-clustering'] =
+ /**
+ * A clustering algorithm that clusters object based on their proximity, producing k distinct clusters.
+ */
+ function(head, req) {
+ start({"code": 501,"headers":{"Content-Type" : "text/plain"}});
+ //TODO: implement kNN clustering list
+ }
+
+
+exports['proximity-clustering'] =
+ /**
+ * A clustering algorithm that clusters object based on their proximity, using a threshold value.
+ */
+ function(head, req) {
+
+ var g = require('vendor/clustering/ProximityCluster'),
+ row,
+ threshold =100;
+
+ start({"headers":{"Content-Type" : "application/json"}});
+ if ('callback' in req.query) send(req.query['callback'] + "(");
+
+ if('threshold' in req.query){ threshold = req.query.threshold;}
+ var pc = new g.PointCluster(parseInt(threshold));
+
+ while (row = getRow()) {
+ pc.addToClosestCluster(row.value);
+ }
+
+ send(JSON.stringify({"rows":pc.getClusters()}));
+
+ if ('callback' in req.query) send(")");
+ };
+
+
+
+exports.radius =
+ /**
+ * This will take the centroid of the bbox parameter and a supplied radius
+ * parameter in meters and filter the rectangularly shaped bounding box
+ * result set by circular radius.
+ *
+ * @author Max Ogden
+ */
+ function(head, req) {
+ var gju = require('vendor/geojson-js-utils/geojson-utils'),
+ row,
+ out,
+ radius = req.query.radius,
+ bbox = JSON.parse("[" + req.query.bbox + "]"),
+ center = gju.rectangleCentroid({
+ "type": "Polygon",
+ "coordinates": [[[bbox[0], bbox[1]], [bbox[2], bbox[3]]]]
+ }),
+ callback = req.query.callback,
+ circle = gju.drawCircle(radius, center),
+ startedOutput = false;
+
+ if (req.headers.Accept.indexOf('application/json') != -1)
+ start({"headers":{"Content-Type" : "application/json"}});
+ else
+ start({"headers":{"Content-Type" : "text/plain"}});
+
+ if ('callback' in req.query) send(req.query['callback'] + "(");
+ send('{"type": "FeatureCollection", "features":[');
+ while (row = getRow()) {
+ if (gju.pointInPolygon(row.geometry, circle)) {
+ if (startedOutput) send(",\n");
+ out = '{"type": "Feature", "geometry": ' + JSON.stringify(row.geometry) +
+ ', "properties": ' + JSON.stringify(row.value) + '}';
+ send(out);
+ startedOutput = true;
+ }
+ }
+ send("\n]};");
+ if ('callback' in req.query) send(")");
+ };
+
View
45 kanso/lib/spatial/spatial.js
@@ -0,0 +1,45 @@
+exports.geoms =
+ /**
+ * A simple spatial view that emits GeoJSON plus the original document id.
+ */
+ function(doc){
+ if(doc.geometry){
+ emit(doc.geometry, doc._id);
+ }
+ }
+
+
+exports.geomsFull =
+ /**
+ * A simple spatial view that emits the GeoJSON plus the complete document.
+ */
+ function(doc){
+ if(doc.geometry){
+ emit(doc.geometry, doc);
+ }
+ }
+
+
+exports.geomsOnly =
+ /**
+ * A simple spatial view that emits only the GeoJSON object without
+ * further values.
+ */
+ function(doc){
+ if(doc.geometry){
+ emit(doc.geometry, null);
+ }
+ }
+
+
+exports.geomsProps =
+ /**
+ * A spatial function that emits the geometry plus the value of the
+ * properties field of the document (or null if not defined).
+ */
+ function(doc){
+ if(doc.geometry){
+ emit(doc.geometry, doc.properties || null);
+ }
+ }
+
View
127 kanso/lib/vendor/clustering.js
@@ -0,0 +1,127 @@
+exports.KNNCluster =
+ /**
+ * kNN-based Clustering
+ */
+ (function() {
+ var cluster = this.cluster = {};
+
+ if (typeof module !== 'undefined' && module.exports) {
+ module.exports = cluster;
+ }
+
+ //TODO
+ })();
+
+
+exports.ProximityCluster =
+ /**
+ * Proximity based clustering
+ */
+ (function() {
+
+ var proxcluster = this.proxcluster = {};
+ if (typeof module !== 'undefined' && module.exports) {
+ module.exports = proxcluster;
+ }
+
+ var gju = require('vendor/geojson-js-utils/geojson-utils');
+
+ proxcluster.PointCluster = function(threshold){
+ this.clusters = [];
+ this.points = [];
+ this.distanceThreshold = threshold;
+
+ };
+
+ proxcluster.PointCluster.prototype.addToClosestCluster = function(point) {
+ var distance = 40000; // Some large number
+ var clusterToAddTo = null;
+ var pos = point.geometry;
+ for (var i = 0, cluster; cluster = this.clusters[i]; i++) {
+ var center = cluster.center;
+
+ if (center) {
+ var d = gju.pointDistance(center, pos)/1000; //convert to km
+ if ( d < this.distanceThreshold) {
+ distance = d;
+ clusterToAddTo = cluster;
+ }
+ }
+ }
+
+ if (clusterToAddTo) {
+ clusterToAddTo.addPoint(point);
+ } else {
+ var cluster = new proxcluster.Cluster(this);
+ cluster.addPoint(point);
+ this.clusters.push(cluster);
+ }
+ };
+
+ proxcluster.PointCluster.prototype.getClusters = function(){
+
+ var clusterdata = [];
+ for (var i = 0, cluster; cluster = this.clusters[i]; i++) {
+ clusterdata.push({"center":cluster.center, "points":cluster.getPoints(), "size": cluster.getSize()});
+ }
+ return clusterdata;
+ }
+
+ /**
+ * A cluster that contains points.
+ *
+ * @param {PointCluster} The PointCluster that this
+ * cluster is associated with.
+ * @constructor
+ * @ignore
+ */
+ proxcluster.Cluster = function (PointCluster) {
+ this.pointCluster = PointCluster;
+ this.center = null;
+ this.points = [];
+ }
+
+ /**
+ * Add a point the cluster.
+ *
+ * @param {point} The point to add.
+ * @return {boolean} True if the point was added.
+ */
+ proxcluster.Cluster.prototype.addPoint = function(point) {
+ if (!this.center) {
+ this.center = point.geometry;
+ } else {
+ var l = this.points.length + 1;
+ var lng = (this.center.coordinates[0] * (l-1) + point.geometry.coordinates[0]) / l;
+ var lat = (this.center.coordinates[1] * (l-1) + point.geometry.coordinates[1]) / l;
+ this.center = {"type": "Point", "coordinates":[lng, lat]};
+ }
+
+
+ this.points.push(point);
+
+ return true;
+ };
+
+
+ /**
+ * Returns points in the cluster
+ *
+ * @return {Array.<points>} The points
+ */
+ proxcluster.Cluster.prototype.getPoints = function() {
+ return this.points;
+ };
+
+ /**
+ * Returns number of points
+ *
+ * @return {int} number of points
+ */
+ proxcluster.Cluster.prototype.getSize = function() {
+ return this.points.length;
+ };
+
+
+ })();
+
View
106 kanso/lib/vendor/geojson-js-utils.js
@@ -0,0 +1,106 @@
+exports.geojson-utils =
+ (function() {
+ var gju = this.gju = {};
+
+ // Export the geojson object for **CommonJS**
+ if (typeof module !== 'undefined' && module.exports) {
+ module.exports = gju;
+ }
+
+ // adapted from http://www.kevlindev.com/gui/math/intersection/Intersection.js
+ gju.lineStringsIntersect = function(l1, l2) {
+ var intersects = [];
+ for (var i = 0; i <= l1.coordinates.length - 2; ++i) {
+ for (var j = 0; j <= l2.coordinates.length - 2; ++j) {
+ var a1 = {x: l1.coordinates[i][1], y: l1.coordinates[i][0]},
+ a2 = {x: l1.coordinates[i+1][1], y: l1.coordinates[i+1][0]},
+ b1 = {x: l2.coordinates[j][1], y: l2.coordinates[j][0]},
+ b2 = {x: l2.coordinates[j+1][1], y: l2.coordinates[j+1][0]},
+ ua_t = (b2.x - b1.x) * (a1.y - b1.y) - (b2.y - b1.y) * (a1.x - b1.x),
+ ub_t = (a2.x - a1.x) * (a1.y - b1.y) - (a2.y - a1.y) * (a1.x - b1.x),
+ u_b = (b2.y - b1.y) * (a2.x - a1.x) - (b2.x - b1.x) * (a2.y - a1.y);
+ if ( u_b != 0 ) {
+ var ua = ua_t / u_b,
+ ub = ub_t / u_b;
+ if ( 0 <= ua && ua <= 1 && 0 <= ub && ub <= 1 ) {
+ intersects.push({
+ 'type': 'Point',
+ 'coordinates': [a1.x + ua * (a2.x - a1.x), a1.y + ua * (a2.y - a1.y)]
+ });
+ }
+ }
+ }
+ }
+ if (intersects.length == 0) intersects = false;
+ return intersects;
+ }
+
+ // adapted from http://jsfromhell.com/math/is-point-in-poly
+ gju.pointInPolygon = function(point, polygon) {
+ var x = point.coordinates[1],
+ y = point.coordinates[0],
+ poly = polygon.coordinates[0]; //TODO: support polygons with holes
+ for (var c = false, i = -1, l = poly.length, j = l - 1; ++i < l; j = i) {
+ var px = poly[i][1], py = poly[i][0],
+ jx = poly[j][1], jy = poly[j][0];
+ if (((py <= y && y < jy) || (jy <= y && y < py)) && (x < (jx - px) * (y - py) / (jy - py) + px)) {
+ c = [point];
+ }
+ }
+ return c;
+ }
+
+ gju.numberToRadius = function(number) {
+ return number * Math.PI / 180;
+ }
+
+ gju.numberToDegree = function(number) {
+ return number * 180 / Math.PI;
+ }
+
+ // written with help from @tautologe
+ gju.drawCircle = function(radiusInMeters, centerPoint) {
+ var center = [centerPoint.coordinates[1], centerPoint.coordinates[0]],
+ dist = (radiusInMeters / 1000) / 6371, // convert meters to radiant
+ radCenter = [gju.numberToRadius(center[0]), gju.numberToRadius(center[1])],
+ steps = 15, // 15 sided circle
+ poly = [[center[0], center[1]]];
+ for (var i = 0; i < steps + 1; i++) {
+ var brng = 2 * Math.PI * i / steps;
+ var lat = Math.asin(Math.sin(radCenter[0]) * Math.cos(dist) +
+ Math.cos(radCenter[0]) * Math.sin(dist) * Math.cos(brng));
+ var lng = radCenter[1] + Math.atan2(Math.sin(brng) * Math.sin(dist) *
+ Math.cos(radCenter[0]),
+ Math.cos(dist) - Math.sin(radCenter[0]) *
+ Math.sin(lat));
+ poly[i] = [];
+ poly[i][1] = gju.numberToDegree(lat);
+ poly[i][0] = gju.numberToDegree(lng);
+ }
+ return { "type": "Polygon",
+ "coordinates": [poly] };
+ }
+
+ gju.rectangleCentroid = function(rectangle) {
+ var bbox = rectangle.coordinates[0];
+ var xmin = bbox[0][0], ymin = bbox[0][1], xmax = bbox[1][0], ymax = bbox[1][1];
+ var xwidth = xmax - xmin;
+ var ywidth = ymax - ymin;
+ return { 'type': 'Point',
+ 'coordinates': [xmin + xwidth/2, ymin + ywidth/2] };
+ }
+
+ // from http://www.movable-type.co.uk/scripts/latlong.html
+ gju.pointDistance = function(pt1, pt2) {
+ var lon1 = pt1.coordinates[0], lat1 = pt1.coordinates[1],
+ lon2 = pt2.coordinates[0], lat2 = pt2.coordinates[1],
+ dLat = gju.numberToRadius(lat2 - lat1),
+ dLon = gju.numberToRadius(lon2 - lon1),
+ a = Math.sin(dLat/2) * Math.sin(dLat/2) +
+ Math.cos(gju.numberToRadius(lat1)) * Math.cos(gju.numberToRadius(lat2)) *
+ Math.sin(dLon/2) * Math.sin(dLon/2),
+ c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
+ return (6371 * c) * 1000; // returns meters
+ }
+
+ })();
View
9 kanso/lib/views/all.js
@@ -0,0 +1,9 @@
+exports.map =
+ /**
+ * A simple map function mocking _all, but allows usage with lists etc.
+ *
+ */
+ function(doc) {
+ emit(doc.id, doc);
+ }
+
View
10 kanso/src/geocouch-utils.js
@@ -0,0 +1,10 @@
+module.exports = {
+ lists: require('./lists/lists'),
+ spatial: require('./spatial/spatial'),
+ views: {
+ all: require('./views/all')
+ },
+ vendor: {
+ clustering: require('./vendor/clustering')
+ }
+}
View
14 kanso/src/lists/lists.js
@@ -0,0 +1,14 @@
+exports.geojson =
+ //= ../../../couchapp/lists/geojson.js
+
+exports.kml =
+ //= ../../../couchapp/lists/kml.js
+
+exports['knn-clustering'] =
+ //= ../../../couchapp/lists/knn-clustering.js
+
+exports['proximity-clustering'] =
+ //= ../../../couchapp/lists/proximity-clustering.js
+
+exports.radius =
+ //= ../../../couchapp/lists/radius.js
View
11 kanso/src/spatial/spatial.js
@@ -0,0 +1,11 @@
+exports.geoms =
+ //= ../../../couchapp/spatial/geoms.js
+
+exports.geomsFull =
+ //= ../../../couchapp/spatial/geomsFull.js
+
+exports.geomsOnly =
+ //= ../../../couchapp/spatial/geomsOnly.js
+
+exports.geomsProps =
+ //= ../../../couchapp/spatial/geomsProps.js
View
5 kanso/src/vendor/clustering.js
@@ -0,0 +1,5 @@
+exports.KNNCluster =
+ //= ../../../couchapp/vendor/clustering/KNNCluster.js
+
+exports.ProximityCluster =
+ //= ../../../couchapp/vendor/clustering/ProximityCluster.js
View
1 kanso/src/vendor/geojson-js-utils.js
@@ -0,0 +1 @@
+//= ../../../couchapp/vendor/geojson-js-utils/geojson-utils.js
View
2 kanso/src/views/all.js
@@ -0,0 +1,2 @@
+exports.map =
+ //= ../../../couchapp/views/all/map.js
View
105 kanso/vendor/geojson-js-utils/geojson-utils.js
@@ -0,0 +1,105 @@
+(function() {
+ var gju = this.gju = {};
+
+ // Export the geojson object for **CommonJS**
+ if (typeof module !== 'undefined' && module.exports) {
+ module.exports = gju;
+ }
+
+ // adapted from http://www.kevlindev.com/gui/math/intersection/Intersection.js
+ gju.lineStringsIntersect = function(l1, l2) {
+ var intersects = [];
+ for (var i = 0; i <= l1.coordinates.length - 2; ++i) {
+ for (var j = 0; j <= l2.coordinates.length - 2; ++j) {
+ var a1 = {x: l1.coordinates[i][1], y: l1.coordinates[i][0]},
+ a2 = {x: l1.coordinates[i+1][1], y: l1.coordinates[i+1][0]},
+ b1 = {x: l2.coordinates[j][1], y: l2.coordinates[j][0]},
+ b2 = {x: l2.coordinates[j+1][1], y: l2.coordinates[j+1][0]},
+ ua_t = (b2.x - b1.x) * (a1.y - b1.y) - (b2.y - b1.y) * (a1.x - b1.x),
+ ub_t = (a2.x - a1.x) * (a1.y - b1.y) - (a2.y - a1.y) * (a1.x - b1.x),
+ u_b = (b2.y - b1.y) * (a2.x - a1.x) - (b2.x - b1.x) * (a2.y - a1.y);
+ if ( u_b != 0 ) {
+ var ua = ua_t / u_b,
+ ub = ub_t / u_b;
+ if ( 0 <= ua && ua <= 1 && 0 <= ub && ub <= 1 ) {
+ intersects.push({
+ 'type': 'Point',
+ 'coordinates': [a1.x + ua * (a2.x - a1.x), a1.y + ua * (a2.y - a1.y)]
+ });
+ }
+ }
+ }
+ }
+ if (intersects.length == 0) intersects = false;
+ return intersects;
+ }
+
+ // adapted from http://jsfromhell.com/math/is-point-in-poly
+ gju.pointInPolygon = function(point, polygon) {
+ var x = point.coordinates[1],
+ y = point.coordinates[0],
+ poly = polygon.coordinates[0]; //TODO: support polygons with holes
+ for (var c = false, i = -1, l = poly.length, j = l - 1; ++i < l; j = i) {
+ var px = poly[i][1], py = poly[i][0],
+ jx = poly[j][1], jy = poly[j][0];
+ if (((py <= y && y < jy) || (jy <= y && y < py)) && (x < (jx - px) * (y - py) / (jy - py) + px)) {
+ c = [point];
+ }
+ }
+ return c;
+ }
+
+ gju.numberToRadius = function(number) {
+ return number * Math.PI / 180;
+ }
+
+ gju.numberToDegree = function(number) {
+ return number * 180 / Math.PI;
+ }
+
+ // written with help from @tautologe
+ gju.drawCircle = function(radiusInMeters, centerPoint) {
+ var center = [centerPoint.coordinates[1], centerPoint.coordinates[0]],
+ dist = (radiusInMeters / 1000) / 6371, // convert meters to radiant
+ radCenter = [gju.numberToRadius(center[0]), gju.numberToRadius(center[1])],
+ steps = 15, // 15 sided circle
+ poly = [[center[0], center[1]]];
+ for (var i = 0; i < steps + 1; i++) {
+ var brng = 2 * Math.PI * i / steps;
+ var lat = Math.asin(Math.sin(radCenter[0]) * Math.cos(dist) +
+ Math.cos(radCenter[0]) * Math.sin(dist) * Math.cos(brng));
+ var lng = radCenter[1] + Math.atan2(Math.sin(brng) * Math.sin(dist) *
+ Math.cos(radCenter[0]),
+ Math.cos(dist) - Math.sin(radCenter[0]) *
+ Math.sin(lat));
+ poly[i] = [];
+ poly[i][1] = gju.numberToDegree(lat);
+ poly[i][0] = gju.numberToDegree(lng);
+ }
+ return { "type": "Polygon",
+ "coordinates": [poly] };
+ }
+
+ gju.rectangleCentroid = function(rectangle) {
+ var bbox = rectangle.coordinates[0];
+ var xmin = bbox[0][0], ymin = bbox[0][1], xmax = bbox[1][0], ymax = bbox[1][1];
+ var xwidth = xmax - xmin;
+ var ywidth = ymax - ymin;
+ return { 'type': 'Point',
+ 'coordinates': [xmin + xwidth/2, ymin + ywidth/2] };
+ }
+
+ // from http://www.movable-type.co.uk/scripts/latlong.html
+ gju.pointDistance = function(pt1, pt2) {
+ var lon1 = pt1.coordinates[0], lat1 = pt1.coordinates[1],
+ lon2 = pt2.coordinates[0], lat2 = pt2.coordinates[1],
+ dLat = gju.numberToRadius(lat2 - lat1),
+ dLon = gju.numberToRadius(lon2 - lon1),
+ a = Math.sin(dLat/2) * Math.sin(dLat/2) +
+ Math.cos(gju.numberToRadius(lat1)) * Math.cos(gju.numberToRadius(lat2)) *
+ Math.sin(dLon/2) * Math.sin(dLon/2),
+ c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
+ return (6371 * c) * 1000; // returns meters
+ }
+
+})();

No commit comments for this range

Something went wrong with that request. Please try again.