You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
cloudsolutions-atoms/lib/modules/asset_tracking/map_tracking_page.dart

505 lines
20 KiB
Dart

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map_geojson2/flutter_map_geojson2.dart';
import 'package:latlong2/latlong.dart';
import 'package:provider/provider.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/string_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/helper/utils.dart';
import 'package:test_sa/modules/asset_tracking/models/coordinates_model.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/new_views/common_widgets/app_filled_button.dart';
import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
import 'package:test_sa/new_views/common_widgets/multiple_item_drop_down_menu.dart';
import 'package:test_sa/new_views/common_widgets/single_item_drop_down_menu.dart';
import 'package:test_sa/providers/loading_list_notifier.dart';
import 'package:test_sa/views/widgets/item_views/info_text_widget.dart';
import 'asset_tracking_item_view.dart';
import 'asset_tracking_provider.dart';
import 'models/asset_tracking_model.dart';
import 'models/floor_map_model.dart';
import 'models/penguin_site_model.dart';
import 'models/zone_data_model.dart';
class MapTrackingPage extends StatefulWidget {
final AssetTrackingModel? assetTrackingData;
MapTrackingPage({Key? key, this.assetTrackingData}) : super(key: key);
@override
_MapTrackingPageState createState() {
return _MapTrackingPageState();
}
}
class _MapTrackingPageState extends State<MapTrackingPage> {
final MapController mapController = MapController();
late AssetTrackingProvider assetTrackingProvider;
late bool inAppView;
FloorMapModel? floorMapModel;
Map<String, dynamic>? _geojson;
List<Marker> markers = []; // move to provider
LatLngBounds? _computedBounds;
final ExpansibleController expansibleController = ExpansibleController();
static const LatLng _fallbackCenter = LatLng(24.81200245073082, 46.624273601310186);
@override
void initState() {
super.initState();
inAppView = widget.assetTrackingData == null;
assetTrackingProvider = Provider.of<AssetTrackingProvider>(context, listen: false);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!inAppView) {
getFloorMap(widget.assetTrackingData!.location!.floor!.id!);
} else {
getPenguinSites();
}
});
}
@override
void dispose() {
_polygonHitNotifier.dispose();
super.dispose();
}
void getPenguinSites() async {
Utils.showLoading(context);
await assetTrackingProvider.getPenguinSites();
Utils.hideLoading(context);
}
void getPenguinBuildings(int id) async {
Utils.showLoading(context);
await assetTrackingProvider.getPenguinBuildings(id);
Utils.hideLoading(context);
}
void getPenguinFloors(int id) async {
Utils.showLoading(context);
await assetTrackingProvider.getPenguinFloors(id);
Utils.hideLoading(context);
}
void getFloorMap(int id) {
Utils.showLoading(context);
assetTrackingProvider.getFloorMap(id).then((floorMap) {
Utils.hideLoading(context);
_onFloorMapLoaded(floorMap);
}).catchError((error) {
Utils.hideLoading(context);
});
}
void _onFloorMapLoaded(FloorMapModel? loadedFloorMap) {
floorMapModel = loadedFloorMap;
if (floorMapModel?.geojsonData == null) {
setState(() {});
return;
}
try {
final decoded = floorMapModel!.geojsonData!;
final bounds = _computeBoundsFromGeoJson(decoded);
setState(() {
_geojson = decoded;
_computedBounds = bounds;
markers = [
Marker(
point: LatLng(widget.assetTrackingData!.location!.latitude!, widget.assetTrackingData!.location!.longitude!),
child: const Icon(Icons.location_pin, color: AppColor.red30),
),
];
});
WidgetsBinding.instance.addPostFrameCallback((_) {
mapController.fitCamera(
CameraFit.bounds(bounds: bounds, padding: const EdgeInsets.all(24)),
);
});
} catch (e) {
print('Failed to parse/compute bounds for floor geojson: $e');
setState(() {}); // still show the map with whatever we have
}
}
LatLngBounds _computeBoundsFromGeoJson(Map<String, dynamic> geojson) {
final features = (geojson['features'] as List<dynamic>? ?? []);
double? minLng, minLat, maxLng, maxLat;
void visit(dynamic node) {
if (node is List && node.isNotEmpty && node.first is num) {
final lng = (node[0] as num).toDouble();
final lat = (node[1] as num).toDouble();
minLng = (minLng == null || lng < minLng!) ? lng : minLng;
maxLng = (maxLng == null || lng > maxLng!) ? lng : maxLng;
minLat = (minLat == null || lat < minLat!) ? lat : minLat;
maxLat = (maxLat == null || lat > maxLat!) ? lat : maxLat;
} else if (node is List) {
for (final child in node) {
visit(child);
}
}
}
for (final feature in features) {
final geometry = feature['geometry'] as Map<String, dynamic>?;
final coordinates = geometry?['coordinates'];
if (coordinates == null) continue;
visit(coordinates);
}
if (minLng == null || minLat == null || maxLng == null || maxLat == null) {
throw StateError('No coordinates found in floor geojson — cannot compute bounds.');
}
return LatLngBounds(
LatLng(minLat!, minLng!),
LatLng(maxLat!, maxLng!),
);
}
double _dragPosition = 0.5;
late double _sheetPosition = _dragPosition;
final minChildSize = 0.25;
final maxChildSize = 1.0;
// final LayerHitNotifier<Map<String, dynamic>> _polygonHitNotifier = ValueNotifier(null);
final LayerHitNotifier<Object> _polygonHitNotifier = ValueNotifier(null);
@override
Widget build(BuildContext context) {
Widget mapWidget = FlutterMap(
mapController: mapController,
options: const MapOptions(initialCenter: _fallbackCenter, initialZoom: 16.0),
children: [
TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'com.hmg.atoms',
),
if (_geojson != null) ...[
GestureDetector(
onTap: () {
// final hitResult = _polygonHitNotifier.value;
//
// if (hitResult == null || hitResult.hitValues.isEmpty) return;
// print("hitResult:$hitResult");
// final hitValue = hitResult.hitValues.first;
//
// // Narrow safely instead of assuming the type.
// if (hitValue is Map<String, dynamic>) {
// print(hitValue);
// } else {
// // ignore: avoid_print
// print('Unexpected hit value type: ${hitValue.runtimeType} -> $hitValue');
// }
},
child: GeoJsonLayer.memory(
_geojson!,
hitNotifier: _polygonHitNotifier,
styleDefaults: GeoJsonStyleDefaults.leaflet,
),
),
// if (!inAppView || )
if (markers.isNotEmpty) MarkerLayer(markers: markers),
if (inAppView)
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Selector<AssetTrackingProvider, List<AssetTrackingModel>>(
selector: (_, myModel) => myModel.assetsOnTheFloorList,
builder: (_, assetsOnTheFloorList, __) {
return assetsOnTheFloorList.isEmpty
? const SizedBox()
: Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.only(right: 0, left: 0),
decoration: BoxDecoration(
color: AppColor.tabBarBackground(context),
border: Border.all(color: AppColor.tabBarBorder(context)),
borderRadius: BorderRadius.circular(20),
),
child: UpwardExpansionTile(title: InfoTextLabelWidget(label: "${assetsOnTheFloorList.length} Assets found on this floor"), children: [
LimitedBox(
maxHeight: 120,
child: MultipleItemDropDownMenu<AssetTrackingModel, NullableLoadingProvider>(
context: context,
title: 'Assets on The Floor',
hintMsg: 'Search By Label or Serial Number',
initialValue: [],
showShadow: false,
staticData: assetsOnTheFloorList,
showCancel: true,
showAsFullScreenDialog: true,
onSelect: (selectedAssets) {
setState(() {
markers = [
for (var asset in selectedAssets!)
Marker(
point: LatLng(asset.location!.latitude!, asset.location!.longitude!),
child: Tooltip(
triggerMode: TooltipTriggerMode.tap,
message: "Last seen: ${asset.location?.lastSeen?.toServiceRequestDetailsFormat}",
child: const Icon(Icons.location_pin, color: AppColor.red30),
),
),
];
});
// markers
},
).paddingOnly(start: 16, end: 16, top: 16),
),
]),
);
// Column(
// mainAxisSize: MainAxisSize.min,
// children: [
// Container(
// width: 50.toScreenWidth,
// height: 6.toScreenHeight,
// decoration: BoxDecoration(color: AppColor.white50, borderRadius: BorderRadius.circular(30)),
// ).center,
// InfoTextLabelWidget(label: "There is ${assetsOnTheFloorList.length} assets on this floor"),
// ],
// ).bottomSheetContainer(context, padding: const EdgeInsets.all(16));
}),
),
],
if (inAppView)
Selector<AssetTrackingProvider, List<Coordinates>>(
selector: (_, assetTPro) => assetTPro.coordinates,
builder: (_, coordinates, __) {
if (coordinates.isEmpty) return const SizedBox();
List<LatLng> list = [
for (var coordinate in (coordinates)) LatLng(coordinate.latitude!, coordinate.longitude!),
];
List<Polygon> zonesPolygons = [
Polygon(
label: assetTrackingProvider.selectedBuilding?.label ?? "",
labelStyle: AppTextStyles.bodyText2.copyWith(color: AppColor.textColor(context)),
points: list,
color: AppColor.red30.withOpacity(0.3),
borderColor: AppColor.red30,
borderStrokeWidth: 2.0,
),
];
return PolygonLayer(polygons: zonesPolygons);
}),
Positioned(
top: 70,
right: 8,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.add),
padding: EdgeInsets.zero,
style: IconButton.styleFrom(
backgroundColor: AppColor.background(context), // Sets the background color
foregroundColor: AppColor.icon2Color(context), // Sets the icon color
),
visualDensity: VisualDensity.compact,
tooltip: "Zoom In",
onPressed: () {
final zoom = mapController.camera.zoom + 1;
mapController.move(mapController.camera.center, zoom);
},
),
IconButton(
icon: const Icon(Icons.remove),
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
style: IconButton.styleFrom(
backgroundColor: AppColor.background(context), // Sets the background color
foregroundColor: AppColor.icon2Color(context), // Sets the icon color
),
tooltip: "Zoom Out",
onPressed: () {
final zoom = mapController.camera.zoom - 1;
mapController.move(mapController.camera.center, zoom);
},
),
],
),
)
],
);
return inAppView
? Stack(
alignment: AlignmentDirectional.topCenter,
children: [
mapWidget,
Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.only(right: 16, left: 16),
decoration: BoxDecoration(
color: AppColor.tabBarBackground(context),
border: Border.all(color: AppColor.tabBarBorder(context)),
borderRadius: BorderRadius.circular(20),
),
child: Theme(
data: ThemeData(splashColor: Colors.transparent, hoverColor: Colors.transparent, highlightColor: Colors.transparent),
child: Consumer<AssetTrackingProvider>(builder: (context, assetProvider, _) {
return ExpansionTile(
shape: Border.all(color: Colors.transparent, width: 0),
collapsedShape: Border.all(color: Colors.transparent, width: 0),
controller: expansibleController,
title: InfoTextLabelWidget(label: assetProvider.getExpansionHeader),
iconColor: AppColor.icon2Color(context),
collapsedIconColor: AppColor.icon2Color(context),
dense: true,
childrenPadding: const EdgeInsets.all(0),
tilePadding: const EdgeInsets.all(0),
children: <Widget>[
SingleItemDropDownMenu<PenguinSiteModel, NullableLoadingProvider>(
context: context,
title: "Select Site",
staticData: assetProvider.sitesList,
initialValue: assetProvider.selectedSite,
showShadow: false,
// backgroundColor: AppColor.fieldBgColor(context),
showAsBottomSheet: true,
onSelect: (value) {
if (value != null) {
assetProvider.selectedSite = value;
getPenguinBuildings(value.id!);
}
},
),
4.height,
SingleItemDropDownMenu<PenguinSiteModel, NullableLoadingProvider>(
context: context,
title: "Select Building",
staticData: assetProvider.buildList,
initialValue: assetProvider.selectedBuilding,
showShadow: false,
enabled: assetProvider.selectedSite != null,
// backgroundColor: AppColor.fieldBgColor(context),
showAsBottomSheet: true,
onSelect: (value) {
if (value != null) {
assetProvider.selectedBuilding = value;
getPenguinFloors(value.id!);
}
},
),
4.height,
SingleItemDropDownMenu<PenguinSiteModel, NullableLoadingProvider>(
context: context,
title: "Select Floor",
staticData: assetProvider.floorList,
initialValue: assetProvider.selectedFloor,
showShadow: false,
enabled: assetProvider.selectedBuilding != null,
// backgroundColor: AppColor.fieldBgColor(context),
showAsBottomSheet: true,
onSelect: (value) {
if (value != null) {
assetProvider.selectedFloor = value;
}
},
),
8.height,
Align(
alignment: AlignmentGeometry.centerRight,
child: AppTextButton(
label: "View",
disableButton: assetProvider.selectedSite == null || assetProvider.selectedBuilding == null,
onPressed: () {
expansibleController.collapse();
assetTrackingProvider.coordinates.clear();
assetTrackingProvider.assetsOnTheFloorList.clear();
assetTrackingProvider.notify();
assetTrackingProvider.getAssetsFromPenguinByFloor();
if (assetProvider.selectedFloor != null) {
getFloorMap(assetProvider.selectedFloor!.id!);
assetTrackingProvider.getZoneFromPenguin();
} else {
assetTrackingProvider.calculateCoordinates();
}
},
),
),
16.height,
],
);
}),
),
)
],
)
: Scaffold(appBar: const DefaultAppBar(title: "Live Asset Tracking"), body: mapWidget);
}
}
class UpwardExpansionTile extends StatefulWidget {
final Widget title;
final List<Widget> children;
const UpwardExpansionTile({Key? key, required this.title, required this.children}) : super(key: key);
@override
_UpwardExpansionTileState createState() => _UpwardExpansionTileState();
}
class _UpwardExpansionTileState extends State<UpwardExpansionTile> {
bool _isExpanded = false;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Expanded content appears ABOVE the title
AnimatedCrossFade(
firstChild: const SizedBox.shrink(),
secondChild: Column(
mainAxisSize: MainAxisSize.min,
children: widget.children,
),
crossFadeState: _isExpanded ? CrossFadeState.showSecond : CrossFadeState.showFirst,
duration: const Duration(milliseconds: 200),
),
// The clickable header/title sits at the bottom
InkWell(
onTap: () {
setState(() {
_isExpanded = !_isExpanded;
});
},
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
widget.title,
Icon(_isExpanded ? Icons.expand_less : Icons.expand_more),
],
),
),
),
],
);
}
}