// ignore_for_file: use_build_context_synchronously
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:kvwsmb_survey_app/Bloc/data_cubit.dart';
import 'package:kvwsmb_survey_app/constants/globals.dart';
import 'package:kvwsmb_survey_app/custom_properties/ecomi_object.dart';
import 'package:kvwsmb_survey_app/helper/convert_wkt_latlng.dart';
List<String> _filterKeys((List<String>, String) args) {
final (keys, query) = args;
return keys.where((key) => key.contains(query)).toList();
}
void openSearchDialog(
DataCubit dataCubit,
BuildContext context,
List<Map<String, dynamic>> layerList,
Map<String, List<Map<String, dynamic>>> attributeList,
MapController mapController,
double maxzoom,
Function()? reloadFeatures,
) {
showGeneralDialog(
context: context,
pageBuilder:
(context, animation, secondaryAnimation) => SearchDialog(
dataCubit: dataCubit,
layerList: layerList,
attributeList: attributeList,
mapController: mapController,
maxzoom: maxzoom,
reloadFeatures: reloadFeatures,
),
transitionBuilder: (context, animation, secondaryAnimation, child) {
return Align(
alignment: Alignment.topCenter,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, -0.1),
end: Offset.zero,
).animate(animation),
child: child,
),
);
},
transitionDuration: const Duration(milliseconds: 200),
);
}
class SearchDialog extends StatefulWidget {
final DataCubit dataCubit;
final List<Map<String, dynamic>> layerList;
final Map<String, List<Map<String, dynamic>>> attributeList;
final MapController mapController;
final double maxzoom;
final Function()? reloadFeatures;
const SearchDialog({
super.key,
required this.dataCubit,
required this.layerList,
required this.attributeList,
required this.mapController,
required this.maxzoom,
this.reloadFeatures,
});
@override
SearchDialogState createState() => SearchDialogState();
}
class SearchDialogState extends State<SearchDialog> {
String? selectedLayerId;
String? selectedAttrId;
String searchQuery = '';
bool isDataReady = false;
String? errorMessage;
List<EcomiObject> searchResults = [];
Map<String, EcomiObject> prefilteredMap = {};
Timer? debounce;
int pageSize = 10;
ScrollController scrollController = ScrollController();
final TextEditingController searchController = TextEditingController();
@override
void initState() {
super.initState();
searchController.addListener(() {
if (searchController.text.isEmpty) {
setState(() {
searchQuery = '';
searchResults = [];
});
}
});
}
@override
void dispose() {
debounce?.cancel();
super.dispose();
}
String normalizeKey(String input) {
return input
.toLowerCase()
.replaceAll(RegExp(r'[^\w\s]'), '')
.replaceAll(' ', '_');
}
Future<void> prefilterData() async {
if (selectedLayerId == null || selectedAttrId == null) {
prefilteredMap = {};
return;
}
setState(() {
isDataReady = false;
errorMessage = null;
});
try {
final layer = widget.layerList.firstWhere(
(l) => l['layerId'] == selectedLayerId,
orElse: () => {},
);
final geometryType = layer['geometryType'] as String?;
String type =
geometryType!.toLowerCase().contains('point')
? 'marker'
: geometryType.toLowerCase().contains('linestring')
? 'polyline'
: 'polygon';
final rawResults = await _filterFeatures(
widget.dataCubit,
type,
selectedLayerId!,
selectedAttrId!,
'',
);
final Map<String, EcomiObject> tempMap = {};
for (var obj in rawResults) {
final value = obj.attributes[selectedAttrId!]?.toString() ?? '';
final key = normalizeKey(value);
tempMap[key] = obj;
}
setState(() {
prefilteredMap = tempMap;
Future.delayed(const Duration(seconds: 2), () {
setState(() {
isDataReady = true;
});
});
});
} catch (e) {
setState(() {
isDataReady = false;
errorMessage = 'Prefilter failed: $e';
});
}
}
void performSearch() {
debounce?.cancel();
debounce = Timer(const Duration(milliseconds: 300), () async {
if (searchQuery.isEmpty || !isDataReady) {
setState(() {
searchResults = [];
errorMessage = null;
});
return;
}
final String query = searchQuery.toLowerCase();
final List<String> keys = prefilteredMap.keys.toList();
final List<String> matchedKeys = await compute(_filterKeys, (
keys,
query,
));
setState(() {
searchResults = matchedKeys.map((key) => prefilteredMap[key]!).toList();
});
});
}
@override
Widget build(BuildContext context) {
final mediaQuery = MediaQuery.of(context);
final dialogWidth = mediaQuery.size.width * 0.8;
final maxHeight = mediaQuery.size.height * 0.6;
final layerItems =
widget.layerList.map<DropdownMenuItem>((layer) {
return DropdownMenuItem(
value: layer['layerId'],
child: Text(
layer['name'] ?? 'Unnamed Layer',
overflow: TextOverflow.ellipsis,
),
);
}).toList();
return Scaffold(
resizeToAvoidBottomInset: false,
backgroundColor: Colors.transparent,
body: Stack(
children: [
Positioned.fill(
child: GestureDetector(
onTap: Navigator.of(context).pop,
behavior: HitTestBehavior.opaque,
),
),
Positioned(
top: 40,
left: (mediaQuery.size.width - dialogWidth) / 2,
child: Material(
elevation: 8,
borderRadius: BorderRadius.circular(16),
color: Colors.grey.shade300,
child: Container(
width: dialogWidth,
constraints: BoxConstraints(maxHeight: maxHeight),
padding: const EdgeInsets.all(16),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Search Feature',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 20,
),
),
const SizedBox(height: 16),
// Layer Dropdown
DropdownButtonFormField(
decoration: InputDecoration(
labelText: 'Select Layer',
filled: true,
fillColor: Colors.grey[50],
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey[300]!),
),
contentPadding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
value: selectedLayerId,
items: layerItems,
onChanged: (value) {
setState(() {
selectedLayerId = value;
selectedAttrId = null;
searchQuery = '';
searchResults = [];
prefilteredMap = {};
isDataReady = false;
errorMessage = null;
});
},
isExpanded: true,
),
const SizedBox(height: 16),
// Attribute Dropdown
DropdownButtonFormField(
decoration: InputDecoration(
labelText: 'Select Attribute',
enabled: selectedLayerId != null,
filled: true,
fillColor: Colors.grey[50],
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey[300]!),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
value: selectedAttrId,
items:
(widget.attributeList[selectedLayerId] ?? [])
.where((attr) {
if (selectedLayerId == privateWell) {
return attr['attrId'] == 'attr0' ||
attr['attrId'] == 'attr1';
}
return true;
})
.map(
(attr) => DropdownMenuItem(
value: attr['attrId'].toString(),
child: Text(attr['name'] ?? 'N/A'),
),
)
.toList(),
onChanged: (value) {
setState(() {
selectedAttrId = value;
searchQuery = '';
searchResults = [];
prefilteredMap = {};
isDataReady = false;
errorMessage = null;
pageSize = 10;
});
prefilterData();
},
isExpanded: true,
),
const SizedBox(height: 16),
// Search TextField
TextField(
autofocus: true,
enabled: selectedAttrId != null,
controller: searchController,
decoration: InputDecoration(
labelText: 'Search',
hintText: 'Type to search...',
filled: true,
fillColor: Colors.grey[50],
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey[300]!),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
suffixIcon:
searchController.text.isNotEmpty
? IconButton(
icon: Icon(Icons.clear),
onPressed: () {
searchController.clear();
setState(() {
searchQuery = '';
searchResults = [];
});
},
)
: null,
),
onChanged: (value) {
setState(() {
searchQuery = normalizeKey(value);
});
performSearch();
},
),
const SizedBox(height: 8),
// Search Results
SizedBox(
height: 200,
child: Visibility(
visible: searchResults.isNotEmpty,
child: ListView.builder(
itemCount: pageSize + 1,
itemBuilder: (context, index) {
if (index < pageSize &&
index < searchResults.length) {
final feature = searchResults[index];
final displayText =
(feature.layerId == privateWell &&
selectedAttrId == 'attr0')
? (double.tryParse(
feature.attributes[selectedAttrId!] ??
'',
) ??
0.0)
.toInt()
.toString()
: feature.attributes[selectedAttrId!]
?.toString() ??
'N/A';
return ListTile(
tileColor: Colors.white70,
title: Text(displayText),
onTap: () async {
await _moveMapToFeature(
feature.multi,
feature.geometry,
widget.mapController,
widget.maxzoom,
);
if (widget.reloadFeatures != null) {
widget.reloadFeatures!();
}
Navigator.of(context).pop();
},
);
} else {
return Padding(
padding: const EdgeInsets.symmetric(
vertical: 8.0,
),
child: TextButton(
onPressed: () {
setState(() {
pageSize += 10;
});
},
child: const Text('Load More'),
),
);
}
},
),
),
),
if (errorMessage != null)
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Text(
errorMessage!,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
fontSize: 14,
),
),
),
// Action Buttons
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: Navigator.of(context).pop,
child: const Text('Cancel'),
),
if (selectedAttrId != null)
ElevatedButton(
onPressed:
isDataReady && searchQuery.isNotEmpty
? () async {
final userInput =
searchQuery.toLowerCase();
final matches =
searchResults.where((result) {
final rawValue =
result
.attributes[selectedAttrId!] ??
'';
final normalized =
normalizeSpecialCase(
result.layerId,
selectedAttrId!,
rawValue,
);
return normalized.toLowerCase() ==
userInput;
}).toList();
if (matches.isNotEmpty &&
matches.length == 1) {
await _moveMapToFeature(
matches[0].multi,
matches[0].geometry,
widget.mapController,
widget.maxzoom,
);
Navigator.of(context).pop();
return;
}
performSearch();
}
: null,
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(
horizontal: 16,
),
),
child: const Text('Search'),
),
],
),
],
),
),
),
),
),
],
),
);
}
}
Future<Iterable<dynamic>> _filterFeatures(
DataCubit dataCubit,
String type,
String layerId,
String attrId,
String query,
) async {
final String lowerQuery = query.toLowerCase();
if (type == 'marker') {
return dataCubit.markers.where(
(obj) =>
obj.layerId == layerId &&
obj.attributes.containsKey(attrId) &&
(obj.attributes[attrId]?.toString().toLowerCase() ?? '').contains(
lowerQuery,
),
);
} else if (type == 'polyline') {
return dataCubit.polylines.where(
(obj) =>
obj.layerId == layerId &&
obj.attributes.containsKey(attrId) &&
(obj.attributes[attrId]?.toString().toLowerCase() ?? '').contains(
lowerQuery,
),
);
} else {
return dataCubit.polygons.where(
(obj) =>
obj.layerId == layerId &&
obj.attributes.containsKey(attrId) &&
(obj.attributes[attrId]?.toString().toLowerCase() ?? '').contains(
lowerQuery,
),
);
}
}
Future _moveMapToFeature(
bool isMulti,
String geometryWkt,
MapController mapController,
double maxzoom,
) async {
try {
List points = [];
if (geometryWkt.toLowerCase().contains('point')) {
if (isMulti) {
points = multiPointWktToLatLng(geometryWkt);
} else {
points = [pointWktToLatLng(geometryWkt)];
}
} else if (geometryWkt.toLowerCase().contains('line')) {
if (isMulti) {
final latlists = multiLineWktToLatLng(geometryWkt);
points = latlists[0];
} else {
points = lineWktToLatLng(geometryWkt);
}
} else if (geometryWkt.toLowerCase().contains('polygon')) {
if (isMulti) {
final latlists = multiPolygonWktToLatLng(geometryWkt);
points = latlists[0];
} else {
points = polygonWktToLatLng(geometryWkt);
}
}
if (points.isEmpty) {
throw Exception('No valid points found in geometry');
} else {
mapController.move(points[0], maxzoom);
}
} catch (e) {
debugPrint('isMulti: $isMulti');
debugPrint('geometry: $geometryWkt');
debugPrint("Failed to move to feature: $e");
rethrow; // Re-throw to allow UI to handle the error
}
}
String normalizeSpecialCase(String layerId, String attrId, String rawValue) {
if (attrId == 'attr0') {
final numValue = double.tryParse(rawValue);
if (numValue != null) {
return numValue.toInt().toString();
}
}
return rawValue;
}