Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New module @turf/line-offset #729

Merged
merged 20 commits into from
May 24, 2017
Merged

New module @turf/line-offset #729

merged 20 commits into from
May 24, 2017

Conversation

rowanwins
Copy link
Member

@rowanwins rowanwins commented May 10, 2017

New Module @turf/line-offset

Adds a new lineOffset module as per this issue. Basically takes an input line and returns a new line offset by the distance.

Geometry Support

  • LineString => LineString
  • MultiLineString => MultiLineString

Geometry NOT supported

  • Point => LineString (alternative @turf/circle?)
  • MultiPoint => MultiLineString (alternative @turf/circle?)
  • Polygon => LineString (alternative @turf/buffer / @turf/transform-scale)
  • MultiPolygon => MultiLineString (alternative @turf/buffer / @turf/transform-scale)
  • FeatureCollection/GeometryCollection (might contain mixed geometries and cause errors)

To-Do

JSDocs

/**
 * Takes a {@link LineString|line} and returns a {@link LineString|line} at offset by the specified distance.
 *
 * @name lineOffset
 * @param {Geometry|Feature<LineString>} line input line
 * @param {number} offset distance to offset the line (can be of negative value)
 * @param {string} [units=kilometers] can be degrees, radians, miles, kilometers, inches, yards, meters
 * @returns {Feature<LineString>} Line offset from the input line
 * @example
 * var line = {
 *   "type": "Feature",
 *   "properties": {},
 *   "geometry": {
 *     "type": "LineString",
 *     "coordinates": [[-83, 30], [-84, 36], [-78, 41]]
 *   }
 * };
 *
 * var offsetLine = turf.lineOffset(line, 2, 'miles');
 *
 * //addToMap
 * var addToMap = [offsetLine, line]
 */

Examples

image

  • Use a meaningful title for the pull request. Include the name of the package modified.
  • Have read How To Contribute.
  • Run npm test at the sub modules where changes have occurred.
  • Run npm run lint to ensure code style at the turf module level.

@DenisCarriere
Copy link
Member

@rowanwins Can you provide more information in your Initial Description, that way when we reference this PR we have all the information available for the module at the top.

Use this as a rough example: #700

And for some reason the benchmark isn't printing results?

You need to upgrade the benchmark version (v1.0.0 has issues with ES6 syntax).

$ yarn add --dev tape benchmark

}
};

var route = JSON.parse(fs.readFileSync(__dirname + '/test/fixtures/route.geojson'));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try to make the new benchmark.js files as minimalistic as possible, use a few of the latest modules as examples:
https://github.com/Turfjs/turf/blob/0cfbda88456a89c7cd988ac17afd6056a42f59a8/packages/turf-rhumb-distance/bench.js

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doh didn't push the right benchmark file!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:) nice

/**
* http://turfjs.org/docs/#lineOffset
*/
declare function lineOffset(line: LineString | GeoJSON.LineString, distance: number, units?: string): LineString;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

* Takes a {@link LineString|line} and returns a {@link LineString|line} at offset by the specified distance.
*
* @name lineOffset
* @param {Feature<LineString>|Geometry<LineString>} line input line
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can group the JSDoc types together

/**
 * @param {Geometry|Feature<LineString>} line input line

seg2Coords[0][1] = int.y;
}
});
var finalCoords = segments.map(function (segment) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth using forEach or for loop since it will be significantly faster than using map (~30-40% faster - Use benchmark to get an exact %)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could be faster, although the benchmark is performing 600,000 operations per second so we're talking about shaving milli-milli-milli-mill seconds :) Can probably be tackled in the overall refactoring of those loops

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll have a look at the refactoring, sometimes it's not worth it, but sometimes it's an easy change for a pretty significant performance increase. Not a deal breaker whatsoever

coordEach(line, function (currentCoords, currentIndex) {
if (currentIndex !== coords.length - 1) {
var outCoords = processSegment(currentCoords, coords[currentIndex + 1], offsetDegrees);
segments.push(outCoords);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Iterating over all coords, then iterating over all segments, then iterating over segments again.

Looks like this could be done by only iterating your coordinates once.

You'll need to convert this code block into it's own method and use a callback to prevent the double iterations.

I can tackle this if you don't feel comfortable using callbacks

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it is a bit messy at the moment, I'll probably wont get a chance to look at it for another few nights so if you've got time and want to tackle it go nuts :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do :) I'll have a look at it more tomorrow, it's a good starting point so far!
+1

return segment[0];
});
finalCoords.push(segments[segments.length - 1][1]);
return lineString(finalCoords);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also translate the properties from the input LineString?

return lineString(finalCoords);
};

// Inspiration taken from http://stackoverflow.com/questions/2825412/draw-a-parallel-line
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Nice reference

"offset"
],
"author": "Turf Authors",
"license": "MIT",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add yourself as contributors.

},
"homepage": "https://github.com/Turfjs/turf",
"devDependencies": {
"benchmark": "^1.0.0",
Copy link
Member

@DenisCarriere DenisCarriere May 11, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Upgrade benchmark & tape

I'll upgrade all devDependencies in TurfJS

"@turf/helpers": "^4.1.0",
"@turf/invariant": "^4.2.0",
"@turf/meta": "^4.2.0",
"intersection": "0.0.1"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd drop intersection in favor of using @turf/line-intersect.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

intersection works out where lines would intersect based on their direction, even if they don't actually intersect, eg it works irrespective of length. See this interactive example.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oooohh, yes that's completely different, I wonder if it's worth including that logic directly in Turf. I'm not a big fan of external dependencies, especially if it can be refactored in less than 30-50 LOC

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah it probably could be included, I had a bit of a look at the source code and it's not huge but it was enough of a hassle that I thought I'd pull in the external first just to test the concept first.

if (index !== segments.length - 1) {
var seg2Coords = segments[index + 1];
var seg1 = {start: {x: segment[0][0], y: segment[0][1]}, end: {x: segment[1][0], y: segment[1][1]}};
var seg2 = {start: {x: seg2Coords[0][0], y: seg2Coords[0][1]}, end: {x: seg2Coords[1][0], y: seg2Coords[1][1]}};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty sure this can be handle by @turf/line-intersect.
We could include Array<Array<number>> support in line-intersect (geometry.coordinates)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment above about how the intersect module differs from line-intersect

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 yep makes sense

"devDependencies": {
"benchmark": "^1.0.0",
"tape": "^3.5.0",
"@turf/helpers": "^4.1.0"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing write-json-file & load-json-file devDependencies

@DenisCarriere
Copy link
Member

To make the tests pass you might need to wrap the results with @turf/truncate since the error is coming from coordinates being slightly off of 0.000001.

@rowanwins
Copy link
Member Author

Gday @DenisCarriere

Have just updated the whole looping approach and its now much tidier I think.

Only thing is the benchmark results look like they are slower than before but in reality I think it's now twice as fast, either a) my laptop is running on battery and goes slower, or b) I changed a benchmark test parameter. Anyway I've run the same benchmark test on both variations on the code this evening and the latest commit is twice as fast as the old.

Cheers

@DenisCarriere
Copy link
Member

Woot! Passing tests, one of the reasons why I decided to build @turf/truncate, it was a total pain having 0.000001 offset causing the deepEqual to fail.

@rowanwins
Copy link
Member Author

Updated to support lines with only 1 segment and lines that are completely straight (horizontal and vertical).

@rowanwins
Copy link
Member Author

PS Turns out that having my battery plugged in does effect the performance of my benchmark tests!

linestring-feature with changer === 1M ops/sec
linestring-feature with battery === 300K ops/sec

@DenisCarriere
Copy link
Member

👍 Yea the benchmark results are very dependent on CPU performance, but it's a good gage of performance. Also closing 10+ Google Chrome browser windows help 🤓

@DenisCarriere
Copy link
Member

DenisCarriere commented May 17, 2017

@rowanwins Just an update on this PR, once we get a few other geometries supported this will be ready to merge.

Added Geometry Support details above

@DenisCarriere DenisCarriere self-assigned this May 17, 2017
@rowanwins
Copy link
Member Author

Gday @DenisCarriere

It did just cross my mind that this could conceivable be used as a replacement for a buffer module, particularly for polys... Not sure how performance would go but worth a quick trial.

@DenisCarriere
Copy link
Member

DenisCarriere commented May 17, 2017

Dropping Polygon or MultiPolygon support since this algorithm is not designed for this type of offset. Below are examples of attempts using line-offset on Polygons:

Red: Input
Blue: Results

1000 miles - Inner rings become larger than outer rings

image

-500 miles - Doesn't make any sense

image

Start & End vertices are disconnected (might be worth looking into for LineString)

image

CC: @rowanwins

@DenisCarriere
Copy link
Member

DenisCarriere commented May 17, 2017

be used as a replacement for a buffer module, particularly for polys

I thought so too! Minus the crazy outputs! Lets stick with LineStrings on this module.

I really like it so far, but not with Polygons.

@DenisCarriere
Copy link
Member

DenisCarriere commented May 17, 2017

@rowanwins Added MultiLineString support, I'm ready to merge this if ever you are 👍 with it.

@DenisCarriere
Copy link
Member

@rowanwins Another issue that can be addressed at a later time is the Equidistance offset, same issue we had with @turf/buffer #718.

Issue in Northern Latitudes

50km offset north/south directions, 12km offset west/east direction

https://github.com/Turfjs/turf/blob/bc34c926a11a424992b9e08643603fa81d54329f/packages/turf-line-offset/test/out/northern-line.geojson
image

@rowanwins
Copy link
Member Author

merge away. I might have a tinker with the polygon issue because im sure the code is there, just might need a bit of refactoring

@stebogit stebogit mentioned this pull request May 17, 2017
6 tasks
@stebogit
Copy link
Collaborator

@rowanwins @DenisCarriere I believe the output with (Multi)Polygons is actually the correct/expected result for this module, as it creates a parallel (and elongated/shortened) line alongside the original one. A Polygon just happens to be a closed line, however the module I think should not connect the start and end points of the resulting line.
If that was the actual need, then it would probably be more appropriate a scale transformation (#747) of the Polygon.
Ergo, I'd say is correct applying this module only to (opened) lines.

@DenisCarriere
Copy link
Member

the module I think should not connect the start and end points of the resulting line.

👍 Agreed

is correct applying this module only to (opened) lines.

Agreed, only supporting LineString would be best for this module.

@stebogit
Copy link
Collaborator

stebogit commented May 17, 2017

@rowanwins @DenisCarriere I believe these formulas might help to correct the distortion here and in other modules.

@DenisCarriere
Copy link
Member

DenisCarriere commented May 17, 2017

these formulas might help to correct the distortion here

That's what was used for @turf/buffer.
https://github.com/Turfjs/turf/blob/master/packages/turf-buffer/index.js#L117-L163

@rowanwins
Copy link
Member Author

Hi @DenisCarriere & @stebogit

One thought on the whole distortion issue, my understanding is that projections have been designed differently, some projections are designed for preserving shape, while others are designed to preserve area etc etc

With the lines @DenisCarriere references above we're making that judgement for ppl as to what the best projection is (one that preserves shape). D3 actually supports a bunch which are useful for different purposes.

Rather than us forcing an choice on people perhaps we should be getting them to pass in their data with the projection they deem appropriate?

Thoughts?

@stebogit
Copy link
Collaborator

I personally don't have experience with projections different from (Web)Mercator (i.e. Google Maps).
I'm wondering also how many users use Turf in a non-Mercator map, being Pseudo-Mercator the

"de facto standard for Web mapping applications"

and Turf.js a JavaScript (i.e. web) library.

Just my thought.
I'd be genuinely curious to know about interesting applications in other environments with different projections.

@DenisCarriere
Copy link
Member

Rather than us forcing an choice on people perhaps we should be getting them to pass in their data with the projection they deem appropriate?

👎 TurfJS shouldn't be providing projection support.

@w8r explained it well in #660 (comment).

The most important point is that you have to do the geometrical calculations in the R2 space where they make sense, so abstract equidistant does the trick.

@DenisCarriere
Copy link
Member

DenisCarriere commented May 18, 2017

@rowanwins Using that equidistance projection trick will solve the offset being different. This isn't a projection "visual effect", the offset is legitimately different (12km & 50km).
image

@DenisCarriere
Copy link
Member

DenisCarriere commented May 24, 2017

Going to merge this since it's 95% complete, only tiny issue is situations up north (can be done in the next release or a new PR).

@rowanwins @stebogit

@DenisCarriere DenisCarriere merged commit 67ee226 into master May 24, 2017
@DenisCarriere DenisCarriere deleted the lineOffset branch May 24, 2017 18:54
@rowanwins
Copy link
Member Author

Gday @DenisCarriere

Just checking back in on the projection discussion. I wasn't proposing for turf to support projections, there is already proj4js to do that. I just wanted to check that by forcing a projection during a process like buffer, we weren't setting ourselves up for trouble. It's been way too long since I thought about datums and projections so apologies for the misuse of terminology etc :)

@DenisCarriere
Copy link
Member

Agree we should do the same as the buffer, however we should make the project/reproject process as a module or external dependency.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants