searchRooms method
Searches room names, numbers, and building-qualified classroom references.
The complete metadata index loads on first search and expires after 14 days.
Existing complete data remains usable offline. Without a complete index,
network failure propagates so a partial match cannot cause a wrong unique
navigation. Exact matches take precedence over substring matches.
buildingNames optionally maps building codes to additional display-name
aliases. Original server names remain searchable and are returned intact.
Only overview buildings with an advertised first floor are searchable;
their rooms on other floors remain eligible.
Implementation
Future<List<CampusMapRoom>> searchRooms(
String query, {
Map<String, String> buildingNames = const {},
}) async {
final normalized = normalizeCampusMapQuery(query);
if (normalized.isEmpty) return [];
var stamp = (await _sync())?.indexFetchedAt;
if (stamp == null || _now().difference(stamp) >= _cacheTtl) {
try {
await _refreshRoomIndex();
} catch (_) {
stamp = (await _sync())?.indexFetchedAt;
if (stamp == null) rethrow;
}
}
// Do not select geometry when searching thousands of lightweight rows.
final table = _db.campusMapRooms;
final floors = _db.campusMapFloors;
final buildings = _db.campusMapBuildings;
final browsableBuildings = _db.selectOnly(floors)
..addColumns([floors.building])
..where(floors.code.equals('1F'));
final searchQuery = _db.selectOnly(table)
..addColumns([
table.id,
table.layerName,
table.nameZh,
table.nameEn,
table.number,
floors.code,
floors.building,
buildings.name,
])
..join([
innerJoin(floors, floors.layerName.equalsExp(table.layerName)),
innerJoin(buildings, buildings.code.equalsExp(floors.building)),
])
..where(floors.building.isInQuery(browsableBuildings));
if (_firstFloorOutlines case final outlines?) {
searchQuery.where(floors.building.isIn(outlines.keys));
}
final exact = <CampusMapRoom>[];
final partial = <CampusMapRoom>[];
for (final row in await searchQuery.get()) {
final room = CampusMapRoom(
id: row.read(table.id)!,
buildingCode: row.read(floors.building)!,
buildingName:
_firstFloorOutlines?[row.read(floors.building)!]?.name ??
row.read(buildings.name)!,
floor: CampusMapFloor(
layerName: row.read(table.layerName)!,
code: row.read(floors.code)!,
),
nameZh: row.read(table.nameZh)!,
nameEn: row.read(table.nameEn)!,
number: row.read(table.number)!,
);
final candidates = [
room.nameZh,
room.nameEn,
room.number,
'${room.buildingCode}${room.number}',
'${room.buildingName}${room.number}',
'${room.buildingName}${room.nameZh}',
if (buildingNames[room.buildingCode] case final displayName?) ...[
'$displayName${room.number}',
'$displayName${room.nameZh}',
'$displayName${room.nameEn}',
],
].where((value) => value.isNotEmpty).map(normalizeCampusMapQuery);
if (candidates.contains(normalized)) {
exact.add(room);
} else if (candidates.any((value) => value.contains(normalized))) {
partial.add(room);
}
}
final matches = exact.isNotEmpty ? exact : partial;
return matches..sort((a, b) {
final byBuilding = a.buildingName.compareTo(b.buildingName);
if (byBuilding != 0) return byBuilding;
final byFloor = a.floor.order.compareTo(b.floor.order);
return byFloor == 0 ? a.number.compareTo(b.number) : byFloor;
});
}