geojson-tools
Advanced tools
Comparing version 0.1.5 to 0.1.6
@@ -0,1 +1,7 @@ | ||
0.1.6 / 2014-06-13 | ||
================== | ||
* Add support for making a `LineString` or array of coordinates complex by reducing the maximum distance between each points | ||
* [WIP] Add support for some additional GeoJSON objects. This has not been tested, and has thus not yet been documented. | ||
0.1.5 / 2014-03-21 | ||
@@ -2,0 +8,0 @@ ================== |
{ | ||
"name": "geojson-tools", | ||
"version": "0.1.5", | ||
"version": "0.1.6", | ||
"description": "Tools for working with location data, using the GeoJSON specification", | ||
@@ -27,3 +27,7 @@ "main": "index.js", | ||
"underscore": "^1.6.0" | ||
}, | ||
"devDependencies": { | ||
"mocha": "1.18.*", | ||
"chai": "~1.9.1" | ||
} | ||
} |
184
README.md
@@ -22,2 +22,3 @@ # geojson-tools.js | ||
* [getDistance](#getDistance) | ||
* [complexify](#complexify) | ||
@@ -55,2 +56,185 @@ ### Conversions | ||
<a name="complexify" /> | ||
### complexify(linestring, distance) | ||
Convert `LineString` or array of coordinates to a 'complex' line with specified maximum distance between each set of points. | ||
__Argumenta"" | ||
* linestring - a valid GeoJSON `LineString`, or an array of locations, in the format `[lat, lng]`. | ||
* distance - the maximum distance between each two locations, in meters. | ||
__Example__ | ||
```js | ||
var array = [ | ||
[20, 30], | ||
[20.5, 29.5] | ||
]; | ||
complexify(array, .5); // 500 meters | ||
/* | ||
[ | ||
[ | ||
20, | ||
30 | ||
], | ||
[ | ||
20.01374457881841, | ||
29.98625542118159 | ||
], | ||
[ | ||
20.02749358953798, | ||
29.97250641046202 | ||
], | ||
[ | ||
20.04124228409795, | ||
29.95875771590205 | ||
], | ||
[ | ||
20.054991756377518, | ||
29.945008243622482 | ||
], | ||
[ | ||
20.068742346786582, | ||
29.931257653213418 | ||
], | ||
[ | ||
20.082493595207826, | ||
29.917506404792174 | ||
], | ||
[ | ||
20.09624419012715, | ||
29.90375580987285 | ||
], | ||
[ | ||
20.109998454446686, | ||
29.890001545553314 | ||
], | ||
[ | ||
20.1237529737179, | ||
29.8762470262821 | ||
], | ||
[ | ||
20.13750307275061, | ||
29.86249692724939 | ||
], | ||
[ | ||
20.151256033129513, | ||
29.848743966870487 | ||
], | ||
[ | ||
20.165010879444292, | ||
29.834989120555708 | ||
], | ||
[ | ||
20.17876504224938, | ||
29.82123495775062 | ||
], | ||
[ | ||
20.192520769690717, | ||
29.807479230309283 | ||
], | ||
[ | ||
20.206277898630216, | ||
29.793722101369784 | ||
], | ||
[ | ||
20.22003390658687, | ||
29.77996609341313 | ||
], | ||
[ | ||
20.233790231590877, | ||
29.766209768409123 | ||
], | ||
[ | ||
20.247544863923423, | ||
29.752455136076577 | ||
], | ||
[ | ||
20.261305062529242, | ||
29.738694937470758 | ||
], | ||
[ | ||
20.275060711787024, | ||
29.724939288212976 | ||
], | ||
[ | ||
20.288822447172574, | ||
29.711177552827426 | ||
], | ||
[ | ||
20.3025819531983, | ||
29.6974180468017 | ||
], | ||
[ | ||
20.3163428239271, | ||
29.6836571760729 | ||
], | ||
[ | ||
20.330106243909288, | ||
29.669893756090712 | ||
], | ||
[ | ||
20.34386828218919, | ||
29.65613171781081 | ||
], | ||
[ | ||
20.357630509669633, | ||
29.642369490330367 | ||
], | ||
[ | ||
20.37139577415928, | ||
29.62860422584072 | ||
], | ||
[ | ||
20.385162033041937, | ||
29.614837966958063 | ||
], | ||
[ | ||
20.398927945160654, | ||
29.601072054839346 | ||
], | ||
[ | ||
20.412692053486403, | ||
29.587307946513597 | ||
], | ||
[ | ||
20.426458446138625, | ||
29.573541553861375 | ||
], | ||
[ | ||
20.4402208587639, | ||
29.5597791412361 | ||
], | ||
[ | ||
20.45398897904669, | ||
29.54601102095331 | ||
], | ||
[ | ||
20.467757803444808, | ||
29.532242196555192 | ||
], | ||
[ | ||
20.481524285475142, | ||
29.518475714524858 | ||
], | ||
[ | ||
20.49529252216346, | ||
29.50470747783654 | ||
], | ||
[ | ||
20.5, | ||
29.5 | ||
] | ||
]; | ||
*/ | ||
distance of 'simple' linestring: 76.321 km | ||
distance of 'complex' linesting: 76.321 km | ||
``` | ||
<b>Note:</b> The algorithms works relatively well, and will return results that are > 99.9% accurate over short distances. Specifying distances > 500 km might return undesireable results. | ||
We have also not added the option to specify precision, and precision is set at the default rounding of 3 decimals, this is to avoid infinite loops. | ||
## Conversions | ||
@@ -57,0 +241,0 @@ |
@@ -19,4 +19,7 @@ /** | ||
var toGeoJSON = function (array, type) { | ||
var georesult = {}; | ||
if ( !type ) { | ||
var georesult = {}, | ||
arr, | ||
error, | ||
nested; | ||
if (!type) { | ||
type = "point"; | ||
@@ -26,9 +29,9 @@ } | ||
switch (type) { | ||
case 'point': | ||
var _array = _.flatten(array); | ||
var _a = JSON.stringify(array) || {}; | ||
if (_array.length !== 2) { | ||
return new Error("expected a single set of coordinates in [lat, lng] format. \nReceived " + _a); | ||
} | ||
var arr = [_array[1], _array[0]]; | ||
case 'point': | ||
var _array = _.flatten(array); | ||
var _a = JSON.stringify(array) || {}; | ||
if (_array.length !== 2) { | ||
error = new Error("expected a single set of coordinates in [lat, lng] format. \nReceived " + _a); | ||
} else { | ||
arr = [parseFloat(_array[1]), parseFloat(_array[0])]; | ||
georesult = { | ||
@@ -38,37 +41,105 @@ type: "Point", | ||
}; | ||
return georesult; | ||
} | ||
break; | ||
case 'linestring': | ||
var arr = []; | ||
_.each(array, function (a) { | ||
arr.push([a[1], a[0]]); | ||
case 'linestring': | ||
case 'multipoint': | ||
arr = []; | ||
_.each(array, function (a) { | ||
arr.push([parseFloat(a[1]), parseFloat(a[0])]); | ||
}); | ||
if (type === 'linestring') { | ||
type = 'LineString'; | ||
} else if (type === 'MultiPoint') { | ||
type = 'MultiPoint'; | ||
} | ||
georesult = { | ||
type: type, | ||
coordinates: arr | ||
}; | ||
break; | ||
case 'polygon': | ||
if (_.isArray(array) && _.isArray(array[0]) && !_.isArray(array[0][0])) { | ||
array = [array]; | ||
} | ||
arr = []; | ||
_.find(array, function (_array) { | ||
if (_array.length < 3) { | ||
error = new Error("Expecting 'array' to have at length of at least 3 sets of coordinates."); | ||
return true; | ||
} | ||
if (_array.toString() !== _.last(_array).toString()) { | ||
_array.push(_array[0]); | ||
} | ||
nested = []; | ||
_.each(_array, function (a) { | ||
nested.push([parseFloat(a[1]), parseFloat(a[0])]); | ||
}); | ||
arr.push(nested); | ||
return false; | ||
}); | ||
if (!error) { | ||
georesult = { | ||
type: "LineString", | ||
coordinates: _.uniq(arr, true, function (x) { | ||
return JSON.stringify(x) ; | ||
}) | ||
type: "Polygon", | ||
coordinates: arr | ||
}; | ||
return georesult; | ||
} | ||
break; | ||
case 'polygon': | ||
var arr = [[]]; | ||
if (array.length < 3) { | ||
return new Error("Expecting 'array' to have at length of at least 3 sets of coordinates."); | ||
case 'multilinestring': | ||
arr = []; | ||
// validate multilinestring | ||
_.find(array, function (a) { | ||
if (a.length < 2 && !error) { | ||
error = new Error("Expecting each LineString in MultiiLineString to have at least 2 points."); | ||
return true; | ||
} | ||
if (array[0].toString() !== _.last(array).toString()) { | ||
array.push(array[0]); | ||
} | ||
_.each(array, function (a) { | ||
arr[0].push([a[1], a[0]]); | ||
nested = []; | ||
_.each(a, function (_a) { | ||
nested.push([parseFloat(_a[1]), parseFloat(_a[0])]); | ||
}); | ||
arr.push(nested); | ||
return false; | ||
}); | ||
if (!error) { | ||
georesult = { | ||
type: "Polygon", | ||
type: "MultiLineString", | ||
coordinates: arr | ||
}; | ||
return georesult; | ||
} | ||
break; | ||
default: | ||
return new Error("type not recognised. Should be 'point', 'linestring', or 'polygon'"); | ||
case 'multipolygon': | ||
arr = []; | ||
var outer, | ||
inner; | ||
_.find(array, function (a) { | ||
outer = []; | ||
_.find(a, function (_a) { | ||
inner = []; | ||
if (_a.length < 3) { | ||
error = new Error("Expecting each array in MultiPolygon to have at least 3 points."); | ||
return true; | ||
} | ||
_.find(_a, function (__a) { | ||
inner.push([parseFloat(__a[1]), parseFloat(__a[0])]); | ||
return false; | ||
}); | ||
outer.push(inner); | ||
return false; | ||
}); | ||
arr.push(outer); | ||
return false; | ||
}); | ||
if (!error) { | ||
georesult = { | ||
type: "MultiPolygon", | ||
coordinates: arr | ||
}; | ||
} | ||
break; | ||
default: | ||
error = new Error("type not recognised. Should be 'point', 'linestring', or 'polygon'"); | ||
} | ||
if (error) { | ||
return error; | ||
} | ||
return georesult; | ||
}; | ||
@@ -82,2 +153,4 @@ | ||
var toArray = function (geoobj) { | ||
var array, | ||
error; | ||
if (!geoobj.type || !geoobj.coordinates) { | ||
@@ -87,42 +160,57 @@ return new Error("The object specified is not a a valid GeoJSON object"); | ||
switch (geoobj.type.toLowerCase()) { | ||
case 'point': | ||
var point = [geoobj.coordinates[1],geoobj.coordinates[0]]; | ||
return point; | ||
case 'point': | ||
array = [parseFloat(geoobj.coordinates[1]), parseFloat(geoobj.coordinates[0])]; | ||
break; | ||
case 'linestring': | ||
// check if it is a valid line | ||
var array = geoobj.coordinates; | ||
var line = []; | ||
_.each(array, function (ln) { | ||
if (typeof ln === 'object') { | ||
line.push([ln[1], ln[0]]); | ||
} | ||
else { | ||
return new Error("the object specified is not a valid GeoJSON LineString"); | ||
} | ||
}); | ||
return line; | ||
case 'linestring': | ||
// check if it is a valid line | ||
array = geoobj.coordinates; | ||
var line = []; | ||
_.find(array, function (ln) { | ||
if (typeof ln === 'object') { | ||
line.push([parseFloat(ln[1]), parseFloat(ln[0])]); | ||
return false; | ||
} | ||
error = new Error("the object specified is not a valid GeoJSON LineString"); | ||
return true; | ||
}); | ||
if (!error) { | ||
array = line; | ||
} | ||
break; | ||
case 'polygon': | ||
var array = geoobj.coordinates[0]; | ||
if (!array.length) { | ||
return new Error("the object specified is not a valid GeoJSON Polygon"); | ||
case 'polygon': | ||
var poly; | ||
array = []; | ||
// check if valid object | ||
_.find(geoobj.coordinates, function (a) { | ||
if (!a.length) { | ||
error = new Error("the object specified is not a valid GeoJSON Polygon"); | ||
return true; | ||
} | ||
var poly = []; | ||
_.each(array, function (a) { | ||
if (a[0].toString() !== _.last(a).toString()) { | ||
return new Error("The first and last coordinates of a Polygon are not the same"); | ||
} | ||
if (a.length < 4) { | ||
return new Error("A valid Polygon should have a minimum of 4 coordinates"); | ||
} | ||
_.each(_.initial(a), function (pl) { | ||
poly.push([pl[1], pl[0]]); | ||
}); | ||
poly = []; | ||
if (a[0].toString() !== _.last(a).toString()) { | ||
error = new Error("The first and last coordinates of the Polygon are not the same"); | ||
return true; | ||
} | ||
if (a.length < 4) { | ||
error = new Error("A valid Polygon should have a minimum set of 4 points"); | ||
return true; | ||
} | ||
_.each(_.initial(a), function (pl) { | ||
poly.push([parseFloat(pl[1]), parseFloat(pl[0])]); | ||
}); | ||
return poly; | ||
array.push(poly); | ||
if (!error) { | ||
array = poly; | ||
} | ||
return false; | ||
}); | ||
break; | ||
default: | ||
return new Error("unknown GeoJSON type specified"); | ||
default: | ||
error = new Error("unknown GeoJSON type specified"); | ||
} | ||
if (error) { | ||
return error; | ||
} | ||
return array; | ||
}; | ||
@@ -137,4 +225,4 @@ | ||
var getDistance = function (array, decimals) { | ||
if(typeof(Number.prototype.toRad) === "undefined") { | ||
Number.prototype.toRad = function () { | ||
if (Number.prototype.toRad === undefined) { | ||
Number.prototype.toRad = function () { | ||
return this * Math.PI / 180; | ||
@@ -147,21 +235,33 @@ }; | ||
distance = 0, | ||
len = array.length; | ||
for (var i = 0; (i + 1) < len; i++) { | ||
var x1 = array[i]; | ||
var x2 = array[i + 1]; | ||
len = array.length, | ||
i, | ||
x1, | ||
x2, | ||
lat1, | ||
lat2, | ||
lon1, | ||
lon2, | ||
dLat, | ||
dLon, | ||
a, | ||
c, | ||
d; | ||
for (i = 0; (i + 1) < len; i++) { | ||
x1 = array[i]; | ||
x2 = array[i + 1]; | ||
var lat1 = parseFloat(x1[0]); | ||
var lat2 = parseFloat(x2[0]); | ||
var lon1 = parseFloat(x1[1]); | ||
var lon2 = parseFloat(x2[1]); | ||
lat1 = parseFloat(x1[0]); | ||
lat2 = parseFloat(x2[0]); | ||
lon1 = parseFloat(x1[1]); | ||
lon2 = parseFloat(x2[1]); | ||
var dLat = (lat2 - lat1).toRad(); | ||
var dLon = (lon2 - lon1).toRad(); | ||
dLat = (lat2 - lat1).toRad(); | ||
dLon = (lon2 - lon1).toRad(); | ||
lat1 = lat1.toRad(); | ||
lat2 = lat2.toRad(); | ||
var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + | ||
a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + | ||
Math.sin(dLon / 2) * Math.sin(dLon / 2) * Math.cos(lat1) * Math.cos(lat2); | ||
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); | ||
var d = earthRadius * c; | ||
c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); | ||
d = earthRadius * c; | ||
distance += d; | ||
@@ -173,2 +273,105 @@ } | ||
/** | ||
* takes a `LineString` or an array of coordinates, and returns one with more coordinates | ||
+ having the distance parameter as the maximum distance between each set of coordinates. | ||
* | ||
* @param {Object} either a valid LineString, or an array of coordinates | ||
* @param {Number} maximum distance in meters between points | ||
* @returns {Object} either a LineString or array of coordinates | ||
*/ | ||
var complexify = function (linestring, distance) { | ||
if (!_.isNumber(distance) || distance < 0.10) { | ||
console.error(new Error('distance should be a number greater than 10 meters')); | ||
return null; | ||
} | ||
var returnGeoJSON; | ||
if (!_.isArray(linestring)) { | ||
// check if it is a GeoJSON LineString and convert it | ||
if (linestring.coordinates && _.isArray(linestring.coordinates) && linestring.type.toLowerCase() === 'linestring') { | ||
returnGeoJSON = true; | ||
linestring = toArray(linestring); | ||
} else { | ||
console.error(new Error('distance should be a number greater than 10 meters')); | ||
return null; | ||
} | ||
} | ||
var t, // threshold | ||
cur, | ||
prev, // temporary points | ||
result = [], | ||
points = [], | ||
d; | ||
cur = linestring.shift(); | ||
result.push(cur); | ||
points.push(cur); | ||
// variables used in the loop | ||
var reasonable, | ||
ratio, | ||
_d, | ||
totalDistance, | ||
a, | ||
b, | ||
bearing, | ||
c; | ||
_.each(linestring, function (point) { | ||
prev = cur; | ||
cur = point; | ||
d = getDistance([cur, prev]); | ||
if (d > distance) { | ||
t = 0; | ||
reasonable = false; | ||
// estimate where distance could be, then perform a binary search to find the best-fit coordinates | ||
ratio = distance / d; | ||
_d = distance; | ||
totalDistance = 0; | ||
while (!reasonable || (totalDistance + distance < d)) { | ||
a = _.last(result); | ||
b = cur; | ||
bearing = []; | ||
if (a[0] > b[0]) { | ||
bearing.push(-1); | ||
} else { | ||
bearing.push(1); | ||
} | ||
if (a[1] > b[1]) { | ||
bearing.push(-1); | ||
} else { | ||
bearing.push(1); | ||
} | ||
c = [(b[0] - a[0]) * ratio * bearing[0], (b[1] - a[1]) * ratio * bearing[1]]; | ||
// console.log(c); | ||
if (bearing[0] > 0) { | ||
c[0] = c[0] + a[0]; | ||
} else { | ||
c[0] = a[0] - c[0]; | ||
} | ||
if (bearing[1] > 0) { | ||
c[1] = c[1] + a[1]; | ||
} else { | ||
c[1] = a[1] - c[1]; | ||
} | ||
bearing = []; | ||
_d = getDistance([a, c]); | ||
if (_d !== distance) { | ||
t = (_d / distance); // goalseek threshold and search for next nearest point | ||
ratio = ratio + (ratio * (1 - t)); | ||
} else { | ||
totalDistance += distance; | ||
reasonable = true; | ||
result.push(c); | ||
} | ||
} | ||
totalDistance = 0; | ||
reasonable = false; | ||
result.push(cur); | ||
} else { | ||
result.push(cur); | ||
} | ||
}); | ||
if (returnGeoJSON) { | ||
return toGeoJSON(result, 'linestring'); | ||
} | ||
return result; | ||
}; | ||
/* | ||
@@ -179,2 +382,3 @@ * Export functions | ||
exports.toArray = toArray; | ||
exports.getDistance = getDistance; | ||
exports.getDistance = getDistance; | ||
exports.complexify = complexify; |
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
20036
363
333
2
1