Skip to content

Commit c3ac0d2

Browse files
committed
Cell towers in the map!
1 parent 3668a5d commit c3ac0d2

9 files changed

Lines changed: 277 additions & 5 deletions

File tree

‎lib/components/base/bars/top_bar.dart‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ class _TopBarState extends State<TopBar> {
141141

142142
showModalBottomSheet(
143143
context: context,
144+
showDragHandle: true,
144145
shape: const RoundedRectangleBorder(
145146
borderRadius: BorderRadius.vertical(top: Radius.circular(24.0)),
146147
),

‎lib/components/base/body/map/map.dart‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import 'package:netmanager/components/base/body/map/widgets/live_map.dart';
88
import 'package:netmanager/components/base/body/map/widgets/record_card.dart';
99
import 'package:netmanager/components/dialogs/error.dart';
1010
import 'package:netmanager/components/modals/record_modal.dart';
11+
import 'package:netmanager/database/cell_database.dart';
12+
import 'package:netmanager/types/database/cell_tower.dart';
1113
import 'package:netmanager/utils/cell_utils.dart';
1214
import 'package:netmanager/components/base/body/map/widgets/map_overlay.dart';
1315
import 'package:netmanager/types/cell/sim_data.dart';
@@ -69,6 +71,10 @@ class _MapBodyState extends State<MapBody> with SingleTickerProviderStateMixin {
6971
String signalStrength = "N/A";
7072
String signalStrengthString = "N/A";
7173

74+
LatLngBounds? _cachedBounds;
75+
List<CellTower> _cachedCellTowers = [];
76+
String _lastPlmn = "";
77+
7278
final ValueNotifier<List<String>> displayTitlesNotifier = ValueNotifier([
7379
"Speed",
7480
"Cell ID",
@@ -147,6 +153,7 @@ class _MapBodyState extends State<MapBody> with SingleTickerProviderStateMixin {
147153

148154
showModalBottomSheet(
149155
context: context,
156+
showDragHandle: true,
150157
shape: const RoundedRectangleBorder(
151158
borderRadius: BorderRadius.vertical(top: Radius.circular(24.0)),
152159
),
@@ -315,6 +322,7 @@ class _MapBodyState extends State<MapBody> with SingleTickerProviderStateMixin {
315322

316323
try {
317324
simData = SIMData.fromJson(map);
325+
_lastPlmn = simData.networkPlmn;
318326
} catch (e) {
319327
return;
320328
}
@@ -434,6 +442,46 @@ class _MapBodyState extends State<MapBody> with SingleTickerProviderStateMixin {
434442
});
435443
}
436444

445+
void checkAndLoadTowers(LatLngBounds visibleBounds) async {
446+
try {
447+
if (_lastPlmn.isEmpty) return;
448+
449+
if (_cachedBounds != null &&
450+
visibleBounds.southWest.latitude >=
451+
_cachedBounds!.southWest.latitude &&
452+
visibleBounds.northEast.latitude <=
453+
_cachedBounds!.northEast.latitude &&
454+
visibleBounds.southWest.longitude >=
455+
_cachedBounds!.southWest.longitude &&
456+
visibleBounds.northEast.longitude <=
457+
_cachedBounds!.northEast.longitude) {
458+
return;
459+
}
460+
461+
const double spatialPadding = 0.03; // todo check if that's ok
462+
final double minLat = visibleBounds.southWest.latitude - spatialPadding;
463+
final double maxLat = visibleBounds.northEast.latitude + spatialPadding;
464+
final double minLng = visibleBounds.southWest.longitude - spatialPadding;
465+
final double maxLng = visibleBounds.northEast.longitude + spatialPadding;
466+
467+
final towers = await CellDatabase.fetchMapCellTowers(
468+
plmn: _lastPlmn,
469+
minLat: minLat,
470+
maxLat: maxLat,
471+
minLng: minLng,
472+
maxLng: maxLng,
473+
);
474+
475+
setState(() {
476+
_cachedCellTowers = towers;
477+
_cachedBounds = LatLngBounds(
478+
LatLng(minLat, minLng),
479+
LatLng(maxLat, maxLng),
480+
);
481+
});
482+
} catch (e) {}
483+
}
484+
437485
@override
438486
Widget build(BuildContext context) {
439487
return Column(
@@ -458,6 +506,10 @@ class _MapBodyState extends State<MapBody> with SingleTickerProviderStateMixin {
458506
activeReplayData: _activeReplayData,
459507
followUser: _follow,
460508
sharedPreferences: sharedPreferences,
509+
cellTowers: _cachedCellTowers,
510+
onPositionChanged: (camera) {
511+
checkAndLoadTowers(camera.visibleBounds);
512+
},
461513
onMarkerTap: (record) {
462514
setState(() {
463515
_selectedRecord = record;
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_map/flutter_map.dart';
3+
import 'package:netmanager/components/modals/tower_modal.dart';
4+
import 'package:netmanager/types/database/cell_tower.dart';
5+
6+
class CellTowers extends StatelessWidget {
7+
final List<CellTower> cellTowers;
8+
9+
const CellTowers({super.key, required this.cellTowers});
10+
11+
@override
12+
Widget build(BuildContext context) {
13+
return MarkerLayer(
14+
markers: cellTowers.map((tower) {
15+
return Marker(
16+
point: tower.getLatLng(),
17+
width: 24,
18+
height: 24,
19+
child: GestureDetector(
20+
onTap: () => showModalBottomSheet(
21+
context: context,
22+
showDragHandle: true,
23+
shape: const RoundedRectangleBorder(
24+
borderRadius: BorderRadius.vertical(top: Radius.circular(24.0)),
25+
),
26+
backgroundColor: Theme.of(context).colorScheme.surface,
27+
builder: (BuildContext context) {
28+
return towerModal(context, tower);
29+
},
30+
),
31+
child: Container(
32+
decoration: BoxDecoration(
33+
color: Theme.of(
34+
context,
35+
).colorScheme.surface.withValues(alpha: 0.8),
36+
shape: BoxShape.circle,
37+
),
38+
child: Icon(
39+
Icons.cell_tower_rounded,
40+
size: 18,
41+
color: Theme.of(context).colorScheme.secondary,
42+
),
43+
),
44+
),
45+
);
46+
}).toList(),
47+
);
48+
}
49+
}

‎lib/components/base/body/map/widgets/live_map.dart‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import 'package:flutter/material.dart';
22
import 'package:flutter_map/flutter_map.dart';
33
import 'package:latlong2/latlong.dart';
4+
import 'package:netmanager/components/base/body/map/widgets/cell_towers.dart';
45
import 'package:netmanager/components/base/body/map/widgets/location_dot.dart';
56
import 'package:netmanager/components/base/body/map/widgets/visual_records.dart';
7+
import 'package:netmanager/types/database/cell_tower.dart';
68
import 'package:netmanager/utils/map_tile_builder.dart';
79
import 'package:netmanager/types/recording/record.dart';
810
import 'package:netmanager/types/recording/recorded_data.dart';
@@ -17,6 +19,8 @@ class LiveMap extends StatelessWidget {
1719
final RecordedData? activeReplayData;
1820
final bool followUser;
1921
final SharedPreferences sharedPreferences;
22+
final List<CellTower> cellTowers;
23+
final Function(MapCamera camera) onPositionChanged;
2024
final ValueChanged<Record> onMarkerTap;
2125
final ValueChanged<bool> onMapInteraction;
2226
final VoidCallback onMapReady;
@@ -36,6 +40,8 @@ class LiveMap extends StatelessWidget {
3640
required this.activeReplayData,
3741
required this.followUser,
3842
required this.sharedPreferences,
43+
required this.cellTowers,
44+
required this.onPositionChanged,
3945
required this.onMarkerTap,
4046
required this.onMapInteraction,
4147
required this.onMapReady,
@@ -65,6 +71,9 @@ class LiveMap extends StatelessWidget {
6571
minZoom: 7.0,
6672
maxZoom: 16.0,
6773
initialZoom: 14.0,
74+
onPositionChanged: (camera, hasGesture) {
75+
onPositionChanged(camera);
76+
},
6877
interactionOptions: InteractionOptions(
6978
flags:
7079
InteractiveFlag.pinchZoom |
@@ -103,6 +112,7 @@ class LiveMap extends StatelessWidget {
103112
selectedRecord: selectedRecord,
104113
onMarkerTap: onMarkerTap,
105114
),
115+
if (cellTowers.isNotEmpty) CellTowers(cellTowers: cellTowers),
106116
if (currentLocation != null)
107117
MarkerLayer(
108118
markers: [

‎lib/components/base/body/speedtest/speedtest.dart‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,7 @@ class _SpeedtestBodyState extends State<SpeedtestBody> {
331331
: () {
332332
showModalBottomSheet(
333333
context: context,
334+
showDragHandle: true,
334335
shape: const RoundedRectangleBorder(
335336
borderRadius: BorderRadius.vertical(
336337
top: Radius.circular(24.0),
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:netmanager/types/database/cell_tower.dart';
3+
4+
Widget towerModal(BuildContext context, CellTower cellTower) {
5+
return Padding(
6+
padding: const EdgeInsets.fromLTRB(16.0, 4.0, 16.0, 16.0),
7+
child: Column(
8+
mainAxisSize: MainAxisSize.min,
9+
crossAxisAlignment: CrossAxisAlignment.start,
10+
children: [
11+
Text(
12+
"Cell tower",
13+
style: Theme.of(
14+
context,
15+
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
16+
),
17+
Text(
18+
cellTower.getLatLng().toSexagesimal(),
19+
style: Theme.of(context).textTheme.bodySmall,
20+
),
21+
const SizedBox(height: 8),
22+
const Divider(),
23+
Flexible(
24+
child: ListView.builder(
25+
shrinkWrap: true,
26+
itemCount: cellTower.cells.length,
27+
itemBuilder: (context, index) {
28+
final cell = cellTower.cells[index];
29+
final Color genColor = cell.networkGen == 5
30+
? Theme.of(context).colorScheme.tertiary
31+
: Theme.of(context).colorScheme.primary;
32+
33+
return ListTile(
34+
dense: true,
35+
contentPadding: EdgeInsets.zero,
36+
leading: CircleAvatar(
37+
radius: 15,
38+
backgroundColor: genColor.withValues(alpha: 0.15),
39+
child: Text(
40+
"${cell.networkGen}G",
41+
style: TextStyle(
42+
fontSize: 10,
43+
fontWeight: FontWeight.bold,
44+
color: genColor,
45+
),
46+
),
47+
),
48+
title: Text(
49+
cell.description,
50+
style: const TextStyle(fontWeight: FontWeight.w600),
51+
),
52+
subtitle: Text(
53+
"CID: ${cell.cid}. ${cell.channelNumber != null ? "ARFCN: ${cell.channelNumber}" : ""}",
54+
),
55+
);
56+
},
57+
),
58+
),
59+
],
60+
),
61+
);
62+
}

‎lib/database/cell_database.dart‎

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import 'package:latlong2/latlong.dart';
2+
import 'package:netmanager/types/database/cell_tower.dart';
3+
import 'package:netmanager/types/database/database_cell.dart';
14
import 'package:sqflite/sqflite.dart';
25

36
class CellDatabase {
@@ -32,6 +35,11 @@ class CellDatabase {
3235
await db.execute(
3336
"CREATE INDEX idx_cells_plmn_cid ON cells (plmn, cid)",
3437
);
38+
39+
// check shared preferences for map feature!!
40+
await db.execute(
41+
"CREATE INDEX idx_cells_plmn_lat_lng ON cells (plmn, latitude, longitude)",
42+
);
3543
},
3644
);
3745
}
@@ -40,16 +48,17 @@ class CellDatabase {
4048
String plmn,
4149
Set<int> targetCids,
4250
) async {
43-
if (_database == null || targetCids.isEmpty || plmn.isEmpty) return {};
51+
if (targetCids.isEmpty || plmn.isEmpty) return {};
52+
Database db = await getDatabase();
4453

4554
final Map<int, String> results = {};
4655
final String idPlaceholders = targetCids.map((_) => "?").join(", ");
4756

48-
final List<Map<String, dynamic>> records = await _database!.rawQuery(
57+
final List<Map<String, dynamic>> records = await db.rawQuery(
4958
'''
50-
SELECT cid, description FROM cells
51-
WHERE plmn = ? AND cid IN ($idPlaceholders)
52-
''',
59+
SELECT cid, description FROM cells
60+
WHERE plmn = ? AND cid IN ($idPlaceholders)
61+
''',
5362
[plmn, ...targetCids],
5463
);
5564

@@ -63,4 +72,61 @@ class CellDatabase {
6372

6473
return results;
6574
}
75+
76+
static Future<List<CellTower>> fetchMapCellTowers({
77+
required String plmn,
78+
required double minLat,
79+
required double maxLat,
80+
required double minLng,
81+
required double maxLng,
82+
}) async {
83+
if (plmn.isEmpty) return [];
84+
Database db = await getDatabase();
85+
86+
final List<Map<String, dynamic>> records = await db.rawQuery(
87+
'''
88+
SELECT cid, networkgen, latitude, longitude, description, channelnumber
89+
FROM cells
90+
WHERE plmn = ?
91+
AND latitude BETWEEN ? AND ?
92+
AND longitude BETWEEN ? AND ?
93+
LIMIT 500
94+
''',
95+
[plmn, minLat, maxLat, minLng, maxLng],
96+
);
97+
98+
final Map<String, List<DatabaseCell>> groupedTowers = {};
99+
final Map<String, LatLng> coordinateMap = {};
100+
101+
for (final row in records) {
102+
final double lat = (row["latitude"] as num).toDouble();
103+
final double lng = (row["longitude"] as num).toDouble();
104+
105+
final String shortCoords =
106+
"${lat.toStringAsFixed(5)}_${lng.toStringAsFixed(5)}";
107+
108+
final cell = DatabaseCell(
109+
cid: row["cid"] as int,
110+
networkGen: row["networkgen"] as int,
111+
description: row["description"] as String? ?? "",
112+
channelNumber: row["channelnumber"] as int?,
113+
);
114+
115+
if (!groupedTowers.containsKey(shortCoords)) {
116+
groupedTowers[shortCoords] = [];
117+
coordinateMap[shortCoords] = LatLng(lat, lng);
118+
}
119+
120+
groupedTowers[shortCoords]!.add(cell);
121+
}
122+
123+
return groupedTowers.entries.map((entry) {
124+
final coords = coordinateMap[entry.key]!;
125+
return CellTower(
126+
latitude: coords.latitude,
127+
longitude: coords.longitude,
128+
cells: entry.value,
129+
);
130+
}).toList();
131+
}
66132
}

‎lib/types/database/cell_tower.dart‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import 'package:latlong2/latlong.dart';
2+
import 'package:netmanager/types/database/database_cell.dart';
3+
4+
class CellTower {
5+
final double latitude;
6+
final double longitude;
7+
final List<DatabaseCell> cells;
8+
9+
CellTower({
10+
required this.latitude,
11+
required this.longitude,
12+
required this.cells,
13+
});
14+
15+
LatLng getLatLng() {
16+
return LatLng(latitude, longitude);
17+
}
18+
}

0 commit comments

Comments
 (0)