Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/uni_app/lib/generated/intl/messages_en.dart
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,7 @@ class MessageLookup extends MessageLookupByLibrary {
"save": MessageLookupByLibrary.simpleMessage("Save"),
"schedule": MessageLookupByLibrary.simpleMessage("Schedule"),
"school_calendar": MessageLookupByLibrary.simpleMessage("School Calendar"),
"search": MessageLookupByLibrary.simpleMessage("Search"),
"search_here": MessageLookupByLibrary.simpleMessage("Search here"),
"see_more": MessageLookupByLibrary.simpleMessage("See more"),
"select_all": MessageLookupByLibrary.simpleMessage("Select All"),
"semester": MessageLookupByLibrary.simpleMessage("Semester"),
Expand Down
2 changes: 1 addition & 1 deletion packages/uni_app/lib/generated/intl/messages_pt_PT.dart
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ class MessageLookup extends MessageLookupByLibrary {
"school_calendar": MessageLookupByLibrary.simpleMessage(
"Calendário Escolar",
),
"search": MessageLookupByLibrary.simpleMessage("Pesquisar"),
"search_here": MessageLookupByLibrary.simpleMessage("Pesquisar aqui"),
"see_more": MessageLookupByLibrary.simpleMessage("Ver mais"),
"select_all": MessageLookupByLibrary.simpleMessage("Selecionar Todos"),
"semester": MessageLookupByLibrary.simpleMessage("Semestre"),
Expand Down
6 changes: 3 additions & 3 deletions packages/uni_app/lib/generated/l10n.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/uni_app/lib/l10n/intl_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@
"@year": {},
"see_more": "See more",
"@see_more": {},
"search": "Search",
"search_here": "Search here",
"@search": {},
"confirm_logout": "Do you really want to log out? Your local data will be deleted and you will have to log in again.",
"@confirm_logout": {},
Expand Down
2 changes: 1 addition & 1 deletion packages/uni_app/lib/l10n/intl_pt_PT.arb
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@
"@widget_prompt": {},
"year": "Ano",
"@year": {},
"search": "Pesquisar",
"search_here": "Pesquisar aqui",
"@search": {},
"confirm_logout": "Tens a certeza de que queres terminar sessão? Os teus dados locais serão apagados e terás de iniciar sessão novamente.",
"@confirm_logout": {},
Expand Down
52 changes: 52 additions & 0 deletions packages/uni_app/lib/model/entities/indoor_floor_plan.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import 'package:latlong2/latlong.dart';

/// Represents an indoor floor plan with its geometry and features
class IndoorFloorPlan {
IndoorFloorPlan({
required this.buildingId,
required this.floor,
required this.outline,
required this.rooms,
required this.corridors,
required this.amenities,
});

final String buildingId;
final int floor;
final List<LatLng> outline; // Building outline for this floor
final List<IndoorRoom> rooms;
final List<IndoorCorridor> corridors;
final List<IndoorAmenity> amenities;
}

class IndoorRoom {
IndoorRoom({
required this.ref,
required this.polygon,
this.name,
this.type,
});

final String ref; // Room number (e.g., "B101")
final List<LatLng> polygon; // Room outline
final String? name;
final String? type; // office, classroom, lab, etc.
}

class IndoorCorridor {
IndoorCorridor({required this.polygon});

final List<LatLng> polygon;
}

class IndoorAmenity {
IndoorAmenity({
required this.position,
required this.type,
this.name,
});

final LatLng position;
final String type; // toilets, vending_machine, etc.
final String? name;
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:uni/controller/fetchers/location_fetcher/location_fetcher_asset.dart';
import 'package:uni/controller/fetchers/location_fetcher/location_fetcher_osm.dart';
import 'package:uni/model/entities/indoor_floor_plan.dart';
import 'package:uni/model/entities/location_group.dart';
import 'package:uni/model/providers/riverpod/cached_async_notifier.dart';

// Provider for location groups (map markers)
final locationsProvider =
AsyncNotifierProvider<FacultyLocationsNotifier, List<LocationGroup>?>(
FacultyLocationsNotifier.new,
);

// Provider for indoor floor plans (building layouts)
final indoorFloorPlansProvider =
AsyncNotifierProvider<IndoorFloorPlansNotifier, List<IndoorFloorPlan>?>(
IndoorFloorPlansNotifier.new,
);

class FacultyLocationsNotifier
extends CachedAsyncNotifier<List<LocationGroup>> {
@override
Expand All @@ -20,6 +29,37 @@ class FacultyLocationsNotifier

@override
Future<List<LocationGroup>> loadFromRemote() async {
return state.value!;
try {
final osmData = await LocationFetcherOSM().getLocations();

if (osmData.isNotEmpty) {
return osmData;
}

return await loadFromStorage();
} catch (e) {
return loadFromStorage();
}
}
}

class IndoorFloorPlansNotifier
extends CachedAsyncNotifier<List<IndoorFloorPlan>> {
@override
Duration? get cacheDuration => const Duration(days: 30);

@override
Future<List<IndoorFloorPlan>> loadFromStorage() async {
// TODO: Load from asset JSON as fallback
return [];
}

@override
Future<List<IndoorFloorPlan>> loadFromRemote() async {
try {
return await LocationFetcherOSM().getIndoorFloorPlans();
} catch (e) {
return loadFromStorage();
}
}
}
107 changes: 77 additions & 30 deletions packages/uni_app/lib/view/map/map.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:uni/controller/networking/url_launcher.dart';
import 'package:uni/generated/l10n.dart';
import 'package:uni/model/entities/indoor_floor_plan.dart';
import 'package:uni/model/entities/location_group.dart';
import 'package:uni/model/providers/riverpod/default_consumer.dart';
import 'package:uni/model/providers/riverpod/faculty_locations_provider.dart';
import 'package:uni/view/map/widgets/floor_selector.dart';
import 'package:uni/view/map/widgets/floorless_marker_popup.dart';
import 'package:uni/view/map/widgets/indoor_floor_layer.dart';
import 'package:uni/view/map/widgets/marker.dart';
import 'package:uni/view/map/widgets/marker_popup.dart';
import 'package:uni/view/widgets/pages_layouts/general/widgets/bottom_navigation_bar.dart';
Expand All @@ -29,12 +32,15 @@ class MapPageStateView extends ConsumerState<MapPage> {
var _searchTerms = '';
late final PopupController _popupLayerController;
LatLngBounds? _bounds;
int? _selectedFloor;
bool _showIndoorLayer = false;

@override
void initState() {
super.initState();
_searchTerms = '';
_popupLayerController = PopupController();
_selectedFloor = null;
}

@override
Expand All @@ -45,9 +51,17 @@ class MapPageStateView extends ConsumerState<MapPage> {

@override
Widget build(BuildContext context) {

final indoorPlansAsync = ref.watch(indoorFloorPlansProvider);

return DefaultConsumer<List<LocationGroup>>(
provider: locationsProvider,
builder: (context, ref, locations) {
final indoorPlans = indoorPlansAsync.when(
data: (plans) => plans ?? [],
loading: () => <IndoorFloorPlan>[],
error: (_, _) => <IndoorFloorPlan>[],
);
var bounds = _bounds;
bounds ??= LatLngBounds.fromPoints(
locations.map((location) => location.latlng).toList(),
Expand All @@ -67,6 +81,16 @@ class MapPageStateView extends ConsumerState<MapPage> {
});
}

if (_selectedFloor != null) {
filteredLocations.retainWhere((location) {
return location.floors.containsKey(_selectedFloor);
});
}

final allFloors =
locations.expand((group) => group.floors.keys).toSet().toList()
..sort((a, b) => b.compareTo(a));

return AnnotatedRegion<SystemUiOverlayStyle>(
value: AppSystemOverlayStyles.base.copyWith(
statusBarIconBrightness: Brightness.dark,
Expand All @@ -82,13 +106,14 @@ class MapPageStateView extends ConsumerState<MapPage> {
minZoom: 16,
maxZoom: 19,
initialCenter: bounds.center,
initialCameraFit: CameraFit.insideBounds(bounds: bounds),
initialZoom: 17,
cameraConstraint: CameraConstraint.containCenter(
bounds: bounds,
),
onTap:
(tapPosition, latlng) =>
_popupLayerController.hideAllPopups(),
onTap: (tapPosition, latlng) {
_popupLayerController.hideAllPopups();
FocusScope.of(context).unfocus();
},
interactionOptions: const InteractionOptions(
flags: InteractiveFlag.all - InteractiveFlag.rotate,
),
Expand All @@ -104,6 +129,11 @@ class MapPageStateView extends ConsumerState<MapPage> {
retinaMode: RetinaMode.isHighDensity(context),
maxNativeZoom: 20,
),
if (_showIndoorLayer && _selectedFloor != null)
IndoorFloorLayer(
floorPlans: indoorPlans,
selectedFloor: _selectedFloor,
),
PopupMarkerLayer(
options: PopupMarkerLayerOptions(
markers:
Expand All @@ -128,40 +158,50 @@ class MapPageStateView extends ConsumerState<MapPage> {
),
),
),
PopupMarkerLayer(
options: PopupMarkerLayerOptions(
markers:
filteredLocations.map((location) {
return LocationMarker(location.latlng, location);
}).toList(),
popupController: _popupLayerController,
popupDisplayOptions: PopupDisplayOptions(
animation: const PopupAnimation.fade(
duration: Duration(milliseconds: 400),
Positioned(
right: 10,
bottom: 650,
child: SafeArea(
child: FloatingActionButton(
mini: true,
onPressed: () {
setState(() {
_showIndoorLayer = !_showIndoorLayer;
});
},
child: Icon(
_showIndoorLayer ? Icons.layers_clear : Icons.layers,
),
builder: (_, marker) {
if (marker is LocationMarker) {
return marker.locationGroup.isFloorless
? FloorlessLocationMarkerPopup(
marker.locationGroup,
)
: LocationMarkerPopup(marker.locationGroup);
}
return const Card(child: Text(''));
),
),
),
Positioned(
right: 10,
top: 400,
child: SafeArea(
child: FloorSelector(
floors: allFloors,
selectedFloor: _selectedFloor,
onFloorSelected: (floor) {
setState(() {
_selectedFloor = floor;
_popupLayerController.hideAllPopups();
});
},
),
),
),
SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 10,
padding: const EdgeInsets.only(
left: 10,
right: 10,
top: 12,
),
child: PhysicalModel(
borderRadius: BorderRadius.circular(10),
color: Colors.white,
elevation: 3,
color: const Color(0xFFFFF5F3),
elevation: 4,
child: TextFormField(
key: searchFormKey,
onChanged: (text) {
Expand All @@ -179,15 +219,22 @@ class MapPageStateView extends ConsumerState<MapPage> {
child: SvgPicture.asset(
'assets/images/logo_dark.svg',
semanticsLabel: 'search',
width: 10,
width: 44,
height: 25,
),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.all(10),
hintText: '${S.of(context).search}...',
hintText: S.of(context).search_here,
hintStyle: const TextStyle(
fontFamily: 'Roboto',
fontSize: 9,
fontWeight: FontWeight.w400,
color: Color(0xFF7F7F7F),
),
),
),
),
Expand Down
Loading