-
-
Notifications
You must be signed in to change notification settings - Fork 860
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor!: make
LatLngBounds
mutable & depend less on 'latlong2' (#…
…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
Showing
2 changed files
with
174 additions
and
117 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)'; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters