Skip to content

Commit

Permalink
feat(geobase): project bounding box #96
Browse files Browse the repository at this point in the history
  • Loading branch information
navispatial committed Aug 10, 2023
1 parent fe1625a commit 12277d7
Show file tree
Hide file tree
Showing 7 changed files with 80 additions and 3 deletions.
1 change: 1 addition & 0 deletions dart/geobase/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ NOTE: Version 0.5.0 currently [under development](https://github.com/navibyte/ge
* [Alternative logics to resolve axis order #182](https://github.com/navibyte/geospatial/issues/182)
* [Handle explicit and implicit bounds in geometries and features on geobase #141](https://github.com/navibyte/geospatial/issues/141)
* [Add project method for Position class #184](https://github.com/navibyte/geospatial/issues/184)
* [Project bounding box #96](https://github.com/navibyte/geospatial/issues/96)

🛠 Refactoring:
* [Enforce "geometry" under "Feature" when encoding GeoJSON text output #91](https://github.com/navibyte/geospatial/issues/91)
Expand Down
8 changes: 8 additions & 0 deletions dart/geobase/lib/src/coordinates/base/box.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'dart:math' as math;

import '/src/codes/coords.dart';
import '/src/constants/epsilon.dart';
import '/src/coordinates/projection/projection.dart';
import '/src/utils/format_validation.dart';
import '/src/utils/num.dart';
import '/src/utils/tolerance.dart';
Expand Down Expand Up @@ -161,6 +162,13 @@ abstract class Box extends Positionable {
/// equals between min and max) or 4 positions (otherwise).
Iterable<Position> get corners2D;

/// Projects this bounding box to another box using [projection].
///
/// Subtypes may specify a more accurate bounding box type for the returned
/// object (for example a *geographic* bounding box would return a *projected*
/// box when forward-projecting, and other way when inverse-projecting).
Box project(Projection projection);

/// True if this box equals with [other] by testing 2D coordinates only.
///
/// Differences on 2D coordinate values (ie. x and y, or lon and lat) between
Expand Down
2 changes: 1 addition & 1 deletion dart/geobase/lib/src/coordinates/base/position.dart
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ abstract class Position extends Positionable {
/// measured position.
Position copyWith({num? x, num? y, num? z, num? m});

/// Projects this position to another position using the [projection].
/// Projects this position to another position using [projection].
///
/// Subtypes may specify a more accurate position type for the returned object
/// (for example a *geographic* position would return a *projected* position
Expand Down
17 changes: 17 additions & 0 deletions dart/geobase/lib/src/coordinates/geographic/geobox.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import 'package:meta/meta.dart';
import '/src/codes/coords.dart';
import '/src/coordinates/base/aligned.dart';
import '/src/coordinates/base/box.dart';
import '/src/coordinates/projected/projbox.dart';
import '/src/coordinates/projection/projection.dart';

import 'dms.dart';
import 'geographic.dart';
Expand Down Expand Up @@ -255,6 +257,21 @@ class GeoBox extends Box {
Iterable<Geographic> get corners2D =>
Box.createCorners2D(this, Geographic.create);

/// Projects this geographic bounding box to a projected box using
/// the forward [projection].
@override
ProjBox project(Projection projection) {
// get distinct corners (one, two or four) in 2D for the geographic bbox
final corners = corners2D;

// project all corner positions (using the forward projection)
final projected = corners.map((pos) => pos.project(projection));

// create a projected bbox
// (calculating min and max coords in all axes from corner positions)
return ProjBox.from(projected);
}

@override
int get spatialDimension => type.spatialDimension;

Expand Down
17 changes: 17 additions & 0 deletions dart/geobase/lib/src/coordinates/projected/projbox.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import 'package:meta/meta.dart';
import '/src/codes/coords.dart';
import '/src/coordinates/base/aligned.dart';
import '/src/coordinates/base/box.dart';
import '/src/coordinates/geographic/geobox.dart';
import '/src/coordinates/projection/projection.dart';

import 'projected.dart';

Expand Down Expand Up @@ -195,6 +197,21 @@ class ProjBox extends Box {
Iterable<Projected> get corners2D =>
Box.createCorners2D(this, Projected.create);

/// Projects this projected bounding box to a geographic box using
/// the inverse [projection].
@override
GeoBox project(Projection projection) {
// get distinct corners (one, two or four) in 2D for the projected bbox
final corners = corners2D;

// unproject all corner positions (using the inverse projection)
final unprojected = corners.map((pos) => pos.project(projection));

// create an unprojected (geographic) bbox
// (calculating min and max coords in all axes from corner positions)
return GeoBox.from(unprojected);
}

@override
int get spatialDimension => type.spatialDimension;

Expand Down
20 changes: 20 additions & 0 deletions dart/geobase/lib/src/vector_data/array/box_coords.dart
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ abstract class BoxCoords extends Box with _CoordinatesMixin {
return BoxCoords.view(list, type: type);
}

/// A minimum bounding box calculated from [positions].
///
/// Throws FormatException if cannot create (ie. [positions] is empty).
factory BoxCoords.from(Iterable<Position> positions) =>
Box.createBoxFrom(positions, BoxCoords.create);

/// Parses a bounding box with coordinate values parsed from [text].
///
/// Coordinate values in [text] are separated by [delimiter].
Expand Down Expand Up @@ -177,6 +183,20 @@ class _BoxCoordsImpl extends BoxCoords {
Iterable<Position> get corners2D =>
Box.createCorners2D(this, Projected.create);

/// Projects this bounding box to another box using [projection].
@override
BoxCoords project(Projection projection) {
// get distinct corners (one, two or four) in 2D for the bounding bbox
final corners = corners2D;

// project all corner positions (using the projection)
final projected = corners.map((pos) => pos.project(projection));

// create a new bounding bbox
// (calculating min and max coords in all axes from corner positions)
return BoxCoords.from(projected);
}

@override
PositionCoords get min => _doCreateRange(
_data,
Expand Down
18 changes: 16 additions & 2 deletions dart/geobase/test/projections/projection_web_mercator_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ import 'projection_sample.dart';

void main() {
group('Test projections between WGS84 and Web Mercator', () {
final toWebMercator = WGS84.webMercator.forward;
final toWgs84 = WGS84.webMercator.inverse;

test('webMercatorToWgs84(Projected to Geographic)', () {
final toWgs84 = WGS84.webMercator.inverse;
for (final coords in wgs84ToWebMercatorData) {
final point2 = Projected(x: coords[2], y: coords[3]);
final geoPoint2 = Geographic(lon: coords[0], lat: coords[1]);
Expand All @@ -37,7 +39,6 @@ void main() {
});

test('wgs84ToWebMercator(Geographic to Projected)', () {
final toWebMercator = WGS84.webMercator.forward;
for (final coords in wgs84ToWebMercatorData) {
final geoPoint3 = Geographic(lon: coords[0], lat: coords[1]);
final point3 = Projected(x: coords[2], y: coords[3]);
Expand All @@ -53,6 +54,19 @@ void main() {
);
}
});

test('project bbox between GeoBox and ProjBox', () {
const gb = GeoBox(
west: 10.19238847,
south: -11.349348834,
east: 15.23095884,
north: 21.094852974,
);
final wm = gb.project(toWebMercator);
final gb2 = wm.project(toWgs84);
expect(gb.toText(decimals: 8), gb2.toText(decimals: 8));
expect(gb.equals2D(gb2), true);
});
});

group('Test flat coordinate arrays', () {
Expand Down

0 comments on commit 12277d7

Please sign in to comment.