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

refactor!: make LatLngBounds mutable & depend less on 'latlong2' #1834

Merged
merged 10 commits into from
Mar 12, 2024
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
Loading