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

Feat [transform-translate] handles geometry preservation #2775

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/turf-transform-translate/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ on the provided direction angle.
* `options.units` **Units** in which `distance` will be express; miles, kilometers, degrees, or radians (optional, default `'kilometers'`)
* `options.zTranslation` **[number][3]** length of the vertical motion, same unit of distance (optional, default `0`)
* `options.mutate` **[boolean][5]** allows GeoJSON input to be mutated (significant performance increase if true) (optional, default `false`)
* `options.aroundCenter` **[boolean][5]** when set to true, for each feature the center is translated and the geometry reconstructed around it. Otherwise, points are independently translated. (optional, default `false`)

### Examples

Expand Down
117 changes: 106 additions & 11 deletions packages/turf-transform-translate/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { GeoJSON, GeometryCollection } from "geojson";
import { coordEach } from "@turf/meta";
import { FeatureCollection, GeoJSON, GeometryCollection } from "geojson";
import { coordEach, featureEach } from "@turf/meta";
import { isObject, Units } from "@turf/helpers";
import { getCoords } from "@turf/invariant";
import { clone } from "@turf/clone";
import { rhumbDestination } from "@turf/rhumb-destination";
import { bearing } from "@turf/bearing";
import { centroid } from "@turf/centroid";
import { destination } from "@turf/destination";
import { distance } from "@turf/distance";

/**
* Moves any geojson Feature or Geometry of a specified distance along a Rhumb Line
Expand All @@ -17,6 +21,7 @@ import { rhumbDestination } from "@turf/rhumb-destination";
* @param {Units} [options.units='kilometers'] in which `distance` will be express; miles, kilometers, degrees, or radians
* @param {number} [options.zTranslation=0] length of the vertical motion, same unit of distance
* @param {boolean} [options.mutate=false] allows GeoJSON input to be mutated (significant performance increase if true)
* @param {boolean} [options.aroundCenter=false] when set to true, for each feature the center is translated and the geometry reconstructed around it. Otherwise, points are independently translated.
* @returns {GeoJSON|GeometryCollection} the translated GeoJSON object
* @example
* var poly = turf.polygon([[[0,29],[3.5,29],[2.5,32],[0,29]]]);
Expand All @@ -34,6 +39,7 @@ function transformTranslate<T extends GeoJSON | GeometryCollection>(
units?: Units;
zTranslation?: number;
mutate?: boolean;
aroundCenter?: boolean;
}
): T {
// Optional parameters
Expand All @@ -42,6 +48,7 @@ function transformTranslate<T extends GeoJSON | GeometryCollection>(
var units = options.units;
var zTranslation = options.zTranslation;
var mutate = options.mutate;
var aroundCenter = options.aroundCenter;

// Input validation
if (!geojson) throw new Error("geojson is required");
Expand All @@ -66,17 +73,105 @@ function transformTranslate<T extends GeoJSON | GeometryCollection>(
// Clone geojson to avoid side effects
if (mutate === false || mutate === undefined) geojson = clone(geojson);

// Translate each coordinate
coordEach(geojson, function (pointCoords) {
var newCoords = getCoords(
rhumbDestination(pointCoords, distance, direction, { units: units })
if (aroundCenter === false || aroundCenter === undefined) {
// Translate each coordinate
coordEach(geojson, function (pointCoords) {
var newCoords = getCoords(
rhumbDestination(pointCoords, distance, direction, { units: units })
);
pointCoords[0] = newCoords[0];
pointCoords[1] = newCoords[1];
if (zTranslation && pointCoords.length === 3)
pointCoords[2] += zTranslation;
});
} else {
/// Translate each feature using its center
if (geojson.type === "FeatureCollection") {
featureEach(geojson, function (feature, index) {
// The type guard above is not recognised in the callback so we have to
// cast to accept responsibility.
(geojson as FeatureCollection).features[index] =
translateAroundCentroid(
feature,
distance,
direction,
zTranslation,
units
);
});
} else {
geojson = translateAroundCentroid(
geojson,
distance,
direction,
zTranslation,
units
);
}
}

return geojson;
}

/**
* Translate Feature/Geometry
*
* @private
* @param {GeoJSON|GeometryCollection} feature feature or geometry collection to translate
* @param {number} distanceTranslation of translation greater than 0
* @param {number} directionTranslation of translation
* @param {number} zTranslation
* @param {Units} units in which the distance is expressed
* @returns {GeoJSON|GeometryCollection} translated GeoJSON Feature/Geometry
*/
function translateAroundCentroid<T extends GeoJSON | GeometryCollection>(
feature: T,
distanceTranslation: number,
directionTranslation: number,
zTranslation?: number,
units?: Units
): T {
if (
distanceTranslation === undefined ||
distanceTranslation === null ||
isNaN(distanceTranslation)
)
throw new Error("distance is required");
if (distanceTranslation < 0)
throw new Error("distance should be greater than 0");
if (
directionTranslation === undefined ||
directionTranslation === null ||
isNaN(directionTranslation)
)
throw new Error("direction is required");
zTranslation = zTranslation !== undefined ? zTranslation : 0;

if (distanceTranslation === 0) return feature;

const featureCentroid = centroid(feature);
const newCentroid = getCoords(
rhumbDestination(
featureCentroid,
distanceTranslation,
directionTranslation,
{ units: units }
)
);

// Scale each coordinate
coordEach(feature, function (coord) {
const originalDistance = distance(featureCentroid, coord);
const originalBearing = bearing(featureCentroid, coord);
const newCoord = getCoords(
destination(newCentroid, originalDistance, originalBearing)
);
pointCoords[0] = newCoords[0];
pointCoords[1] = newCoords[1];
if (zTranslation && pointCoords.length === 3)
pointCoords[2] += zTranslation;
coord[0] = newCoord[0];
coord[1] = newCoord[1];
if (zTranslation && coord.length === 3) coord[2] += zTranslation;
});
return geojson;

return feature;
}

export { transformTranslate };
Expand Down
7 changes: 7 additions & 0 deletions packages/turf-transform-translate/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@
"test:types": "tsc --esModuleInterop --module node16 --moduleResolution node16 --noEmit --strict types.ts"
},
"devDependencies": {
"@turf/area": "workspace:^",
"@turf/circle": "workspace:^",
"@turf/intersect": "workspace:^",
"@turf/truncate": "workspace:^",
"@types/benchmark": "^2.1.5",
"@types/tape": "^4.13.4",
Expand All @@ -71,7 +74,11 @@
"write-json-file": "^5.0.0"
},
"dependencies": {
"@turf/bearing": "workspace:^",
"@turf/centroid": "workspace:^",
"@turf/clone": "workspace:^",
"@turf/destination": "workspace:^",
"@turf/distance": "workspace:^",
"@turf/helpers": "workspace:^",
"@turf/invariant": "workspace:^",
"@turf/meta": "workspace:^",
Expand Down
44 changes: 40 additions & 4 deletions packages/turf-transform-translate/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import path from "path";
import { fileURLToPath } from "url";
import { loadJsonFileSync } from "load-json-file";
import { writeJsonFileSync } from "write-json-file";
import { area } from "@turf/area";
import { circle } from "@turf/circle";
import { intersect } from "@turf/intersect";
import { truncate } from "@turf/truncate";
import {
point,
Expand Down Expand Up @@ -37,8 +40,17 @@ test("translate", (t) => {
units,
zTranslation,
});
const translatedAroundCenter = translate(geojson, distance, direction, {
units,
zTranslation,
aroundCenter: true,
});
const result = featureCollection([
colorize(truncate(translated, { precision: 6, coordinates: 3 })),
colorize(truncate(translated, { precision: 6, coordinates: 3 }), "#F00"),
colorize(
truncate(translatedAroundCenter, { precision: 6, coordinates: 3 }),
"#00F"
),
geojson,
]);

Expand Down Expand Up @@ -135,19 +147,43 @@ test("rotate -- geometry support", (t) => {
t.end();
});

test("turf-translate -- circle consistency", (t) => {
const circleCenter = point([0, 60]);
const circleCenterTranslated = translate(circleCenter, 1000, 30);
const circleOrigin = circle(circleCenter, 1000);
const circleTranslated = translate(circleOrigin, 1000, 30, {
aroundCenter: true,
});
const circleReconstructed = circle(circleCenterTranslated, 1000);

const intersectionGeom = intersect(
featureCollection([circleTranslated, circleReconstructed])
);
const areaIntersection =
intersectionGeom != null ? area(intersectionGeom.geometry) : 0;
const areaCircle =
circleTranslated != null ? area(circleTranslated.geometry) : 0;
t.true(
Math.abs(areaIntersection - areaCircle) / areaCircle < 0.01,
"both areas are equal"
);
t.end();
});

// style result
function colorize(geojson: Feature) {
function colorize(geojson: Feature, color?) {
// We are going to add some properties, so make sure properties attribute is
// present.
color = color || "#F00";
geojson.properties = geojson.properties ?? {};
if (
geojson.geometry.type === "Point" ||
geojson.geometry.type === "MultiPoint"
) {
geojson.properties["marker-color"] = "#F00";
geojson.properties["marker-color"] = color;
geojson.properties["marker-symbol"] = "star";
} else {
geojson.properties["stroke"] = "#F00";
geojson.properties["stroke"] = color;
geojson.properties["stroke-width"] = 4;
}
return geojson;
Expand Down
79 changes: 79 additions & 0 deletions packages/turf-transform-translate/test/in/circle.geojson
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
{
"type": "Feature",
"properties": {
"direction": 30,
"distance": 1000
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[0, 68.99320363724537],
[-2.4429584198737904, 68.93313263109376],
[-4.827615673717056, 68.75446551386186],
[-7.099933947678443, 68.46170890478926],
[-9.213648021302797, 68.06196358356087],
[-11.132504186336082, 67.56439307389124],
[-12.831074422570252, 66.97961850084515],
[-14.294324293079713, 66.31912548486699],
[-15.516311969170072, 65.59474542818991],
[-16.49844020508527, 64.81824400722547],
[-17.247616061432137, 64.00102375186508],
[-17.7745577051021, 63.153930055547875],
[-18.0923735451161, 62.28714117393077],
[-18.2154519279803, 61.41012065685274],
[-18.158645231502856, 60.53161254825021],
[-17.93670515509441, 59.65966336278071],
[-17.56391759236705, 58.80165885845095],
[-17.053887726029547, 57.96436721224244],
[-16.419433163873737, 57.1539830832056],
[-15.672551513843027, 56.37616919405936],
[-14.824436921265958, 55.636093578844374],
[-13.885526976991425, 54.938461667547514],
[-12.865566866929816, 54.2875430393439],
[-11.773681779319768, 53.687193081741135],
[-10.618451635896449, 53.1408700250564],
[-9.407984404938373, 52.65164793888436],
[-8.149985801204723, 52.222226319863246],
[-6.85182425431596, 51.85493689498236],
[-5.520590767778858, 51.55174822927353],
[-4.16315379565268, 51.31426867194753],
[-2.786209605013719, 51.14374810765571],
[-1.3963288207704132, 51.041078903744776],
[-1.7431403842134502e-15, 51.00679636275462],
[1.39632882077041, 51.041078903744776],
[2.7862096050137155, 51.14374810765571],
[4.163153795652676, 51.31426867194753],
[5.520590767778848, 51.55174822927353],
[6.8518242543159555, 51.85493689498236],
[8.149985801204727, 52.222226319863246],
[9.40798440493837, 52.65164793888436],
[10.618451635896443, 53.140870025056394],
[11.773681779319771, 53.687193081741135],
[12.865566866929806, 54.287543039343895],
[13.885526976991414, 54.9384616675475],
[14.824436921265967, 55.63609357884438],
[15.672551513843027, 56.37616919405936],
[16.419433163873734, 57.1539830832056],
[17.053887726029547, 57.96436721224244],
[17.56391759236705, 58.80165885845095],
[17.93670515509441, 59.65966336278071],
[18.158645231502856, 60.53161254825021],
[18.2154519279803, 61.41012065685273],
[18.0923735451161, 62.28714117393077],
[17.7745577051021, 63.153930055547875],
[17.247616061432137, 64.00102375186508],
[16.49844020508527, 64.81824400722547],
[15.516311969170077, 65.59474542818991],
[14.294324293079722, 66.31912548486699],
[12.831074422570252, 66.97961850084515],
[11.132504186336087, 67.56439307389124],
[9.213648021302793, 68.06196358356087],
[7.099933947678468, 68.46170890478926],
[4.827615673717068, 68.75446551386186],
[2.442958419873788, 68.93313263109376],
[0, 68.99320363724537]
]
]
}
}
Loading