zone label added.

main_production_asset_tracking
Sikander Saleem 2 weeks ago
parent ba3a4c4ca9
commit 16d80afb20

@ -61,6 +61,7 @@ class AssetTrackingProvider extends ChangeNotifier {
selectedSite = null;
assetsList = [];
assetsOnTheFloorList = [];
coordinates = [];
notify();
}

@ -27,6 +27,22 @@ import 'models/floor_map_model.dart';
import 'models/penguin_site_model.dart';
import 'models/zone_data_model.dart';
class GeoZone {
final Map<String, dynamic> properties;
final List<LatLng> points;
final List<AssetTrackingModel> assets;
GeoZone({required this.properties, required this.points, required this.assets});
String get name {
for (final key in const ['name', 'Name', 'label', 'title', 'zoneName', 'id']) {
final value = properties[key];
if (value != null && value.toString().trim().isNotEmpty) return value.toString();
}
return "Zone";
}
}
class MapTrackingPage extends StatefulWidget {
final AssetTrackingModel? assetTrackingData;
@ -103,6 +119,7 @@ class _MapTrackingPageState extends State<MapTrackingPage> {
void _onFloorMapLoaded(FloorMapModel? loadedFloorMap) {
floorMapModel = loadedFloorMap;
_selectedZoneProperties = null;
if (floorMapModel?.geojsonData == null) {
setState(() {});
@ -113,14 +130,18 @@ class _MapTrackingPageState extends State<MapTrackingPage> {
final decoded = floorMapModel!.geojsonData!;
final bounds = _computeBoundsFromGeoJson(decoded);
final assetLocation = widget.assetTrackingData?.location;
final hasAssetPosition = !inAppView && assetLocation?.latitude != null && assetLocation?.longitude != null;
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),
),
if (hasAssetPosition)
Marker(
point: LatLng(assetLocation!.latitude!, assetLocation.longitude!),
child: const Icon(Icons.location_pin, color: AppColor.red30),
),
];
});
@ -172,6 +193,175 @@ class _MapTrackingPageState extends State<MapTrackingPage> {
);
}
Map<String, dynamic>? _selectedZoneProperties;
Polygon _buildZonePolygon(List<LatLng> points, List<List<LatLng>>? holes, Map<String, dynamic> properties, List<AssetTrackingModel> floorAssets) {
final base = defaultOnPolygon(points, holes, properties, defaults: GeoJsonStyleDefaults.leaflet);
final zoneAssets = floorAssets.where((asset) => _isAssetInsideZone(asset, points, holes)).toList();
final zone = GeoZone(properties: properties, points: points, assets: zoneAssets);
final isSelected = identical(properties, _selectedZoneProperties);
return Polygon<Object>(
points: base.points,
holePointsList: base.holePointsList,
color: isSelected ? AppColor.red30.withOpacity(0.35) : base.color,
borderColor: isSelected ? AppColor.red30 : base.borderColor,
borderStrokeWidth: base.borderStrokeWidth,
// Count is shown as a circular badge marker (see _buildZoneBadgeMarkers).
hitValue: zone,
);
}
/// Parses polygon features from [_geojson] and returns a circular badge marker
/// (with the asset count) placed at the center of every zone that has assets.
List<Marker> _buildZoneBadgeMarkers(List<AssetTrackingModel> floorAssets) {
if (!inAppView || floorAssets.isEmpty || _geojson == null) return const [];
List<LatLng> parseRing(dynamic ring) => [
for (final c in (ring as List))
if (c is List && c.length >= 2 && c[0] is num && c[1] is num) LatLng((c[1] as num).toDouble(), (c[0] as num).toDouble()),
];
final badges = <Marker>[];
void addZone(dynamic polygonCoords, Map<String, dynamic> properties) {
if (polygonCoords is! List || polygonCoords.isEmpty) return;
final outer = parseRing(polygonCoords.first);
if (outer.length < 3) return;
final holes = [for (final r in polygonCoords.skip(1)) parseRing(r)].where((h) => h.length >= 3).toList();
final zoneAssets = floorAssets.where((a) => _isAssetInsideZone(a, outer, holes)).toList();
if (zoneAssets.isEmpty) return;
final center = Polygon(
points: outer,
holePointsList: holes.isEmpty ? null : holes,
labelPlacementCalculator: const PolygonLabelPlacementCalculator.simpleCentroid(),
).labelPosition;
final zone = GeoZone(properties: properties, points: outer, assets: zoneAssets);
badges.add(
Marker(
point: center,
width: 21,
height: 21,
child: GestureDetector(
onTap: () => _showZoneDetails(zone),
child: _ZoneCountBadge(count: zoneAssets.length),
),
),
);
}
for (final feature in (_geojson!['features'] as List? ?? const [])) {
if (feature is! Map) continue;
final geometry = feature['geometry'];
if (geometry is! Map) continue;
final properties = (feature['properties'] as Map<String, dynamic>?) ?? <String, dynamic>{};
final coordinates = geometry['coordinates'];
switch (geometry['type']) {
case 'Polygon':
addZone(coordinates, properties);
case 'MultiPolygon':
for (final polygon in (coordinates as List? ?? const [])) {
addZone(polygon, properties);
}
}
}
return badges;
}
bool _isAssetInsideZone(AssetTrackingModel asset, List<LatLng> outer, List<List<LatLng>>? holes) {
final lat = asset.location?.latitude;
final lng = asset.location?.longitude;
if (lat == null || lng == null) return false;
final point = LatLng(lat, lng);
if (!_isPointInRing(point, outer)) return false;
for (final hole in holes ?? const <List<LatLng>>[]) {
if (_isPointInRing(point, hole)) return false;
}
return true;
}
/// Ray-casting point-in-polygon test (longitude = x, latitude = y).
bool _isPointInRing(LatLng point, List<LatLng> ring) {
bool inside = false;
final x = point.longitude, y = point.latitude;
for (int i = 0, j = ring.length - 1; i < ring.length; j = i++) {
final xi = ring[i].longitude, yi = ring[i].latitude;
final xj = ring[j].longitude, yj = ring[j].latitude;
final intersects = ((yi > y) != (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
if (intersects) inside = !inside;
}
return inside;
}
void _onFeatureTapped() {
final hitResult = _polygonHitNotifier.value;
if (hitResult == null) return;
final zone = hitResult.hitValues.whereType<GeoZone>().firstOrNull;
if (zone == null) return;
setState(() {
// _selectedZoneProperties = zone.properties;
// if (inAppView) markers = [for (final asset in zone.assets) _buildAssetMarker(asset)];
});
_showZoneDetails(zone);
}
Marker _buildAssetMarker(AssetTrackingModel asset) {
return Marker(
point: LatLng(asset.location!.latitude!, asset.location!.longitude!),
child: Tooltip(
triggerMode: TooltipTriggerMode.tap,
message: "${asset.label ?? ''}\nLast seen: ${asset.location?.lastSeen?.toServiceRequestDetailsFormat}",
child: const Icon(Icons.location_pin, color: AppColor.red30),
),
);
}
void _showZoneDetails(GeoZone zone) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (sheetContext) {
return Container(
constraints: BoxConstraints(maxHeight: MediaQuery.of(sheetContext).size.height * 0.6),
decoration: BoxDecoration(
color: AppColor.background(sheetContext),
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(zone.name, style: AppTextStyles.heading5.copyWith(color: AppColor.textColor(sheetContext))),
4.height,
InfoTextLabelWidget(label: inAppView ? "${zone.assets.length} Asset(s) in this zone" : "Zone"),
if (inAppView && zone.assets.isNotEmpty) ...[
12.height,
Flexible(
child: ListView.separated(
shrinkWrap: true,
itemCount: zone.assets.length,
separatorBuilder: (_, __) => 12.height,
itemBuilder: (_, index) => AssetTrackingListItemView(zone.assets[index], false),
),
),
],
],
),
),
);
},
).whenComplete(() {
if (mounted) setState(() => _selectedZoneProperties = null);
});
}
double _dragPosition = 0.5;
late double _sheetPosition = _dragPosition;
final minChildSize = 0.25;
@ -191,27 +381,28 @@ class _MapTrackingPageState extends State<MapTrackingPage> {
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');
// }
Selector<AssetTrackingProvider, List<AssetTrackingModel>>(
selector: (_, provider) => inAppView ? List<AssetTrackingModel>.of(provider.assetsOnTheFloorList) : const [],
builder: (_, floorAssets, __) {
final zoneBadges = _buildZoneBadgeMarkers(floorAssets);
return Stack(
children: [
GestureDetector(
onTap: _onFeatureTapped,
// A new MemoryGeoJson is created on every build, so GeoJsonLayer re-parses
// the features and `onPolygon` is called again with the latest assets.
child: GeoJsonLayer.memory(
_geojson!,
hitNotifier: _polygonHitNotifier,
styleDefaults: GeoJsonStyleDefaults.leaflet,
onPolygon: (points, holes, properties) => _buildZonePolygon(points, holes, properties, floorAssets),
),
),
// Circular count badges drawn on top of each zone.
if (zoneBadges.isNotEmpty) MarkerLayer(markers: zoneBadges),
],
);
},
child: GeoJsonLayer.memory(
_geojson!,
hitNotifier: _polygonHitNotifier,
styleDefaults: GeoJsonStyleDefaults.leaflet,
),
),
// if (!inAppView || )
@ -270,17 +461,8 @@ class _MapTrackingPageState extends State<MapTrackingPage> {
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),
),
),
];
_selectedZoneProperties = null;
markers = [for (var asset in selectedAssets!) _buildAssetMarker(asset)];
});
// markers
},
@ -453,6 +635,35 @@ class _MapTrackingPageState extends State<MapTrackingPage> {
}
}
class _ZoneCountBadge extends StatelessWidget {
final int count;
const _ZoneCountBadge({required this.count});
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColor.red30,
border: Border.all(color: Colors.white, width: 1),
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.25), blurRadius: 4, offset: const Offset(0, 2))],
),
child: FittedBox(
fit: BoxFit.scaleDown,
child: Padding(
padding: const EdgeInsets.all(0),
child: Text(
count > 99 ? "99+" : "$count",
style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.w600),
),
),
),
);
}
}
class UpwardExpansionTile extends StatefulWidget {
final Widget title;
final List<Widget> children;

Loading…
Cancel
Save