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/transform-scale #759

Merged
merged 18 commits into from
May 29, 2017
Merged

New module @turf/transform-scale #759

merged 18 commits into from
May 29, 2017

Conversation

stebogit
Copy link
Collaborator

Ref: #747

@DenisCarriere @dpmcmlxxvi
The output is currently not correct: it is scaled successfully, but for some reason it does not reflect the factor value.
I implemented my proposal for the origin; I look forward to knowing your opinion.
Also, the scaling along the z-coordinate is not along a Rhumb line, but a simple multiplication by 'factor'. Is there a way, if it make sense at all, to perform such a scaling on z?

For the moment I'd keep it simple and consider only uniform scalings, i.e. scaling factor equal in all directions.

@stebogit stebogit self-assigned this May 27, 2017
@DenisCarriere
Copy link
Member

Keeping it simple is always the winner in my book! 👍 Looks good so far

@DenisCarriere
Copy link
Member

DenisCarriere commented May 27, 2017

Strange that the coverage doesn't compute correctly, it might be looking at the first coverage tests and then upload right away.

image

Tap will be used at the root level and not in the  individual repos
* @name scale
* @param {GeoJSON} geojson object to be scaled
* @param {number} factor of scaling, positive values greater than 0
* @param {boolean} [fromCenter=true] point from which the scaling will occur; it will default to the most
Copy link
Member

Choose a reason for hiding this comment

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

👍 Love it!

We might need to re-word the JSDocs, sounds like the "default" is saying False but in fact it's True.

point from which the scaling will occur; it will default to the most south-west point of a bbox containing the geojson, if true the scaling will origin from the centroid of the geojson

if (!geojson) throw new Error('geojson is required');

if (geojson.type === 'Point')
return (mutate === false || mutate === undefined) ? geojson : JSON.parse(JSON.stringify(geojson));
Copy link
Member

Choose a reason for hiding this comment

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

?? Why the multiple mutate === undefined This should be on it's own and only defined once.

if (fromCenter === true || fromCenter === undefined) {
origin = centroid(geojson);
} else {
var box = bbox(geojson); // [ minX, minY, maxX, maxY ]
Copy link
Member

Choose a reason for hiding this comment

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

Calculating BBox can be slightly computational heavy, so if a bbox is already provided in the geojson (which is defined in the spec) we can use that vs. re-calculating it.

http://geojson.org/geojson-spec.html#bounding-boxes

var box = (geojson.bbox) ? geojson.bbox : bbox(geojson);

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

good to know!

if (mutate === false || mutate === undefined) geojson = JSON.parse(JSON.stringify(geojson));

// Scale each coordinate
coordEach(geojson, function (pointCoords) {
Copy link
Member

Choose a reason for hiding this comment

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

Going to be using coord as variable name since the singular term implies it's a point:

  • coord === [x, y]
  • coords === [[x1, y1], [x2, y2]]

@DenisCarriere
Copy link
Member

DenisCarriere commented May 27, 2017

@stebogit Got an idea for defining the point of origin.

What if allow a param called fromCorner with 5 options (ne, nw, se, sw, center):

var west = bbox[0];
var south = bbox[1];
var east = bbox[2];
var north = bbox[3];
  • ne/northeast/ topright => [east, north]
  • nw/northwest/ topleft => [west, north]
  • se/southeast/ bottomright => [east, south]
  • sw/southwest/ bottomleft => [west, south]
  • center/undefined => center

This would allow an application to scale any GeoJSON in any 5 direction with relative ease.

@stebogit
Copy link
Collaborator Author

@DenisCarriere super! 👍

@DenisCarriere
Copy link
Member

@stebogit Cool! I'll add that, shouldn't be too long.


# scale

Moves any geojson Feature or Geometry of a specified distance along a Rhumb Line
Copy link
Collaborator Author

@stebogit stebogit May 27, 2017

Choose a reason for hiding this comment

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

@DenisCarriere probably you can find a better description for the module, explaining the Rhumb "motion" of the point; this however is the translate description

if (!factor) throw new Error('factor is required');

// Default params
var isMutate = mutate === false || mutate === undefined;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this logic correct? If mutate is false then isMutate is true? Also, if mutate is not defined then isMutate is true. The logic below says if isMutate is true then you clone the data. I think either the logic or the variable names are backwards. You pick.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'd say just:
if (mutate === true) geojson = JSON.parse(JSON.stringify(geojson));
dropping isMutate

Copy link
Collaborator

Choose a reason for hiding this comment

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

So if mutate is true then you clone the data? Seems backwards.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Arrrgh! You're right! Dumb me...
Then
if (!mutate === true) geojson = JSON.parse(JSON.stringify(geojson));
Still no need for isMutate
Right?

Copy link
Collaborator

Choose a reason for hiding this comment

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

That should work. Though there should be enough tests to ensure that it works and the data is not cloning when it should be and vice versa.

However, I usually prefer logical conditions to be explicit and not implicit which require the reader to sit there and figure out what is going on and have to remember javascript falsey rules. Just my preference. But if it works it works.


// Shortcut no-scaling
if (factor === 1)
return (mutate === false || mutate === undefined) ? geojson : JSON.parse(JSON.stringify(geojson));
if (factor === 1) return geojson;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@DenisCarriere what about:
if (factor === 1 || geojson.geometry.type === 'Point' || geojson.type === 'Point') return geojson;
Scaling a Point is pointless 😄

Copy link
Member

@DenisCarriere DenisCarriere May 27, 2017

Choose a reason for hiding this comment

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

Very true

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

this would skip only Point, not MultiPoint. Wouldn't it?

Copy link
Member

Choose a reason for hiding this comment

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

Yep MultiPoint won't be skipped. I'll add that in my next commit, wait out

@dpmcmlxxvi
Copy link
Collaborator

@stebogit Scaling z by a multiplication factor is fine. Rhumb lines are just along the earth's surface.

@dpmcmlxxvi
Copy link
Collaborator

@stebogit You might consider allowing the fromCorner to be either the five strings mentioned by @DenisCarriere or a Point. Plus the name fromCorner might be a little misleading since, for example, the center is not a corner. Maybe just origin.

*
* @name scale
* @param {GeoJSON} geojson object to be scaled
* @param {number} factor of scaling, positive values greater than 0
Copy link
Member

Choose a reason for hiding this comment

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

@stebogit Couldn't it also be negative scaling?

Copy link
Collaborator Author

@stebogit stebogit May 27, 2017

Choose a reason for hiding this comment

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

@DenisCarriere how do you scale negative?
0 - 1 => shrink
1 - Infinity => enlarge
< 0 ?
0 => a Point ? Does this make sense?

@stebogit
Copy link
Collaborator Author

@dpmcmlxxvi 👍

Maybe just origin.

Why not originPosition then? Clear, unmistakable.

@stebogit
Copy link
Collaborator Author

stebogit commented May 27, 2017

allowing the fromCorner to be [...] a Point

@dpmcmlxxvi @DenisCarriere could the origin be anywhere?
i.e. do we want to allow this:
scale picture

I personally find it weird, not obvious (it looks more like a scale + translate), but if you think otherwise let's origin be a user-defined Point!

@dpmcmlxxvi
Copy link
Collaborator

I think lots of scaling operations would be performed from origin points that are not related to the features. For example, fixing sensor mis-calibrations, mis-registrations, etc.

var box = (geojson.bbox) ? geojson.bbox : bbox(geojson); // [ west, south, east, north ]
origin = point([box[0], box[1]]);
}
if (factor === 1 || isPoint) return geojson;

// Clone geojson to avoid side effects
if (isMutate) geojson = JSON.parse(JSON.stringify(geojson));
Copy link
Collaborator Author

@stebogit stebogit May 27, 2017

Choose a reason for hiding this comment

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

@DenisCarriere I'd move this before any return, since the mutate behaviour should be respected even if there is no actual scaling. That's why my original multiple mutate === undefined

* @param {string} [fromCorner] sw/se/nw/ne/center
* @returns {Feature<Point>} Point origin
*/
function defineOrigin(geojson, fromCorner) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Great job! 👍

added tests with origin as point;
@stebogit
Copy link
Collaborator Author

@DenisCarriere @dpmcmlxxvi I fixed a couple of errors in the calculation and added some more tests. Now I believe the output is correct.
However I believe centroid is unexpected as default central origin, I'd rather use center. What do you think?

Example MultiPolygon:
screen shot 2017-05-28 at 12 45 32 am
screen shot 2017-05-28 at 12 46 47 am

@DenisCarriere
Copy link
Member

DenisCarriere commented May 28, 2017

@stebogit @dpmcmlxxvi Shouldn't it be best for Multi(Feature) to have the origin point centered on each individual shape when doing the scale operations.

We could use FlattenEach for that (shouldn't be too much effort, very similar to coordEach syntax).

Edit: Nevermind, that would most likely be the case for a FeatureCollection and not a MultiFeature.

@DenisCarriere
Copy link
Member

DenisCarriere commented May 28, 2017

Handling FeatureCollection

@stebogit FeatureCollection should be handled as an exception, if origin is undefined or string we should apply the scale operation on each feature individually. If origin is provided as a Point then the entire FeatureCollection will be scaled using that Point.

Note: GeometryCollection should NOT be handled like a FeatureCollection since it is considered a single Feature.

Proposed (Multiple centroids per feature)

image

Before (Single centroid origin)

image

@DenisCarriere
Copy link
Member

DenisCarriere commented May 28, 2017

However I believe centroid is unexpected as default central origin, I'd rather use center. What do you think?

@stebogit Yep agreed, center might be a better "default" value.

@dpmcmlxxvi
Copy link
Collaborator

@stebogit It looks good to me. @DenisCarriere As for scaling each feature separately in a FeatureCollection, it's an interesting approach but I'm wondering what you expect the use case to be? What geospatial application would someone need to scale each feature separately from, say, their own centroid? I would think scaling each separately would be the exception and by default treat the collection as the others.

@DenisCarriere
Copy link
Member

Actually centroid looks like it would be the better option vs. center.

Centroid

image

Center

image

@DenisCarriere
Copy link
Member

As for scaling each feature separately in a FeatureCollection, it's an interesting approach but I'm wondering what you expect the use case to be?

@dpmcmlxxvi If you provide a FeatureCollection using the bare minimum parameters scale(geojson, 2), what would you expect?

I personally would expect this:
image

@DenisCarriere
Copy link
Member

DenisCarriere commented May 28, 2017

@dpmcmlxxvi @stebogit Good news!! No issues up in the Northern Latitudes (~75-80 lat)

polygon-resolute-bay.geojson
image

Bad (expected) news, some issues with a Fiji MultiPolygon.

We can ignore this for now, but another day we look for a solution.

polygon-fiji.geojson
image
image

@stebogit
Copy link
Collaborator Author

To me the difference between FeatureCollection and Multi(Feature) is the relationship between the features: the first case we have a collection of independent features (FeatureCollection is like a container for the features), while the second is actually a single feature described by multiple similar features. Therefore I'd expect a transformation on a collection as independently performed.

@DenisCarriere
Copy link
Member

@stebogit Agreed, FeatureCollection is simply a container of Features which scale should be applied to each individual feature. MultiFeature should be treated as an entire object when using Scale (not to use flatten transformation).

@DenisCarriere
Copy link
Member

The FeatureCollection handling logic is pretty straightforward.

// Scale each Feature separately
if (geojson.type === 'FeatureCollection') {
    featureEach(geojson, function (feature, index) {
        geojson.features[index] = scale(feature, factor, origin);
    });
    return geojson;
}
// Scale Feature/Geometry
return scale(geojson, factor, origin);

@dpmcmlxxvi
Copy link
Collaborator

dpmcmlxxvi commented May 28, 2017

@DenisCarriere I guess when I think of a scaling operation I normally expect there to be one fixed origin from which to scale the entire dataset. However, I'm fine with the current implementation, as long as the documentation is clear on what it is doing, because if the user wants a scaling from a single origin they can just provide a Point as the origin, correct?

@stebogit
Copy link
Collaborator Author

@DenisCarriere @dpmcmlxxvi I guess we'd need this then (note originIsPoint is just a logical evaluation):

// Scale each Feature separately
if (geojson.type === 'FeatureCollection' && !originIsPoint) {
    featureEach(geojson, function (feature, index) {
        geojson.features[index] = scale(feature, factor, origin);
    });
    return geojson;
}
// Scale Feature/Geometry
return scale(geojson, factor, origin);

However this behaviour would need to be clearly documented, as I don't think is obvious

@dpmcmlxxvi
Copy link
Collaborator

@stebogit Actually, I think the current implementation handles that case as-is, it is just scaling the coordinates of each feature separately, as opposed to the coordinates of the entire collection, but either way it should be the same effect since the origin is the same.

@DenisCarriere
Copy link
Member

DenisCarriere commented May 28, 2017

However this behaviour would need to be clearly documented, as I don't think is obvious

Agreed, would need to clarify the FeatureCollection behavior.

@stebogit Added !originIsPoint 👍 Good call

@stebogit
Copy link
Collaborator Author

@DenisCarriere the 180th Meridian problem is fascinating, I didn't know about it!
Clearly we need to work on turf-rhumb-destination (which of course presents the same issue) and once fixed that we should have fixed also all the transformations, all affected.
I'll create an issue and work on it right away.

@DenisCarriere
Copy link
Member

👍 Yes the 180th Meridian problem is a fun one! Don't sweat it too much since it's a common problem for A LOT of GIS applications to handle. Especially once you start handing a Geometry which spans on both sides of the 180th Meridian (50km buffer around Fiji is a good example of that).

@stebogit
Copy link
Collaborator Author

@DenisCarriere @dpmcmlxxvi I guess once we merge #770 and re-test everything we're good to merge this. Anything else missing?
Once done here I'll test the other transform modules against the 180th meridian issue.

@DenisCarriere
Copy link
Member

Should be good to go! We can update the fixtures once both PRs are in the master branch.

@DenisCarriere DenisCarriere merged commit e0c42bb into master May 29, 2017
@DenisCarriere DenisCarriere deleted the transform-scale branch May 29, 2017 18:19
@DenisCarriere DenisCarriere mentioned this pull request May 29, 2017
6 tasks
@DenisCarriere DenisCarriere added this to the 4.4.0 milestone May 29, 2017
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