Skip to content

Commit

Permalink
identify infinity values in reprojection and enhanced weird case hand…
Browse files Browse the repository at this point in the history
…ling for contains
  • Loading branch information
DanielJDufour committed Jun 10, 2023
1 parent 01b2b7b commit d5b8d02
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 36 deletions.
17 changes: 13 additions & 4 deletions docs/functions/reproj.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,20 @@ The `reproj` function is arguably the most important function in GeoExtent and i
This function essentially calls [reproject-bbox](https://github.com/DanielJDufour/reproject-bbox) on the extent and returns a new GeoExtent.
It's first argument `to` is the new `srs` or what we are reprojecting to. It can be a number, a string like `EPSG:4326`, [well-known text](https://en.wikipedia.org/wiki/Well-known_text_representation_of_coordinate_reference_systems), or a [proj4js](http://proj4js.org/) string.

# errors
If the reprojection failed for some reason, such as the extent is outside the bounds of the [srs](https://en.wikipedia.org/wiki/Spatial_reference_system), then this function will throw an error. For example,
## fallback strategy
If direct reprojection fails, reproj will attempt to reproject through an intermediary projection. It'll attempt
to reproject to 4326 (aka Latitude/Longitude) and then reproject to the desired projection.

## errors
If the reprojection fails even after the fallback is tried, such as the extent is outside the bounds of the [srs](https://en.wikipedia.org/wiki/Spatial_reference_system), then this function will throw an error. For example,
if you try to reproject the north pole to web mercator then you will receive an error because web mercator doesn't not work at that high of a latitude.

# quiet mode
## infinity
Some projections are bound to certain regions, so reprojection can sometimes lead to infinity values. By default,
reproj will throw an error. However, if you are okay with infinity values, you can pass in an optional parameter
`allow_infinity: true`.

## quiet mode
If you would prefer that reproj return `undefined` instead of throwing an error, you can turn on quiet mode.
```js
const northPole = new GeoExtent([-180, 85, 180, 90], { srs: 4326 });
Expand All @@ -19,7 +28,7 @@ northPole.reproj(3857, { quiet: true });
// undefined
```

# density
## density
You can control the accuracy of the reprojection by passing in a point density parameter.
Sometimes a bounding box will bend when reprojected and the most extreme points won't necessarily be at the corners. If you care for speed more than accuracy, you should choose a lower value.
By default, reproj uses a "high" density adding a 100 points to each side before reprojecting.
Expand Down
24 changes: 12 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"build": "cp ./src/geo-extent.js ./dist/geo-extent.mjs && npx babel ./src/geo-extent.js --out-file ./dist/geo-extent.cjs --plugins=@babel/plugin-transform-modules-commonjs",
"browserify": "npx browserify ./dist/geo-extent.cjs > ./dist/geo-extent.browserify.js",
"f": "npm run format",
"format": "npx prettier --arrow-parens=avoid --print-width=120 --trailing-comma=none --write src/*.js test/*.js test/*.mjs",
"format": "npx prettier --arrow-parens=avoid --print-width=120 --trailing-comma=none --write *.ts src/*.js test/*.js test/*.mjs",
"prepublishOnly": "npm run format && npm run build && npm run browserify && npm run test",
"test": "for f in ./test/*; do echo \"node $f\" && node $f && echo \"npx ts-node $f\" && npx ts-node $f; done"
},
Expand Down Expand Up @@ -49,21 +49,21 @@
},
"homepage": "https://github.com/DanielJDufour/geo-extent#readme",
"devDependencies": {
"@babel/cli": "^7.21.0",
"@babel/core": "^7.21.0",
"@babel/helper-compilation-targets": "^7.20.7",
"@babel/helper-validator-option": "^7.21.0",
"@babel/plugin-transform-modules-commonjs": "^7.21.2",
"flug": "^2.5.0",
"global-jsdom": "^8.7.0",
"jsdom": "^21.1.0",
"leaflet": "^1.9.3"
"@babel/cli": "^7.22.5",
"@babel/core": "^7.22.5",
"@babel/helper-compilation-targets": "^7.22.5",
"@babel/helper-validator-option": "^7.22.5",
"@babel/plugin-transform-modules-commonjs": "^7.22.5",
"flug": "^2.6.0",
"global-jsdom": "^9.0.1",
"jsdom": "^22.1.0",
"leaflet": "^1.9.4"
},
"dependencies": {
"bbox-fns": "^0.3.0",
"bbox-fns": "^0.11.0",
"get-epsg-code": "^0.1.0",
"preciso": "^0.12.0",
"reproject-bbox": "^0.6.0",
"reproject-bbox": "^0.10.0",
"reproject-geojson": "^0.3.0"
}
}
95 changes: 84 additions & 11 deletions src/geo-extent.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import multiply from "preciso/multiply.js";
import subtract from "preciso/subtract.js";

import bboxArray from "bbox-fns/bbox-array.js";
import booleanContains from "bbox-fns/boolean-contains.js";
import booleanIntersects from "bbox-fns/boolean-intersects.js";
import densePolygon from "bbox-fns/dense-polygon.js";

Expand Down Expand Up @@ -247,13 +248,45 @@ export class GeoExtent {
return new this.constructor(this);
}

contains(other) {
const [_this, _other] = this._pre(this, other);
_contains(other, { quiet = false } = { quiet: false }) {
try {
const [_this, _other] = this._pre(this, other);

return booleanContains(_this.bbox, _other.bbox);
} catch (error) {
if (!quiet) throw error;
}
}

const xContains = _other.xmin >= _this.xmin && _other.xmax <= _this.xmax;
const yContains = _other.ymin >= _this.ymin && _other.ymax <= _this.ymax;
contains(other, { debug_level = 0, quiet = true } = { debug_level: 0, quiet: true }) {
const result = this._contains(other, { quiet: true });
if (typeof result === "boolean") return result;

if (isDef(this.srs) && isDef(other.srs)) {
try {
// try reprojecting to projection of second bbox
const this2 = this.reproj(other.srs);
const result2 = this2._contains(other, { quiet: true });
if (typeof result2 === "boolean") return result2;
} catch (error) {
if (debug_level >= 1) console.error(error);
}

try {
// previous attempt was inconclusive, so try again by converting everything to 4326
const this4326 = this.reproj(4326);
const other4326 = other.reproj(4326);
const result4326 = this4326._contains(other4326, { quiet: true });
if (typeof result4326 === "boolean") return result4326;
} catch (error) {
if (debug_level >= 1) console.error(error);
}
}

return xContains && yContains;
if (!quiet)
throw new Error(
`[geo-extent] failed to determine if ${this.bbox} in srs ${this.srs} contains ${other.bbox} in srs ${other.srs}`
);
}

// should return null if no overlap
Expand Down Expand Up @@ -390,7 +423,15 @@ export class GeoExtent {
return false;
}

reproj(to, { density = "high", quiet = false } = { density: "high", quiet: false }) {
reproj(
to,
{ allow_infinity = false, debug_level = 0, density = "high", quiet = false } = {
allow_infinity: false,
debug_level: 0,
density: "high",
quiet: false
}
) {
to = normalize(to); // normalize srs

// don't need to reproject, so just return a clone
Expand Down Expand Up @@ -430,15 +471,47 @@ export class GeoExtent {
to
});
} catch (error) {
if (quiet) return;
throw new Error(`[geo-extent] failed to reproject ${this.bbox} from ${this.srs} to ${to}`);
if (debug_level) console.error(error);
}

if (reprojected.some(isNaN)) {
if (quiet) return;
if (reprojected?.every(isFinite)) {
return new GeoExtent(reprojected, { srs: to });
}
// as a fallback, try reprojecting to EPSG:4326 then to the desired srs
if (to !== 4326) {
let bbox_4326;
try {
bbox_4326 = reprojectBoundingBox({
bbox: this.bbox,
density,
from: this.srs,
to: 4326
});
} catch (error) {
if (debug_level) console.error("failed to create intermediary bbox in EPSG:4326");
}

if (bbox_4326) {
try {
reprojected = reprojectBoundingBox({
bbox: bbox_4326,
density,
from: 4326,
to
});
} catch (err) {
if (debug_level) console.error(`failed to reproject from intermediary bbox ${bbox_4326} in 4326 to ${to}`);
}
}
}

if (allow_infinity || reprojected?.every(isFinite)) {
return new GeoExtent(reprojected, { srs: to });
} else if (quiet) {
return;
} else {
throw new Error(`[geo-extent] failed to reproject ${this.bbox} from ${this.srs} to ${to}`);
}
return new GeoExtent(reprojected, { srs: to });
}

unwrap() {
Expand Down
16 changes: 16 additions & 0 deletions test/test.contains.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,19 @@ test("DC in Continental USA", ({ eq }) => {
eq(northernHemisphere.contains(usa), true);
eq(usa.contains(northernHemisphere), false);
});

test("check if UTM contains Web Mercator tile", ({ eq }) => {
// [geo-extent] failed to reproject -20037508.342789244,-7.081154551613622e-10,0,20037508.342789244 from EPSG:3857 to EPSG:32615
// equivalent to [-96.0417..., 29.5116..., -95.7808..., 29.6233...] in 4326
const utm = new GeoExtent([205437, 3268524, 230448, 3280290], { srs: "EPSG:32615" });

// top left quarter of the world
const tile = new GeoExtent([-20037508.342789244, -7.081154551613622e-10, 0, 20037508.342789244], {
srs: "EPSG:3857"
});

// aoi completely falls within tile
eq(utm.contains(tile), false);

eq(tile.contains(utm), true);
});
23 changes: 23 additions & 0 deletions test/test.reproj.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,26 @@ test("reproject extent that bends out", ({ eq }) => {
57.535885041786784
]);
});

test("reproj to inf", ({ eq }) => {
const bbox = [-10018754.171394622, -7.081154551613622e-10, 0, 10018754.171394624];
const srs = "EPSG:3857";

let msg;
try {
new GeoExtent(bbox, { srs }).reproj(26916, { debug: false, density: 100 });
} catch (error) {
msg = error.message;
}
eq(
msg,
"[geo-extent] failed to reproject -10018754.171394622,-7.081154551613622e-10,0,10018754.171394624 from EPSG:3857 to EPSG:26916"
);

eq(new GeoExtent(bbox, { srs }).reproj(26916, { allow_infinity: true }).bbox, [
166021.4430805326,
-187729840.1254552,
Infinity,
Infinity
]);
});
36 changes: 27 additions & 9 deletions types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,42 @@ export class GeoExtent {
area_str: string;
perimeter: number;
perimeter_str: string;
center: { x: number, y: number };
center_str: { x: string, y: string };
bottomLeft: { x: number, y: number };
bottomRight: { x: number, y: number };
topLeft: { x: number, y: number };
topRight: { x: number, y: number };
center: { x: number; y: number };
center_str: { x: string; y: string };
bottomLeft: { x: number; y: number };
bottomRight: { x: number; y: number };
topLeft: { x: number; y: number };
topRight: { x: number; y: number };
str: string;
leafletBounds: [[number, number], [number, number]];

// functions
asEsriJSON(): { xmin: number, ymin: number, xmax: number, ymax: number, spatialReference: { wkid: string }};
asGeoJSON(): { type: "Feature", geometry: { type: "Polygon", coordinates: [number[]]}};
asEsriJSON(): { xmin: number; ymin: number; xmax: number; ymax: number; spatialReference: { wkid: string } };
asGeoJSON(): { type: "Feature"; geometry: { type: "Polygon"; coordinates: [number[]] } };
clone(): GeoExtent;
combine(other: GeoExtent): GeoExtent;
contains(other: GeoExtent): GeoExtent;
crop(other: GeoExtent): GeoExtent;
equals: (other: GeoExtent, options?: { digits?: number }) => boolean;
overlaps(other: GeoExtent): boolean;
reproj: ((srs: number, options?: { density?: 'lowest' | 'low' | 'medium' | 'high' | 'higher' | 'highest' | number | undefined, quiet: false }) => GeoExtent) | ((srs: number, options: { density?: 'lowest' | 'low' | 'medium' | 'high' | 'higher' | 'highest' | number | undefined, quiet: true }) => (GeoExtent | undefined));
reproj:
| ((
srs: number,
options?: {
allow_infinity?: boolean | undefined;
density?: "lowest" | "low" | "medium" | "high" | "higher" | "highest" | number | undefined;
debug_level?: number | undefined;
quiet: false;
}
) => GeoExtent)
| ((
srs: number,
options: {
allow_infinity?: boolean;
debug_level?: number | undefined;
density?: "lowest" | "low" | "medium" | "high" | "higher" | "highest" | number | undefined;
quiet: true;
}
) => GeoExtent | undefined);
unwrap(): GeoExtent[];
}

0 comments on commit d5b8d02

Please sign in to comment.