Skip to content

Commit

Permalink
refactor!: make LatLngBounds mutable & depend less on 'latlong2' (#…
Browse files Browse the repository at this point in the history
…1834)

* Update latlng_bounds.dart

* update tests and example app

* update usages of LatLngBounds in package

* make LatLngBounds extend functions safe

* requested changes from #1834 (comment)

* improve documentation

* add `toString()`

* fix copy paste error
  • Loading branch information
josxha authored Mar 12, 2024
1 parent bf70478 commit 0fef8d6
Show file tree
Hide file tree
Showing 2 changed files with 174 additions and 117 deletions.
277 changes: 169 additions & 108 deletions lib/src/geo/latlng_bounds.dart
Original file line number Diff line number Diff line change
@@ -1,159 +1,220 @@
import 'dart:math' as math;
import 'dart:math';

import 'package:latlong2/latlong.dart';
import 'package:vector_math/vector_math_64.dart';

/// Data structure representing rectangular bounding box constrained by its
/// northwest and southeast corners
class LatLngBounds {
late LatLng _sw;
late LatLng _ne;
/// The latitude north edge of the bounds
double north;

/// The latitude south edge of the bounds
double south;

/// The longitude east edge of the bounds
double east;

/// The longitude west edge of the bounds
double west;

/// Create new [LatLngBounds] by providing two corners. Both corners have to
/// be on opposite sites but it doesn't matter which opposite corners or in
/// what order the corners are provided.
LatLngBounds(
LatLng corner1,
LatLng corner2,
) : this.fromPoints([corner1, corner2]);
///
/// If you want to create [LatLngBounds] with raw values, use the
/// [LatLngBounds.unsafe] constructor instead.
factory LatLngBounds(LatLng corner1, LatLng corner2) {
final double minX;
final double maxX;
final double minY;
final double maxY;
if (corner1.longitude >= corner2.longitude) {
maxX = corner1.longitude;
minX = corner2.longitude;
} else {
maxX = corner2.longitude;
minX = corner1.longitude;
}
if (corner1.latitude >= corner2.latitude) {
maxY = corner1.latitude;
minY = corner2.latitude;
} else {
maxY = corner2.latitude;
minY = corner1.latitude;
}
return LatLngBounds.unsafe(
north: maxY,
south: minY,
east: maxX,
west: minX,
);
}

/// Create a [LatLngBounds] instance from raw edge values.
///
/// Potentially throws assertion errors if the coordinates exceed their max
/// or min values or if coordinates are meant to be smaller / bigger
/// but aren't.
LatLngBounds.unsafe({
required this.north,
required this.south,
required this.east,
required this.west,
}) : assert(
north <= 90, "The north latitude can't be bigger than 90: $north"),
assert(north >= -90,
"The north latitude can't be smaller than -90: $north"),
assert(
south <= 90, "The south latitude can't be bigger than 90: $south"),
assert(south >= -90,
"The south latitude can't be smaller than -90: $south"),
assert(
east <= 180, "The east longitude can't be bigger than 180: $east"),
assert(east >= -180,
"The east longitude can't be smaller than -180: $east"),
assert(
west <= 180, "The west longitude can't be bigger than 180: $west"),
assert(west >= -180,
"The west longitude can't be smaller than -180: $west"),
assert(north >= south,
"The north latitude can't be smaller than the south latitude"),
assert(east >= west,
"The west longitude can't be smaller than the east longitude");

/// Create a new [LatLngBounds] from a list of [LatLng] points. This
/// calculates the bounding box of the provided points.
LatLngBounds.fromPoints(List<LatLng> points)
: assert(
points.isNotEmpty,
'LatLngBounds cannot be created with an empty List of LatLng',
) {
factory LatLngBounds.fromPoints(List<LatLng> points) {
assert(
points.isNotEmpty,
'LatLngBounds cannot be created with an empty List of LatLng',
);
// initialize bounds with max values.
double minX = 180;
double maxX = -180;
double minY = 90;
double maxY = -90;

// find the largest and smallest latitude and longitude
for (final point in points) {
minX = math.min<double>(minX, point.longitude);
minY = math.min<double>(minY, point.latitude);
maxX = math.max<double>(maxX, point.longitude);
maxY = math.max<double>(maxY, point.latitude);
if (point.longitude < minX) minX = point.longitude;
if (point.longitude > maxX) maxX = point.longitude;
if (point.latitude < minY) minY = point.latitude;
if (point.latitude > maxY) maxY = point.latitude;
}

_sw = LatLng(minY, minX);
_ne = LatLng(maxY, maxX);
return LatLngBounds.unsafe(
north: maxY,
south: minY,
east: maxX,
west: minX,
);
}

/// Expands bounding box by [latLng] coordinate point. This method mutates
/// the bounds object on which it is called.
void extend(LatLng latLng) {
_extend(latLng, latLng);
north = min(90, max(north, latLng.latitude));
south = max(-90, min(south, latLng.latitude));
east = min(180, max(east, latLng.longitude));
west = max(-180, min(west, latLng.longitude));
}

/// Expands bounding box by other [bounds] object. If provided [bounds] object
/// is smaller than current one, it is not shrunk. This method mutates
/// the bounds object on which it is called.
void extendBounds(LatLngBounds bounds) {
_extend(bounds._sw, bounds._ne);
north = min(90, max(north, bounds.north));
south = max(-90, min(south, bounds.south));
east = min(180, max(east, bounds.east));
west = max(-180, min(west, bounds.west));
}

void _extend(LatLng sw2, LatLng ne2) {
_sw = LatLng(
math.min(sw2.latitude, _sw.latitude),
math.min(sw2.longitude, _sw.longitude),
);
_ne = LatLng(
math.max(ne2.latitude, _ne.latitude),
math.max(ne2.longitude, _ne.longitude),
);
}

/// Obtain west edge of the bounds
double get west => southWest.longitude;

/// Obtain south edge of the bounds
double get south => southWest.latitude;

/// Obtain east edge of the bounds
double get east => northEast.longitude;

/// Obtain north edge of the bounds
double get north => northEast.latitude;

/// Obtain coordinates of southwest corner of the bounds
LatLng get southWest => _sw;

/// Obtain coordinates of northeast corner of the bounds
LatLng get northEast => _ne;

/// Obtain coordinates of northwest corner of the bounds
/// Obtain coordinates of southwest corner of the bounds.
///
/// Instead of using latitude or longitude of the corner, use [south] or
/// [west] instead!
LatLng get southWest => LatLng(south, west);

/// Obtain coordinates of northeast corner of the bounds.
///
/// Instead of using latitude or longitude of the corner, use [north] or
/// [east] instead!
LatLng get northEast => LatLng(north, east);

/// Obtain coordinates of northwest corner of the bounds.
///
/// Instead of using latitude or longitude of the corner, use [north] or
/// [west] instead!
LatLng get northWest => LatLng(north, west);

/// Obtain coordinates of southeast corner of the bounds
/// Obtain coordinates of southeast corner of the bounds.
///
/// Instead of using latitude or longitude of the corner, use [south] or
/// [east] instead!
LatLng get southEast => LatLng(south, east);

/// Obtain coordinates of the bounds center
LatLng get center {
/* https://stackoverflow.com/a/4656937
http://www.movable-type.co.uk/scripts/latlong.html
coord 1: southWest
coord 2: northEast
phi: lat
lambda: lng
*/

final phi1 = southWest.latitudeInRad;
final lambda1 = southWest.longitudeInRad;
final phi2 = northEast.latitudeInRad;

final dLambda = degrees2Radians *
(northEast.longitude -
southWest.longitude); // delta lambda = lambda2-lambda1

final bx = math.cos(phi2) * math.cos(dLambda);
final by = math.cos(phi2) * math.sin(dLambda);
final phi3 = math.atan2(math.sin(phi1) + math.sin(phi2),
math.sqrt((math.cos(phi1) + bx) * (math.cos(phi1) + bx) + by * by));
final lambda3 = lambda1 + math.atan2(by, math.cos(phi1) + bx);
// https://stackoverflow.com/a/4656937
// http://www.movable-type.co.uk/scripts/latlong.html
// coord 1: southWest
// coord 2: northEast
// phi: lat
// lambda: lng

final phi1 = south * degrees2Radians;
final lambda1 = west * degrees2Radians;
final phi2 = north * degrees2Radians;

// delta lambda = lambda2-lambda1
final dLambda = degrees2Radians * (east - west);

final bx = cos(phi2) * cos(dLambda);
final by = cos(phi2) * sin(dLambda);
final phi3 = atan2(sin(phi1) + sin(phi2),
sqrt((cos(phi1) + bx) * (cos(phi1) + bx) + by * by));
final lambda3 = lambda1 + atan2(by, cos(phi1) + bx);

// phi3 and lambda3 are actually in radians and LatLng wants degrees
return LatLng(phi3 * radians2Degrees, lambda3 * radians2Degrees);
}

/// Checks whether [point] is inside bounds
bool contains(LatLng point) {
final sw2 = point;
final ne2 = point;
return containsBounds(LatLngBounds(sw2, ne2));
}

/// Checks whether [bounds] is contained inside bounds
bool containsBounds(LatLngBounds bounds) {
final sw2 = bounds._sw;
final ne2 = bounds._ne;
return (sw2.latitude >= _sw.latitude) &&
(ne2.latitude <= _ne.latitude) &&
(sw2.longitude >= _sw.longitude) &&
(ne2.longitude <= _ne.longitude);
}

/// Checks whether at least one edge of [bounds] is overlapping with some
/// other edge of bounds
bool isOverlapping(LatLngBounds bounds) {
/* check if bounding box rectangle is outside the other, if it is then it's
considered not overlapping
*/
if (_sw.latitude > bounds._ne.latitude ||
_ne.latitude < bounds._sw.latitude ||
_ne.longitude < bounds._sw.longitude ||
_sw.longitude > bounds._ne.longitude) {
return false;
}
return true;
}
bool contains(LatLng point) =>
point.longitude >= west &&
point.longitude <= east &&
point.latitude >= south &&
point.latitude <= north;

/// Checks whether the [other] bounding box is contained inside bounds.
bool containsBounds(LatLngBounds other) =>
other.south >= south &&
other.north <= north &&
other.west >= west &&
other.east <= east;

/// Checks whether at least one edge of the [other] bounding box is
/// overlapping with this bounding box.
///
/// Bounding boxes that touch each other but don't overlap are counted as
/// not overlapping.
bool isOverlapping(LatLngBounds other) => !(south > other.north ||
north < other.south ||
east < other.west ||
west > other.east);

@override
int get hashCode => Object.hash(_sw, _ne);
int get hashCode => Object.hash(south, north, east, west);

@override
bool operator ==(Object other) =>
other is LatLngBounds && other._sw == _sw && other._ne == _ne;
identical(this, other) ||
(other is LatLngBounds &&
other.north == north &&
other.south == south &&
other.east == east &&
other.west == west);

@override
String toString() =>
'LatLngBounds(north: $north, south: $south, east: $east, west: $west)';
}
14 changes: 5 additions & 9 deletions lib/src/layer/polyline_layer/polyline_layer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -167,15 +167,11 @@ class _PolylineLayerState<R extends Object> extends State<PolylineLayer<R>> {

// The min(-90), max(180), ... are used to get around the limits of LatLng
// the value cannot be greater or smaller than that
final boundsAdjusted = LatLngBounds(
LatLng(
math.max(-90, bounds.southWest.latitude - margin),
math.max(-180, bounds.southWest.longitude - margin),
),
LatLng(
math.min(90, bounds.northEast.latitude + margin),
math.min(180, bounds.northEast.longitude + margin),
),
final boundsAdjusted = LatLngBounds.unsafe(
west: math.max(-180, bounds.west - margin),
east: math.min(90, bounds.east + margin),
south: math.max(-90, bounds.south - margin),
north: math.min(180, bounds.north + margin),
);

// segment is visible
Expand Down

0 comments on commit 0fef8d6

Please sign in to comment.