WIP geopoints filter on map

This commit is contained in:
Thomas Fransolet 2024-04-16 22:54:57 +02:00
parent fbf051b23f
commit d9efad47f8
8 changed files with 250 additions and 117 deletions

View File

@ -14,7 +14,8 @@ List<Translation> translations = [
"sunday": "Dimanche",
"menu": "Menu",
"quiz.seeResponses": "Voir les réponses",
"quiz.restart": "Recommencer"
"quiz.restart": "Recommencer",
"map.search": "Rechercher"
}),
Translation(language: "EN", data: {
"weather.hourly": "Hourly",
@ -29,7 +30,8 @@ List<Translation> translations = [
"sunday": "Sunday",
"menu": "Menu",
"quiz.seeResponses": "See the answers",
"quiz.restart": "Restart"
"quiz.restart": "Restart",
"map.search": "Search"
}),
Translation(language: "DE", data: {
"weather.hourly": "Nächste Stunden",
@ -44,7 +46,8 @@ List<Translation> translations = [
"sunday": "Sonntag",
"menu": "Speisekarte",
"quiz.seeResponses": "Sehen Sie sich die Antworten an",
"quiz.restart": "Neu starten"
"quiz.restart": "Neu starten",
"map.search": "Suchen"
}),
Translation(language: "NL", data: {
"weather.hourly": "Volgende uren",
@ -59,7 +62,8 @@ List<Translation> translations = [
"sunday": "Zondag",
"menu": "Menu",
"quiz.seeResponses": "Zie de antwoorden",
"quiz.restart": "Herstarten"
"quiz.restart": "Herstarten",
"map.search": "Zoeken"
}),
Translation(language: "IT", data: {
"weather.hourly": "Le prossime ore",
@ -74,7 +78,8 @@ List<Translation> translations = [
"sunday": "Domenica",
"menu": "Menù",
"quiz.seeResponses": "Vedi le risposte",
"quiz.restart": "Ricomincia"
"quiz.restart": "Ricomincia",
"map.search": "Cerca"
}),
Translation(language: "ES", data: {
"weather.hourly": "Próximas horas",
@ -89,7 +94,8 @@ List<Translation> translations = [
"sunday": "Domingo",
"menu": "Menú",
"quiz.seeResponses": "Ver las respuestas",
"quiz.restart": "Reanudar"
"quiz.restart": "Reanudar",
"map.search": "Buscar"
}),
Translation(language: "PL", data: {
"weather.hourly": "Następne godziny",
@ -104,7 +110,8 @@ List<Translation> translations = [
"sunday": "Niedziela",
"menu": "Menu",
"quiz.seeResponses": "Zobacz odpowiedzi",
"quiz.restart": "Uruchom ponownie"
"quiz.restart": "Uruchom ponownie",
"map.search": "Szukaj"
}),
Translation(language: "CN", data: {
"weather.hourly": "接下来的几个小时",
@ -119,7 +126,8 @@ List<Translation> translations = [
"sunday": "星期日",
"menu": "菜单",
"quiz.seeResponses": "查看答案",
"quiz.restart": "重新开始"
"quiz.restart": "重新开始",
"map.search": "搜索"
}),
Translation(language: "UK", data: {
"weather.hourly": "Наступні години",
@ -134,7 +142,8 @@ List<Translation> translations = [
"sunday": "Неділя",
"menu": "Меню",
"quiz.seeResponses": "Подивіться відповіді",
"quiz.restart": "Перезапустіть"
"quiz.restart": "Перезапустіть",
"map.search": "Шукати"
}),
Translation(language: "AR", data: {
"weather.hourly": "الساعات القادمة",
@ -149,6 +158,7 @@ List<Translation> translations = [
"sunday": "الأحد",
"menu": "القائمة",
"quiz.seeResponses": "انظر الإجابات",
"quiz.restart": "إعادة تشغيل"
"quiz.restart": "إعادة تشغيل",
"map.search": "بحث"
}),
];

View File

@ -154,6 +154,7 @@ class _MainViewWidget extends State<MainViewWidget> {
break;
}
return Scaffold(
resizeToAvoidBottomInset: false,
body: SingleChildScrollView(
child: Container(
height: size.height,
@ -328,6 +329,7 @@ class _MainViewWidget extends State<MainViewWidget> {
);
} else {
return Scaffold(
resizeToAvoidBottomInset: false,
body: Container(
height: size.height,
width: size.width,

View File

@ -1,12 +1,14 @@
import 'dart:ui';
//import 'package:animated_tree_view/animated_tree_view.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
import 'package:generate_tree/generate_tree.dart';
import 'package:generate_tree/treeNode.dart';
import 'package:manager_api/api.dart';
import 'package:provider/provider.dart';
import 'package:tablet_app/Helpers/translationHelper.dart';
import 'package:tablet_app/Models/tabletContext.dart';
import 'package:tablet_app/app_context.dart';
import 'package:tablet_app/constants.dart';
@ -29,19 +31,28 @@ class _GeoPointFilterState extends State<GeoPointFilter> {
//late List<TreeNode> treeNodes;
late List<TreeNode> _filteredNodes;
late ValueNotifier<String> _searchTextNotifier;
final TextEditingController _searchController = TextEditingController();
FocusNode focusNode = new FocusNode();
@override
void initState() {
super.initState();
_searchTextNotifier = ValueNotifier<String>('');
//_searchTextNotifier.addListener(_onSearchTextChanged); // Cause the setState ?
// Construire les nœuds de l'arborescence en utilisant les catégories et les géopoints
//treeNodes = buildTreeNodes(widget.categories, widget.geoPoints);
_filteredNodes = buildTreeNodes(widget.categories, widget.geoPoints);
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
void _onSearchTextChanged() {
filterNodes(_searchTextNotifier.value);
//filterNodes(_searchTextNotifier.value);
}
List<TreeNode> buildTreeNodes(List<CategorieDTO> categories, List<GeoPointDTO> geoPoints) {
@ -53,7 +64,7 @@ class _GeoPointFilterState extends State<GeoPointFilter> {
id: category.id ?? 0,
title: parse(category.label!.firstWhere((l) => l.language == widget.language).value!).documentElement!.text,
children: [],
checked: false,
checked: true, // default true
show: false,
pid: 0,
commonID: 0,
@ -63,9 +74,9 @@ class _GeoPointFilterState extends State<GeoPointFilter> {
for (var geoPoint in geoPoints) {
if (geoPoint.categorieId == category.id) {
TreeNode geoPointNode = TreeNode(
id: geoPoint.id ?? 0,
id: int.parse(geoPoint.latitude!.substring(0,10).replaceAll(".", "")+geoPoint.longitude!.substring(0,10).replaceAll(".", "")),
title: parse(geoPoint.title!.firstWhere((l) => l.language == widget.language).value!).documentElement!.text,
checked: false,
checked: true, // default true
show: false,
children: [],
pid: 0,
@ -81,28 +92,60 @@ class _GeoPointFilterState extends State<GeoPointFilter> {
return nodes;
}
void filterNodes(String searchText) {
//setState(() {
_searchTextNotifier.value = searchText;
//_searchText = searchText;
// Filtrer les nœuds en fonction du texte de recherche
_filteredNodes = _searchTextNotifier.value.isEmpty
void filterNodes() {
String searchText = _searchController.text;
setState(() {
_filteredNodes = searchText.isEmpty
? buildTreeNodes(widget.categories, widget.geoPoints)
: _filterNodesBySearchText(_searchTextNotifier.value);
//});
: _filterNodesBySearchText(searchText);
// if unfocus, then
if(searchText.isEmpty && !focusNode.hasFocus) {
// widget.filteredPoints = //todo
sendFilteredGeoPoint();
}
});
}
sendFilteredGeoPoint() {
List<GeoPointDTO> checkedGeoPoints = [];
// Parcourez les nœuds filtrés pour récupérer les GeoPointDTO correspondants qui sont cochés
for (var node in _filteredNodes) {
if (node.children.isNotEmpty) {
for (var childNode in node.children) {
if (childNode.checked) {
checkedGeoPoints.add(widget.geoPoints.firstWhere((point) => int.parse(point.latitude!.substring(0,10).replaceAll(".", "")+point.longitude!.substring(0,10).replaceAll(".", "")) == childNode.id));
}
}
}
}
// Passez la liste des GeoPointDTO cochés à la fonction filteredPoints
widget.filteredPoints(checkedGeoPoints);
}
List<TreeNode> _filterNodesBySearchText(String searchText) {
List<TreeNode> filteredNodes = [];
for (var node in buildTreeNodes(widget.categories, widget.geoPoints)) {
if (node.title.toLowerCase().contains(searchText.toLowerCase())) {
// Si le titre du nœud contient le texte de recherche, ajoutez-le aux nœuds filtrés
if (_nodeOrChildrenContainsText(node, searchText)) {
filteredNodes.add(node);
}
}
return filteredNodes;
}
bool _nodeOrChildrenContainsText(TreeNode node, String searchText) {
if (node.title.toLowerCase().contains(searchText)) {
return true;
}
for (var childNode in node.children) {
if (_nodeOrChildrenContainsText(childNode, searchText)) {
return true;
}
}
return false;
}
@override
Widget build(BuildContext context) {
Size size = MediaQuery.of(context).size;
@ -112,54 +155,85 @@ class _GeoPointFilterState extends State<GeoPointFilter> {
var primaryColor = tabletAppContext.configuration != null ? tabletAppContext.configuration!.primaryColor != null ? new Color(int.parse(tabletAppContext.configuration!.primaryColor!.split('(0x')[1].split(')')[0], radix: 16)) : kTestSecondColor : kTestSecondColor;
return Container(
decoration: BoxDecoration(
color: kBackgroundColor.withOpacity(0.78),
borderRadius: BorderRadius.all(Radius.circular(tabletAppContext.configuration!.roundedValue?.toDouble() ?? 20.0)),
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
TextFormField(
//initialValue: 'Input text',
decoration: InputDecoration(
labelText: 'Recherche',
suffixIcon: Icon(
Icons.search,
return Positioned(
left: 5,
top: 35,
child: Container(
width: size.width * 0.3,
decoration: BoxDecoration(
color: kBackgroundColor.withOpacity(0.78),
borderRadius: BorderRadius.all(Radius.circular(tabletAppContext.configuration!.roundedValue?.toDouble() ?? 20.0)),
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(bottom: 8.0, top: 4.0),
child: TextField(
focusNode: focusNode,
controller: _searchController,
onChanged: (value) {
filterNodes();
},
cursorColor: Colors.black,
style: TextStyle(color: Colors.black),
decoration: InputDecoration(
labelText: TranslationHelper.getFromLocale("map.search", appContext.getContext()),
labelStyle: TextStyle(
color: focusNode.hasFocus ? primaryColor : Colors.black
),
focusColor: primaryColor,
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(tabletAppContext.configuration!.roundedValue?.toDouble() ?? 20.0)),
borderSide: BorderSide(color: primaryColor)),
//labelStyle: TextStyle(color: primaryColor),
suffixIcon: IconButton(
icon: _searchController.text.isNotEmpty ? Icon(Icons.close) : Icon(Icons.search),
onPressed: () {
if(_searchController.text.isNotEmpty) {
_searchController.text = "";
}
filterNodes();
if(_searchController.text.isNotEmpty) {
focusNode.unfocus();
}
},
),
),
),
),
onChanged: filterNodes,
),
ValueListenableBuilder<String>(
valueListenable: _searchTextNotifier,
builder: (context, value, _) {
return Container(
color: Colors.greenAccent,
height: size.height * 0.7,
width: size.width * 0.3,
child: GenerateTree(
data: _filteredNodes,
selectOneToAll: true,
textColor: Colors.black,
onChecked: (node, checked, commonID) {
if(checked) {
//node.
//var test =
//selectedGeoPoints.add();
}
print('isChecked : $checked');
print('common Node ID : ${commonID}');
print('children node data : ${node.children.map((e) => '${e.title}')}');
// Todo update selection
},
checkBoxColor: primaryColor,
childrenPadding: EdgeInsets.only(left: 20, top: 10, right: 0, bottom: 10),
),
);
}
),
],
ValueListenableBuilder<String>(
valueListenable: _searchTextNotifier,
builder: (context, value, _) {
return Container(
//color: Colors.greenAccent,
height: size.height * 0.65,
width: size.width * 0.3,
child: GenerateTree(
data: _filteredNodes,
selectOneToAll: true,
textColor: Colors.black,
onChecked: (node, checked, commonID) {
if(checked) {
//node.
//var test =
//selectedGeoPoints.add();
}
print('isChecked : $checked');
print('common Node ID : ${commonID}');
print('children node data : ${node.children.map((e) => '${e.title}')}');
// Todo update selection
sendFilteredGeoPoint();
},
checkBoxColor: primaryColor,
childrenPadding: EdgeInsets.only(left: 20, top: 10, right: 0, bottom: 10),
),
);
}
),
],
),
),
),
);

View File

@ -8,8 +8,11 @@ import 'package:manager_api/api.dart';
import 'package:provider/provider.dart';
import 'package:tablet_app/Components/multi_select_container.dart';
import 'package:tablet_app/Models/map-marker.dart';
import 'package:tablet_app/Models/tabletContext.dart';
import 'package:tablet_app/Screens/Map/geo_point_filter.dart';
import 'package:tablet_app/Screens/Map/map_context.dart';
import 'package:html/parser.dart' show parse;
import 'package:tablet_app/app_context.dart';
import 'package:tablet_app/constants.dart';
class GoogleMapView extends StatefulWidget {
@ -95,6 +98,8 @@ class _GoogleMapViewState extends State<GoogleMapView> {
@override
Widget build(BuildContext context) {
final mapContext = Provider.of<MapContext>(context);
final appContext = Provider.of<AppContext>(context);
TabletAppContext tabletAppContext = appContext.getContext() as TabletAppContext;
Size size = MediaQuery.of(context).size;
if(!init) {
@ -168,6 +173,22 @@ class _GoogleMapViewState extends State<GoogleMapView> {
},
),
),
Positioned(
left: 5,
top: 35,
child: SizedBox(
width: size.width * 0.3,
height: size.height * 0.76,
child: GeoPointFilter(
language: tabletAppContext.language!,
geoPoints: widget.mapDTO!.points!,
categories: widget.mapDTO!.categories!,
filteredPoints: (filteredPoints) {
print("COUCOU FILTERED POINTS");
print(filteredPoints);
}),
),
),
Positioned(
bottom: 35,
left: 10,

View File

@ -19,11 +19,13 @@ import 'package:tablet_app/constants.dart';
class MapBoxView extends StatefulWidget {
final MapDTO? mapDTO;
final List<GeoPointDTO> geoPoints;
final Uint8List? selectedMarkerIcon;
final String? language;
const MapBoxView({
Key? key,
this.mapDTO,
required this.geoPoints,
this.selectedMarkerIcon,
this.language,
}) : super(key: key);
@ -55,7 +57,7 @@ class AnnotationClickListener extends mapBox.OnPointAnnotationClickListener {
class _MapBoxViewState extends State<MapBoxView> {
late mapBox.MapboxMap? mapboxMap;
late mapBox.PointAnnotationManager? pointAnnotationManager;
mapBox.PointAnnotationManager? pointAnnotationManager;
bool filterZoneSelected = false;
createPoints() {
@ -116,20 +118,28 @@ class _MapBoxViewState extends State<MapBoxView> {
@override
void initState() {
pointsToShow = widget.mapDTO!.points;
pointsToShow = widget.geoPoints;//widget.mapDTO!.points;
selectedCategories = widget.mapDTO!.categories!.map((categorie) => categorie.label!.firstWhere((element) => element.language == widget.language).value!).toList();
super.initState();
}
@override
void dispose() {
// TODO: implement dispose
super.dispose();
}
@override
Widget build(BuildContext context) {
final mapContext = Provider.of<MapContext>(context);
pointsToShow = widget.geoPoints;//widget.mapDTO!.points;
if(pointAnnotationManager != null) {
pointAnnotationManager!.deleteAll();
pointAnnotationManager!.createMulti(createPoints());
//mapContext.notifyListeners();
}
final appContext = Provider.of<AppContext>(context);
TabletAppContext tabletAppContext = appContext.getContext() as TabletAppContext;
Size size = MediaQuery.of(context).size;
@ -178,7 +188,7 @@ class _MapBoxViewState extends State<MapBoxView> {
zoom: widget.mapDTO!.zoom != null ? widget.mapDTO!.zoom!.toDouble() : 12),
)
),
Positioned(
/*Positioned(
left: 5,
top: 35,
child: SizedBox(
@ -193,7 +203,7 @@ class _MapBoxViewState extends State<MapBoxView> {
print(filteredPoints);
}),
),
),
),*/
],
);
}

View File

@ -12,6 +12,8 @@ import 'package:tablet_app/Components/loading_common.dart';
import 'package:tablet_app/Models/map-marker.dart';
import 'dart:ui' as ui;
import 'package:flutter/widgets.dart';
import 'package:tablet_app/Models/tabletContext.dart';
import 'package:tablet_app/Screens/Map/geo_point_filter.dart';
import 'package:tablet_app/Screens/Map/map_box_view.dart';
import 'package:tablet_app/Screens/Map/marker_view.dart';
import 'package:http/http.dart' as http;
@ -35,6 +37,7 @@ class _MapViewWidget extends State<MapViewWidget> {
MapDTO? mapDTO;
//Completer<GoogleMapController> _controller = Completer();
Uint8List? selectedMarkerIcon;
late ValueNotifier<List<GeoPointDTO>> _geoPoints = ValueNotifier<List<GeoPointDTO>>([]);
Future<Uint8List> getBytesFromAsset(ByteData data, int width) async {
//ByteData data = await rootBundle.load(path);
@ -49,6 +52,7 @@ class _MapViewWidget extends State<MapViewWidget> {
@override
void initState() {
mapDTO = MapDTO.fromJson(jsonDecode(widget.section!.data!));
_geoPoints.value = mapDTO!.points!;
super.initState();
}
@ -68,44 +72,52 @@ class _MapViewWidget extends State<MapViewWidget> {
Widget build(BuildContext context) {
//final mapContext = Provider.of<MapContext>(context);
final appContext = Provider.of<AppContext>(context);
return Stack(
children: <Widget>[
FutureBuilder(
future: getByteIcon(mapDTO!.iconSource),
builder: (context, AsyncSnapshot<dynamic> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
print("snapshot");
print(snapshot.data);
print(selectedMarkerIcon);
switch(mapDTO!.mapProvider) {
case MapProvider.Google:
return GoogleMapView(language: appContext.getContext().language, mapDTO: mapDTO, selectedMarkerIcon: selectedMarkerIcon);
case MapProvider.MapBox:
return MapBoxView(language: appContext.getContext().language, mapDTO: mapDTO, selectedMarkerIcon: selectedMarkerIcon);
default:
// By default google
return GoogleMapView(language: appContext.getContext().language, mapDTO: mapDTO, selectedMarkerIcon: selectedMarkerIcon);
}
} else if (snapshot.connectionState == ConnectionState.none) {
return Text("No data");
} else {
return Center(
child: Container(
child: LoadingCommon()
)
);
}
}
),
MarkerViewWidget()
]
/*floatingActionButton: FloatingActionButton.extended(
onPressed: _goToTheLake,
label: Text('To the lake!'),
icon: Icon(Icons.directions_boat),
),*/
TabletAppContext tabletAppContext = appContext.getContext() as TabletAppContext;
return FutureBuilder(
future: getByteIcon(mapDTO!.iconSource),
builder: (context, AsyncSnapshot<dynamic> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return Stack(
children: <Widget>[
ValueListenableBuilder<List<GeoPointDTO>>(
valueListenable: _geoPoints,
builder: (context, value, _) {
switch(mapDTO!.mapProvider) {
case MapProvider.Google:
return GoogleMapView(language: appContext.getContext().language, mapDTO: mapDTO, selectedMarkerIcon: selectedMarkerIcon);
case MapProvider.MapBox:
return MapBoxView(language: appContext.getContext().language, geoPoints: value, mapDTO: mapDTO, selectedMarkerIcon: selectedMarkerIcon);
default:
// By default google
return GoogleMapView(language: appContext.getContext().language, mapDTO: mapDTO, selectedMarkerIcon: selectedMarkerIcon);
}
}
),
GeoPointFilter(
language: tabletAppContext.language!,
geoPoints: mapDTO!.points!,
categories: mapDTO!.categories!,
filteredPoints: (value) {
_geoPoints.value = value!;
}),
MarkerViewWidget(),
]
/*floatingActionButton: FloatingActionButton.extended(
onPressed: _goToTheLake,
label: Text('To the lake!'),
icon: Icon(Icons.directions_boat),
),*/
);
} else if (snapshot.connectionState == ConnectionState.none) {
return Text("No data");
} else {
return Center(
child: Container(
child: LoadingCommon()
)
);
}
}
);
}

View File

@ -24,7 +24,6 @@ class MarkerViewWidget extends StatefulWidget {
}
class _MarkerInfoWidget extends State<MarkerViewWidget> {
Size sizeScreen = new Size(1080.0, 1920.0); // Tablet resolution
CarouselController? sliderController;
ValueNotifier<int> currentIndex = ValueNotifier<int>(1);
@ -51,7 +50,7 @@ class _MarkerInfoWidget extends State<MarkerViewWidget> {
return new AnimatedPositioned(
duration: const Duration(milliseconds: 1500),
curve: Curves.easeInOutSine,
right: 25, // 140
left: 25, // 140
top: 10, // 150
child: Visibility(
visible: (mapContext.getSelectedMarker() as MapMarker).longitude != null,

View File

@ -118,11 +118,16 @@ class _MyAppState extends State<MyApp> {
const Locale('fr', 'FR'),
],*/
theme: ThemeData(
primaryColor: widget.tabletAppContext != null ? widget.tabletAppContext!.configuration != null ? widget.tabletAppContext!.configuration!.primaryColor != null ? new Color(int.parse(widget.tabletAppContext!.configuration!.secondaryColor!.split('(0x')[1].split(')')[0], radix: 16)): kTestSecondColor : kTestSecondColor : kTestSecondColor,
primaryColor: widget.tabletAppContext != null ? widget.tabletAppContext!.configuration != null ? widget.tabletAppContext!.configuration!.primaryColor != null ? new Color(int.parse(widget.tabletAppContext!.configuration!.primaryColor!.split('(0x')[1].split(')')[0], radix: 16)): kTestSecondColor : kTestSecondColor : kTestSecondColor,
primarySwatch: Colors.grey,
scaffoldBackgroundColor: widget.tabletAppContext != null ? widget.tabletAppContext!.configuration != null ? widget.tabletAppContext!.configuration!.secondaryColor != null ? new Color(int.parse(widget.tabletAppContext!.configuration!.secondaryColor!.split('(0x')[1].split(')')[0], radix: 16)): kBackgroundGrey : kBackgroundGrey : kBackgroundGrey,
fontFamily: "Roboto",
textTheme: TextTheme(bodyLarge: TextStyle(color: kTestSecondColor)),
textSelectionTheme: TextSelectionThemeData(
selectionHandleColor: widget.tabletAppContext != null ? widget.tabletAppContext!.configuration != null ? widget.tabletAppContext!.configuration!.primaryColor != null ? new Color(int.parse(widget.tabletAppContext!.configuration!.primaryColor!.split('(0x')[1].split(')')[0], radix: 16)): kTestSecondColor : kTestSecondColor : kTestSecondColor,
cursorColor: Colors.white,
selectionColor: widget.tabletAppContext != null ? widget.tabletAppContext!.configuration != null ? widget.tabletAppContext!.configuration!.primaryColor != null ? new Color(int.parse(widget.tabletAppContext!.configuration!.primaryColor!.split('(0x')[1].split(')')[0], radix: 16)): kTestSecondColor : kTestSecondColor : kTestSecondColor,
),
textTheme: TextTheme(bodyLarge: TextStyle(color: widget.tabletAppContext != null ? widget.tabletAppContext!.configuration != null ? widget.tabletAppContext!.configuration!.primaryColor != null ? new Color(int.parse(widget.tabletAppContext!.configuration!.primaryColor!.split('(0x')[1].split(')')[0], radix: 16)): kTestSecondColor : kTestSecondColor : kTestSecondColor)),
visualDensity: VisualDensity.adaptivePlatformDensity,
),
routes: {