Skip to content

Commit 7b5e966

Browse files
authored
Implement Square (#213)
1 parent 24aa197 commit 7b5e966

File tree

8 files changed

+322
-0
lines changed

8 files changed

+322
-0
lines changed

lib/square.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
library turf_square;
2+
3+
export 'package:geotypes/geotypes.dart';
4+
export 'src/square.dart';

lib/src/square.dart

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import 'package:turf/distance.dart';
2+
import 'package:turf/turf.dart';
3+
4+
/// Takes a [BBox] and returns a square [BBox] of equal dimensions.
5+
BBox square(BBox bbox) {
6+
final horizontalDistance = distance(
7+
Point(coordinates: Position.named(lng: bbox.lng1, lat: bbox.lat1)),
8+
Point(coordinates: Position.named(lng: bbox.lng2, lat: bbox.lat1))
9+
);
10+
final verticalDistance = distance(
11+
Point(coordinates: Position.named(lng: bbox.lng1, lat: bbox.lat1)),
12+
Point(coordinates: Position.named(lng: bbox.lng1, lat: bbox.lat2))
13+
);
14+
15+
if (horizontalDistance >= verticalDistance) {
16+
final verticalMidpoint = (bbox.lat1 + bbox.lat2) / 2;
17+
return BBox.named(
18+
lng1: bbox.lng1,
19+
lat1: verticalMidpoint - (bbox.lng2 - bbox.lng1) / 2,
20+
lng2: bbox.lng2,
21+
lat2: verticalMidpoint + (bbox.lng2 - bbox.lng1) / 2,
22+
);
23+
} else {
24+
final horizontalMidpoint = (bbox.lng1 + bbox.lng2) / 2;
25+
return BBox.named(
26+
lng1: horizontalMidpoint - (bbox.lat2 - bbox.lat1) / 2,
27+
lat1: bbox.lat1,
28+
lng2: horizontalMidpoint + (bbox.lat2 - bbox.lat1) / 2,
29+
lat2: bbox.lat2,
30+
);
31+
}
32+
}

lib/turf.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,6 @@ export 'point_to_line_distance.dart';
3232
export 'polygon_smooth.dart';
3333
export 'polygon_to_line.dart';
3434
export 'polyline.dart';
35+
export 'square.dart';
3536
export 'transform.dart';
3637
export 'truncate.dart';

test/components/square_test.dart

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import 'dart:convert';
2+
import 'dart:io';
3+
import 'package:test/test.dart';
4+
import 'package:turf/turf.dart';
5+
6+
void main() {
7+
group('Square BBox Transformation', () {
8+
// Unit tests for specific scenarios
9+
test('Square a horizontal rectangle', () {
10+
// Input: Rectangle wider than tall
11+
final bbox = BBox.named(lng1: 0, lat1: 0, lng2: 10, lat2: 5);
12+
13+
final squaredBBox = square(bbox);
14+
15+
// Verify square dimensions
16+
expect(squaredBBox.lng2 - squaredBBox.lng1,
17+
squaredBBox.lat2 - squaredBBox.lat1,
18+
reason: 'BBox should be a perfect square');
19+
20+
// Verify center point remains the same
21+
expect((squaredBBox.lng1 + squaredBBox.lng2) / 2,
22+
(bbox.lng1 + bbox.lng2) / 2,
23+
reason: 'Center longitude should remain the same');
24+
expect((squaredBBox.lat1 + squaredBBox.lat2) / 2,
25+
(bbox.lat1 + bbox.lat2) / 2,
26+
reason: 'Center latitude should remain the same');
27+
});
28+
29+
test('Square a vertical rectangle', () {
30+
// Input: Rectangle taller than wide
31+
final bbox = BBox.named(lng1: 0, lat1: 0, lng2: 5, lat2: 10);
32+
33+
final squaredBBox = square(bbox);
34+
35+
// Verify square dimensions
36+
expect(squaredBBox.lng2 - squaredBBox.lng1,
37+
squaredBBox.lat2 - squaredBBox.lat1,
38+
reason: 'BBox should be a perfect square');
39+
40+
// Verify center point remains the same
41+
expect((squaredBBox.lng1 + squaredBBox.lng2) / 2,
42+
(bbox.lng1 + bbox.lng2) / 2,
43+
reason: 'Center longitude should remain the same');
44+
expect((squaredBBox.lat1 + squaredBBox.lat2) / 2,
45+
(bbox.lat1 + bbox.lat2) / 2,
46+
reason: 'Center latitude should remain the same');
47+
});
48+
49+
test('Square an already square BBox', () {
50+
// Input: Already square BBox
51+
final bbox = BBox.named(lng1: 0, lat1: 0, lng2: 10, lat2: 10);
52+
53+
final squaredBBox = square(bbox);
54+
55+
// Verify dimensions remain the same
56+
expect(squaredBBox.lng2 - squaredBBox.lng1,
57+
squaredBBox.lat2 - squaredBBox.lat1,
58+
reason: 'BBox should remain a square');
59+
60+
expect(squaredBBox.lng1, bbox.lng1);
61+
expect(squaredBBox.lat1, bbox.lat1);
62+
expect(squaredBBox.lng2, bbox.lng2);
63+
expect(squaredBBox.lat2, bbox.lat2);
64+
});
65+
66+
test('Square a BBox with negative coordinates', () {
67+
// Input: BBox with negative coordinates
68+
final bbox = BBox.named(lng1: -10, lat1: -5, lng2: 0, lat2: 5);
69+
70+
final squaredBBox = square(bbox);
71+
72+
// Verify square dimensions
73+
expect(squaredBBox.lng2 - squaredBBox.lng1,
74+
squaredBBox.lat2 - squaredBBox.lat1,
75+
reason: 'BBox should be a perfect square');
76+
77+
// Verify center point remains the same
78+
expect((squaredBBox.lng1 + squaredBBox.lng2) / 2,
79+
(bbox.lng1 + bbox.lng2) / 2,
80+
reason: 'Center longitude should remain the same');
81+
expect((squaredBBox.lat1 + squaredBBox.lat2) / 2,
82+
(bbox.lat1 + bbox.lat2) / 2,
83+
reason: 'Center latitude should remain the same');
84+
});
85+
86+
// File-based test for real-world scenarios
87+
// File-based test for real-world scenarios
88+
group('File-based Tests', () {
89+
var inDir = Directory('./test/examples/square/in');
90+
if (inDir.existsSync()) {
91+
for (var file in inDir.listSync(recursive: true)) {
92+
if (file is File && file.path.endsWith('.geojson')) {
93+
test(file.path, () {
94+
var inSource = file.readAsStringSync();
95+
var inJson = jsonDecode(inSource);
96+
97+
// Get the first feature
98+
var feature = inJson['features'][0];
99+
if (feature['geometry']['bbox'] == null) {
100+
throw Exception("Missing 'bbox' in GeoJSON feature geometry");
101+
}
102+
103+
// Extract bbox from the geometry
104+
var bbox = feature['geometry']['bbox'];
105+
106+
// Create BBox instance
107+
var geoBbox = BBox.named(
108+
lng1: bbox[0], // min longitude
109+
lat1: bbox[1], // min latitude
110+
lng2: bbox[2], // max longitude
111+
lat2: bbox[3], // max latitude
112+
);
113+
114+
// Perform square operation (this doesn't change geometry type)
115+
BBox squaredBBox = square(geoBbox);
116+
117+
// Create updated feature with new bbox but original geometry
118+
var updatedFeature = Map<String, dynamic>.from(feature);
119+
updatedFeature['geometry'] =
120+
Map<String, dynamic>.from(feature['geometry']);
121+
updatedFeature['geometry']['bbox'] = [
122+
squaredBBox.lng1,
123+
squaredBBox.lat1,
124+
squaredBBox.lng2,
125+
squaredBBox.lat2,
126+
];
127+
128+
// Prepare output path
129+
var outPath = file.path.replaceAll('/in', '/out');
130+
var outFile = File(outPath);
131+
if (!outFile.existsSync()) {
132+
print('Warning: Output file not found at $outPath');
133+
return;
134+
}
135+
136+
var outSource = outFile.readAsStringSync();
137+
var expectedOutput = jsonDecode(outSource);
138+
var expectedFeature = expectedOutput['features'][0];
139+
140+
// Verify feature collection structure
141+
expect(expectedOutput['type'], 'FeatureCollection');
142+
143+
// Compare geometry types (should remain unchanged)
144+
expect(
145+
updatedFeature['geometry']['type'],
146+
expectedFeature['geometry']['type'],
147+
reason: 'Geometry type should remain the same',
148+
);
149+
150+
// Compare coordinates (should remain unchanged)
151+
expect(
152+
updatedFeature['geometry']['coordinates'],
153+
expectedFeature['geometry']['coordinates'],
154+
reason: 'Coordinates should remain unchanged',
155+
);
156+
157+
// Compare the updated bbox values
158+
expect(
159+
updatedFeature['geometry']['bbox'],
160+
expectedFeature['geometry']['bbox'],
161+
reason: 'BBox should match expected squared version',
162+
);
163+
});
164+
}
165+
}
166+
}
167+
});
168+
});
169+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"type": "FeatureCollection",
3+
"features": [
4+
{
5+
"type": "Feature",
6+
"properties": {},
7+
"geometry": {
8+
"type": "LineString",
9+
"coordinates": [
10+
[
11+
100.0,
12+
0
13+
],
14+
[
15+
110.0,
16+
1
17+
]
18+
],
19+
"bbox": [
20+
100.0,
21+
0.0,
22+
110.0,
23+
1.0
24+
]
25+
}
26+
}
27+
]
28+
}
29+
30+
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"type": "FeatureCollection",
3+
"features": [
4+
{
5+
"type": "Feature",
6+
"properties": {},
7+
"geometry": {
8+
"type": "LineString",
9+
"coordinates": [
10+
[
11+
100,
12+
0
13+
],
14+
[
15+
101,
16+
10
17+
]
18+
],
19+
"bbox": [
20+
95.5,
21+
0,
22+
105.5,
23+
10
24+
]
25+
}
26+
}
27+
]
28+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"type": "FeatureCollection",
3+
"features": [
4+
{
5+
"type": "Feature",
6+
"properties": {},
7+
"geometry": {
8+
"type": "LineString",
9+
"coordinates": [
10+
[
11+
100,
12+
0
13+
],
14+
[
15+
110,
16+
1
17+
]
18+
],
19+
"bbox": [
20+
100.0,
21+
-4.5,
22+
110.0,
23+
5.5
24+
]
25+
}
26+
}
27+
]
28+
}
29+
30+
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"type": "FeatureCollection",
3+
"features": [
4+
{
5+
"type": "Feature",
6+
"properties": {},
7+
"geometry": {
8+
"type": "LineString",
9+
"coordinates": [
10+
[
11+
100,
12+
0
13+
],
14+
[
15+
101,
16+
10
17+
]
18+
],
19+
"bbox": [
20+
95.5,
21+
0,
22+
105.5,
23+
10
24+
]
25+
}
26+
}
27+
]
28+
}

0 commit comments

Comments
 (0)