Skip to content

Commit

Permalink
Update tidy logic (#18)
Browse files Browse the repository at this point in the history
* compare to last selected point rather than last input point to fix filtering bug

* set minimum time/distances to 0

* remove logging

* mock Math.random() and explicitly set filters

* update test files to match new logic

* update test comment

* update test comment

* remove unneeeded comment block

* fix slice index

* remove unneccessary statements

* remove test curl command

* speed up random selection step and update test accordingly

* bump version number

* add RC tag

* npm pkg fix

* update version number to 2.0.0
  • Loading branch information
dpaddon authored Jun 10, 2024
1 parent f1ce812 commit 30d94d9
Show file tree
Hide file tree
Showing 6 changed files with 1,996 additions and 106 deletions.
80 changes: 49 additions & 31 deletions geojson-tidy.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@ var haversine = require('haversine');

module.exports.tidy = tidy;

function shuffle(arr) {
// Fisher-Yates shuffle
for (let i = arr.length -1; i > 0; i--) {
let j = Math.floor(Math.random() * (i+1));
let k = arr[i];
arr[i] = arr[j];
arr[j] = k;
}
return arr;
}

// Public function

function tidy(geojson, options) {
Expand All @@ -10,11 +21,10 @@ function tidy(geojson, options) {

// Set the minimum distance in metres and time interval in seconds between successive coordinates
var filter = {
minimumDistance: options.minimumDistance || 10,
minimumTime: options.minimumTime || 5,
minimumDistance: options.minimumDistance || 0,
minimumTime: options.minimumTime || 0,
maximumPoints: options.maximumPoints || 100
};

// Create the tidy output feature collection
var tidyOutput = {
"type": "FeatureCollection",
Expand All @@ -23,7 +33,7 @@ function tidy(geojson, options) {
var emptyFeature = {
"type": "Feature",
"properties": {
"coordTimes": []
"coordTimes": [],
},
"geometry": {
"type": "LineString",
Expand Down Expand Up @@ -52,26 +62,24 @@ function tidy(geojson, options) {
tidyOutput.features.push(clone(emptyFeature));

// Loop through the coordinate array of the noisy linestring and build a tidy linestring
var keepIdxs = [];

for (var i = 0; i < lineString.length; i++) {

// Add first and last points
if (i === 0 || i == lineString.length - 1) {
tidyOutput.features[tidyOutput.features.length - 1].geometry.coordinates.push(lineString[i]);
if (timeStamp) {
tidyOutput.features[tidyOutput.features.length - 1].properties.coordTimes.push(timeStamp[i]);
}
keepIdxs.push(i);
continue;
}

// Calculate distance between successive points in metres
// Calculate distance between this point and the last point we included
var point1 = {
latitude: lineString[i][1],
longitude: lineString[i][0]
latitude: lineString[keepIdxs[keepIdxs.length - 1]][1],
longitude: lineString[keepIdxs[keepIdxs.length - 1]][0]
};
var point2 = {
latitude: lineString[i + 1][1],
longitude: lineString[i + 1][0]
latitude: lineString[i][1],
longitude: lineString[i][0]
};

var Dx = haversine(point1, point2, {
Expand All @@ -86,8 +94,8 @@ function tidy(geojson, options) {
// Calculate sampling time diference between successive points in seconds
if (timeStamp) {

var time1 = new Date(timeStamp[i]);
var time2 = new Date(timeStamp[i + 1]);
var time1 = new Date(timeStamp[keepIdxs[keepIdxs.length - 1]]);
var time2 = new Date(timeStamp[i]);

var Tx = (time2 - time1) / 1000;

Expand All @@ -97,26 +105,36 @@ function tidy(geojson, options) {
}

}
keepIdxs.push(i)
}

// Copy the point and timestamp to the tidyOutput
tidyOutput.features[tidyOutput.features.length - 1].geometry.coordinates.push(lineString[i]);
if (timeStamp) {
tidyOutput.features[tidyOutput.features.length - 1].properties.coordTimes.push(timeStamp[i]);
}

// If feature exceeds maximum points, start a new feature beginning at the previuos end point
if (tidyOutput.features[tidyOutput.features.length - 1].geometry.coordinates.length % filter.maximumPoints === 0) {
tidyOutput.features.push(clone(emptyFeature));
tidyOutput.features[tidyOutput.features.length - 1].geometry.coordinates.push(lineString[i]);
if (timeStamp) {
tidyOutput.features[tidyOutput.features.length - 1].properties.coordTimes.push(timeStamp[i]);
}
}
// If we have > maximumPoints points, take a random sample.
// Otherwise just return the entire set
if (keepIdxs.length > filter.maximumPoints) {
// Randomly remove points until we hit maxLength

// Split off the first and last indices as we always want to keep these
const firstIdx = keepIdxs[0]
const lastIdx = keepIdxs[keepIdxs.length - 1]
keepIdxs = keepIdxs.slice(1, -1)

// Shuffle the array and take the number of points we want
keepIdxs = shuffle(keepIdxs).slice(0, filter.maximumPoints - 2)

// Add back the first/last points
keepIdxs = [firstIdx, ...keepIdxs, lastIdx]
}
}

// DEBUG
// console.log(JSON.stringify(tidyOutput));
// Now that we know which indices we want to keep, save the corresponding points
keepIdxs.forEach(function (item, index) {
tidyOutput.features[tidyOutput.features.length - 1].geometry.coordinates.push(lineString[item]);
if (timeStamp) {
tidyOutput.features[tidyOutput.features.length - 1].properties.coordTimes.push(timeStamp[item]);
}
});

}

// Your tidy geojson is served
return tidyOutput;
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"name": "@mapbox/geojson-tidy",
"version": "1.0.1",
"version": "2.0.0",
"description": "tidy up a geojson input by filtering out noisy point clusters",
"main": "geojson-tidy.js",
"directories": {
"test": "test"
},
"repository": {
"type": "git",
"url": "[email protected]:mapbox/geojson-tidy.git"
"url": "git+ssh://git@github.com/mapbox/geojson-tidy.git"
},
"scripts": {
"test": "tape test/test.js",
Expand Down Expand Up @@ -37,4 +37,4 @@
"path": "./node_modules/cz-conventional-changelog"
}
}
}
}
34 changes: 24 additions & 10 deletions test/test.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,38 @@
var geojsonTidy = require('../'),
test = require('tape');
test = require('tape');

// Mock Math.random() to return a fixed value so we get repeatable sampling
const mockMath = Object.create(global.Math);
mockMath.random = () => 0.5;
global.Math = mockMath;

test('geojson tidy', function (t) {

t.test('Process a feature collection without timestamps', function (t) {
t.deepEqual (
geojsonTidy.tidy(require('./walk-2.json')),
t.deepEqual(
geojsonTidy.tidy(require('./walk-2.json'), {
minimumDistance: 10,
minimumTime: 5,
maximumPoints: 500 // Set this high to avoid random sampling which confuses comparisons
}),
require('./walk-2-tidy.json')
);
t.end();
});

t.test('Process a feature collection with timestamps', function (t) {
t.deepEqual(
geojsonTidy.tidy(require('./walk-1.json')),
geojsonTidy.tidy(require('./walk-1.json'), {
minimumDistance: 10,
minimumTime: 5,
maximumPoints: 500 // Set this high to avoid random sampling which confuses comparisons
}),
require('./walk-1-tidy.json')
);
t.end();
});

t.test('Process a feature collection with custom minimumDistance minimumTime and maximumPoints', function (t) {
t.test('Process a feature collection with custom minimumDistance, minimumTime and maximumPoints', function (t) {
t.deepEqual(
geojsonTidy.tidy(require('./walk-1.json'), {
"minimumDistance": 20,
Expand All @@ -33,14 +46,15 @@ test('geojson tidy', function (t) {

t.test('Process a feature collection with multiple features', function (t) {
t.deepEqual(
JSON.stringify(geojsonTidy.tidy(require('./cross-country.json'))),
JSON.stringify(geojsonTidy.tidy(require('./cross-country.json'), {
minimumDistance: 10,
minimumTime: 5,
maximumPoints: 100 // Set this high to avoid random sampling which confuses comparisons
})),
JSON.stringify(require('./cross-country-tidy.json'))
);
t.end();
});

t.end();
});

// Test map matching
// curl -X POST -d @test/cross-country.json "https://api-directions-johan-matching.tilestream.net/v4/directions/matching/mapbox.driving.json?access_token=pk.eyJ1IjoicGxhbmVtYWQiLCJhIjoiemdYSVVLRSJ9.g3lbg_eN0kztmsfIPxa9MQ" --header "Content-Type:application/json"
});
23 changes: 1 addition & 22 deletions test/walk-1-resampled.json
Original file line number Diff line number Diff line change
@@ -1,22 +1 @@
{
"type": "FeatureCollection",
"features": [{
"type": "Feature",
"properties": {
"coordTimes": ["2015-06-05T01:07:54Z", "2015-06-05T01:08:57Z", "2015-06-05T01:10:55Z", "2015-06-05T01:15:50Z", "2015-06-05T01:22:12Z", "2015-06-05T01:24:15Z", "2015-06-05T01:26:30Z", "2015-06-05T01:27:25Z", "2015-06-05T01:27:32Z", "2015-06-05T01:28:21Z"]
},
"geometry": {
"type": "LineString",
"coordinates": [[-77.024365, 38.908573, 28], [-77.021984, 38.908354, 27.4], [-77.019947, 38.906015, 26.2], [-77.019874, 38.898852, 11.7], [-77.022118, 38.889388, 4.6], [-77.024836, 38.887695, 8.3], [-77.02998, 38.887682, 10.8], [-77.031757, 38.887988, 11], [-77.031771, 38.888176, 10.6], [-77.031786, 38.889507, 7.4]]
}
}, {
"type": "Feature",
"properties": {
"coordTimes": ["2015-06-05T01:28:21Z", "2015-06-05T01:30:35Z", "2015-06-05T01:30:42Z", "2015-06-05T01:31:16Z", "2015-06-05T01:35:38Z", "2015-06-05T01:37:54Z", "2015-06-05T01:41:18Z"]
},
"geometry": {
"type": "LineString",
"coordinates": [[-77.031786, 38.889507, 7.4], [-77.032016, 38.893607, 2.7], [-77.032047, 38.893811, 3], [-77.032148, 38.894888, 3.6], [-77.026997, 38.898782, 12.2], [-77.027081, 38.902667, 21.1], [-77.02621, 38.908486, 28.8]]
}
}]
}
{"type":"FeatureCollection","features":[{"type":"Feature","properties":{"coordTimes":["2015-06-05T01:07:54Z","2015-06-05T01:08:04Z","2015-06-05T01:31:33Z","2015-06-05T01:08:15Z","2015-06-05T01:25:55Z","2015-06-05T01:08:22Z","2015-06-05T01:37:10Z","2015-06-05T01:08:31Z","2015-06-05T01:28:39Z","2015-06-05T01:41:18Z"]},"geometry":{"type":"LineString","coordinates":[[-77.024365,38.908573,28],[-77.024058,38.908524,27.8],[-77.032088,38.895295,3.7],[-77.023777,38.908523,27.8],[-77.02856,38.887663,10.1],[-77.023494,38.908562,28],[-77.026926,38.901276,16.8],[-77.023017,38.908507,28.1],[-77.031771,38.890055,6.2],[-77.02621,38.908486,28.8]]}}]}
Loading

0 comments on commit 30d94d9

Please sign in to comment.