I have a Flutter application, and I’ve tried a bit of everything so far. However, when I load certain maps into the app (ones larger than 2 MB), it crashes if I zoom in too deeply, and also if the default map is a large one, the app crashes on startup. I’ve tried pretty much everything I can think of, but the catch is that I can’t simply reduce the map size, (the issue might be the map resolution one of the maps has 6906 × 8679, but the simple ones have 711 × 489), because the same map is used for autonomous robot navigation, and I also need to be able to draw paths and points on it. Thank you!
The code for the map and the home
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'dart:ui' as ui;
import 'dart:ui';
.
.
.
class MapPage extends ConsumerStatefulWidget {
const MapPage({super.key, required this.mapSelected});
final Directory? mapSelected;
@override
ConsumerState<ConsumerStatefulWidget> createState() => MapPageState();
}
class MapPageState extends ConsumerState<MapPage> {
final viewTransformationController = TransformationController();
Future<List<ui.Image>> init() async {
var file = File(
path.join(
widget.mapSelected?.path ?? '',
FolderConstants.editedAreaMap,
),
);
if (!file.existsSync()) {
file = File(
path.join(
widget.mapSelected?.path ?? '',
FolderConstants.mapPath,
),
);
}
final bytesMap = file.readAsBytesSync();
final robotId = ref.read(robotConnectControllerProvider);
final typeRobot = RobotTypeToId.getRobotType(robotId ?? '').toString();
late Uint8List bytesRobject;
bytesRobject = Uint8List.view(
(await rootBundle.load(Assets.images.robjectRobot.path)).buffer,);}
final mapImage = await loadImage(bytesMap);
final robjectImage = await loadImage(bytesRobject);
return [mapImage, robjectImage];}
Future<ui.Image> loadImage(Uint8List img) async {
final completer = Completer<ui.Image>();
ui.decodeImageFromList(img, completer.complete);
return completer.future;
}
void zoomCenterFitView(BoxConstraints constraint, double zoomFactor) {
final xTrans = (constraint.maxWidth / 2) * (zoomFactor - 1);
final yTrans = (constraint.maxHeight / 2) * (zoomFactor - 1);
viewTransformationController.value.setEntry(0, 0, zoomFactor);
viewTransformationController.value.setEntry(1, 1, zoomFactor);
viewTransformationController.value.setEntry(2, 2, zoomFactor);
viewTransformationController.value.setEntry(0, 3, -xTrans);
viewTransformationController.value.setEntry(1, 3, -yTrans);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
ref.read(zoomValueControllerProvider.notifier).value = zoomFactor;
});
}
void zoomRobot(Offset? robotPostion, Offset rectSize, double zoomFactor) {
if (robotPostion == null) {
return;
}
final scaledPose = scalePoint(robotPostion, rectSize, zoomFactor);
final xTrans = scaledPose.dx * (zoomFactor - 1);
final yTrans = scaledPose.dy * (zoomFactor - 1);
viewTransformationController.value.setEntry(0, 0, zoomFactor);
viewTransformationController.value.setEntry(1, 1, zoomFactor);
viewTransformationController.value.setEntry(2, 2, zoomFactor);
viewTransformationController.value.setEntry(0, 3, -xTrans);
viewTransformationController.value.setEntry(1, 3, -yTrans);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
ref.read(zoomValueControllerProvider.notifier).value = zoomFactor;
});
}
Offset scalePoint(Offset point, Offset rectSize, double zoomFactor) {
final origin = rectSize / 2;
final pointInRect = point - origin;
final pointScaled = pointInRect * (1 + 1 / (zoomFactor - 1));
return pointScaled + origin;
}
double fittedRatio(Offset screenOffset, Offset rectOffset) {
final widthRatio = screenOffset.dx / rectOffset.dx;
final heightRatio = screenOffset.dy / rectOffset.dy;
return min(widthRatio, heightRatio);
}
Offset fitPoint(Offset point, Offset screenOffset, Offset rectOffset) {
final ratio = fittedRatio(screenOffset, rectOffset);
final fittedPoint = point * ratio;
final spaceDelta = screenOffset - rectOffset * ratio;
return fittedPoint + (spaceDelta / 2);
}
@override
Widget build(BuildContext context) {
ref
..watch(pathControllerProvider)
..watch(pointControllerProvider)
..watch(zoneControllerProvider)
..watch(loadDataFromLocalControllerProvider);
final robotPostions = ref.watch(robotPositionsControllerProvider);
final mapActions = ref.watch(mapActionControllerProvider);
final settingsData = ref.watch(displaySettingControllerProvider);
final zoomRatio = ref.watch(zoomValueControllerProvider);
final step = scaleStep.lowerBound(zoomRatio);
return widget.mapSelected == null
? Container()
: Stack(
children: [
FutureBuilder(
future: init(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return const SizedBox();
}
switch (snapshot.hasData) {
case true:
return LayoutBuilder(
builder: (context, constraint) {
const zoomFactor = 1.0;
final screenOffset = Offset(
constraint.maxWidth,
constraint.maxHeight,
);
final rectOffset = Offset(
snapshot.data!.first.width.toDouble(),
snapshot.data!.first.height.toDouble(),
);
WidgetsBinding.instance
.addPostFrameCallback((timeStamp) {
ref
.read(fittedValueControllerProvider.notifier)
.value = fittedRatio(screenOffset, rectOffset);
});
if (mapActions[5]) {
zoomCenterFitView(constraint, zoomFactor);
}
if (mapActions[4] && robotPostions.isNotEmpty) {
final fittedPoint = fitPoint(
Offset(
robotPostions.last.pose.x +
snapshot.data!.first.width / 2,
robotPostions.last.pose.y +
snapshot.data!.first.height / 2,
),
screenOffset,
rectOffset,
);
zoomRobot(
fittedPoint,
Offset(
constraint.maxWidth,
constraint.maxHeight,
),
double.tryParse(
settingsData.valueOrNull?.scale ?? '',
) ??
4,
);
}
return InteractiveViewer(
transformationController:
viewTransformationController,
minScale: 0.25,
maxScale: 64,
boundaryMargin:
const EdgeInsets.all(double.infinity),
onInteractionUpdate: (_) {
ref
.read(mapActionControllerProvider.notifier)
.changeValue(5, fixedValue: false);
ref
.read(mapActionControllerProvider.notifier)
.changeValue(4, fixedValue: false);
var zoomValue = viewTransformationController.value
.getMaxScaleOnAxis();
if (zoomValue <= 0.25) {
zoomValue = 0.25;
}
if (zoomValue >= 64) {
zoomValue = 64;
}
ref
.read(zoomValueControllerProvider.notifier)
.value = zoomValue;
},
onInteractionEnd: (_) {},
child: _CanvasDrawMap(
gridviewSize: distanceStep[step].toDouble(),
images: snapshot.data!,
screenSize: Size(
constraint.maxWidth,
constraint.maxHeight,
),
),
);
},
);
case false:
return const Center(child: CircularProgressIndicator());
}
},
),
Positioned.fill(
top: null,
child: Row(
children: [
const Spacer(),
MapActionButtons(callback: onActionButton),
Expanded(
child: Container(
padding: const EdgeInsets.only(left: 20),
alignment: Alignment.centerLeft,
child: const ScaleOfMap(),
),
),
],
),
),
],
);
}
void onActionButton(MapActionButtonType type) {
if (type == MapActionButtonType.rotateMap) {
if (ref.read(mapControllerProvider) == MapAction.rotateMap) {
ref.read(mapControllerProvider.notifier).changeState(MapAction.view);
return;
}
ref.read(mapControllerProvider.notifier).changeState(MapAction.rotateMap);
}
if (type case MapActionButtonType.fitScreen) {
ref
.read(mapActionControllerProvider.notifier)
.changeValue(4, fixedValue: false);
setState(() {});
}
if (type case MapActionButtonType.altRoute) {
final robotPostions =
ref.read(robotPositionsControllerProvider.notifier).state;
ref.read(robotPositionsControllerProvider.notifier).state = [
robotPostions.last,
];
setState(() {});
}
if (type case MapActionButtonType.zoomIn) {
ref
.read(mapActionControllerProvider.notifier)
.changeValue(5, fixedValue: false);
setState(() {});
}
}
}
class _CanvasDrawMap extends ConsumerStatefulWidget {
const _CanvasDrawMap({
required this.images,
required this.screenSize,
required this.gridviewSize,
});
final List<ui.Image> images;
final Size screenSize;
final double gridviewSize;
@override
ConsumerState<ConsumerStatefulWidget> createState() => __CanvasDrawMapState();
}
class __CanvasDrawMapState extends ConsumerState<_CanvasDrawMap> {
Future<RobotSetting?> getRobotSetting() async {
final robotId = ref.read(robotConnectControllerProvider);
final robotType = RobotTypeToId.getRobotType(robotId ?? '');
return ref
.read(listRobotSettingControllerProvider.notifier)
.getRobotByType(robotType ?? '');
}
RobotSetting? robotSetting;
@override
Widget build(BuildContext context) {
final listPoint = ref.watch(pointControllerProvider);
final listPath = ref.watch(pathControllerProvider);
final mapController = ref.watch(mapControllerProvider);
final listActionPoints = ref.watch(actionPointsControllerProvider);
final listZone = ref.watch(zoneControllerProvider);
final listButtonActionMap = ref.watch(mapActionControllerProvider);
final robotPositions = ref.watch(robotPositionsControllerProvider);
final ultrasonic = ref.watch(ultrasonicControllerProvider);
final estimatePath = ref.watch(estimatePathControllerProvider);
final isMapEdit = ref.watch(isEditMapProvider);
final mapInfo = ref.watch(mapInfoControllerProvider);
final robotId = ref.watch(robotConnectControllerProvider);
final linkPath = ref.watch(linkPathControllerProvider);
final disinfectionPath = ref.watch(areaPathControllerProvider);
final rotateMapTheta = ref.watch(rotateMapController);
final homePosition = ref.watch(homeDefaultController);
final listRobotSetting = ref.watch(listSettingControllerProvider);
if (listRobotSetting case AsyncData(:final value)) {
final robotId = ref.read(robotConnectControllerProvider);
final robotType = RobotTypeToId.getRobotType(robotId ?? '');
robotSetting = value.firstWhereOrNull((e) => e.type == robotType);
}
if ((ref.watch(connectStatusControllerProvider).valueOrNull ?? false) &&
ref.watch(robotConnectControllerProvider) != null) {
ref.listen(monitoringDataService, (previous, next) {
final currentRobot = next.firstWhereOrNull((r) => r.robotId == robotId);
if (currentRobot == null) return;
final theta = ref.read(rotateMapController);
final newPoint = CalculatePoint.rotatePosition(
Point(
pose: Pose(
theta: currentRobot.position.degree,
x: currentRobot.position.x,
y: currentRobot.position.y,
).convertToAppCoordinate(mapInfo),
),
theta,
);
final homePosition = CalculatePoint.rotatePosition(
Point(
pose: Pose(
theta: currentRobot.homePosition.degree,
x: currentRobot.homePosition.x,
y: currentRobot.homePosition.y,
).convertToAppCoordinate(mapInfo),
),
theta,
);
ref.read(homeDefaultController.notifier).state = homePosition;
List<Point> listPoint;
if (listButtonActionMap[3]) {
listPoint = List<Point>.from(robotPositions)..add(newPoint);
} else {
listPoint = [newPoint];
}
ref.read(robotPositionsControllerProvider.notifier).state = listPoint;
});
}
Future<void> onTapDown(TapDownDetails details) async {
switch (mapController) {
case MapAction.recordPath:
final positionClick =
convertToCanvasCoordinate(details.localPosition);
final robotPosition = robotPositions.lastOrNull;
if (robotPosition == null) return;
final recordButton = RRect.fromRectAndRadius(
Rect.fromCenter(
center:
Offset(robotPosition.pose.x + 24, robotPosition.pose.y - 50),
width: 40,
height: 24,
),
const Radius.circular(24),
);
final cancelButton = RRect.fromRectAndRadius(
Rect.fromCenter(
center:
Offset(robotPosition.pose.x - 24, robotPosition.pose.y - 50),
width: 40,
height: 24,
),
const Radius.circular(24),
);
if (cancelButton.contains(positionClick)) {
await cancelRecordPath(listActionPoints, ref, context);
}
if (recordButton.contains(positionClick)) {
addPointRecordPath(
robotPosition,
ref,
context,
);
}
case _:
break;
}
return;
}
return Container(
color: Colors.transparent,
width: widget.screenSize.width,
height: widget.screenSize.height,
child: FittedBox(
child: IgnorePointer(
ignoring: mapController == MapAction.view,
child: GestureDetector(
onTapDown: onTapDown,
onPanStart: (po) => onPanStart(po, ref),
onPanDown: (po) => onPanDown(po, ref),
onPanUpdate: (po) => onPanUpdate(po, ref),
onPanEnd: (po) => onPanEnd(po, ref, context),
child: Container(
alignment: Alignment.center,
color: Colors.transparent,
width: widget.images.first.width.toDouble(),
height: widget.images.first.height.toDouble(),
child: CustomPaint(
painter: MapPainter(
image: widget.images.first,
action: mapController,
rotateMapTheta: rotateMapTheta,
),
foregroundPainter: ActionPainter(
listPoint: listPoint,
images: widget.images,
context: context,
listPath: listPath,
listActionPoint: listActionPoints,
action: mapController,
gridviewWidth: widget.gridviewSize,
listZone: listZone,
listShowButton: listButtonActionMap,
robotPostions: robotPositions,
estimatePath: estimatePath,
ultrasonic: ultrasonic,
isMapEdit: isMapEdit,
linkPath: linkPath,
disinfectionPath: disinfectionPath,
homeDefault: homePosition,
robotSetting: robotSetting,
drawPointAction: true,
mapInfo: ref.watch(mapInfoControllerProvider),
mapRotate: rotateMapTheta,
),
),
),
),
),
),
);
}
Future<void> cancelRecordPath(
List<Point> listActionPoints,
WidgetRef ref,
BuildContext context,
) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (_) => ConfirmDialog(
title: S.of(context).cancelRecord,
question: S.of(context).cancelRecordConfirm,
confirmText: S.of(context).cancel,
cancelText: S.of(context).close,
),
) ??
false;
if (!confirmed) return;
listActionPoints.clear();
ref.read(mapControllerProvider.notifier).changeState(MapAction.view);
await showDialog<void>(
context: context,
builder: (_) => SuccessDialog(
title: S.of(context).cancelRecord,
content: S.of(context).cancelRecordSuccess,
),
);
}
void addPointRecordPath(
Point robotPosition,
WidgetRef ref,
BuildContext context,
) {
final listActionPoints = ref.read(actionPointsControllerProvider);
if (listActionPoints.isEmpty) {
ref
.read(actionPointsControllerProvider.notifier)
.addNewPoint(robotPosition, context);
showDialog<void>(
context: context,
builder: (_) => SuccessDialog(
title: S.of(context).pathRecord,
content: S.of(context).addPointRecordSuccess,
),
);
return;
}
final xDifference = robotPosition.pose.x - listActionPoints.last.pose.x;
final yDifference = robotPosition.pose.y - listActionPoints.last.pose.y;
final minDistance = ref.watch(recordPathController);
final distance =
sqrt(xDifference * xDifference + yDifference * yDifference);
if (distance < minDistance && listActionPoints.isNotEmpty) {
showDialog<void>(
context: context,
builder: (_) => ErrorDialog(
title: S.of(context).pathRecord,
content: S.of(context).addPointRecordFailed,
),
);
return;
}
ref
.read(actionPointsControllerProvider.notifier)
.addNewPoint(robotPosition, context);
showDialog<void>(
context: context,
builder: (_) => SuccessDialog(
title: S.of(context).pathRecord,
content: S.of(context).addPointRecordSuccess,
),
);
}
Future<void> addCursorPoint(
BuildContext context,
WidgetRef ref,
Point offset,
MapInfo mapInfo,
) async {
ref.read(pointControllerProvider.notifier).addListPoint(
[
Point(
pose: Pose(theta: 0, x: offset.pose.x, y: offset.pose.y),
pointType: PointType.shelter,
),
],
);
ref.read(mapControllerProvider.notifier).changeState(MapAction.view);
final confirmResult = await showDialog<bool>(
barrierDismissible: false,
context: context,
builder: (_) => ConfirmDialog(
title: S.of(context).goToCursor,
question: S.of(context).goToCursorConfirm,
confirmText: S.of(context).goTo,
),
);
if (!(confirmResult ?? false)) {
ref.read(pointControllerProvider.notifier).removeLastPoint();
return;
}
final mapRotate = ref.read(rotateMapController);
final robotActionService = RobotActionService(context: context, ref: ref);
final robotPoint = CalculatePoint.rotatePosition(offset, -mapRotate);
final newPosition = robotPoint.pose.convertToRobotCoordinate(mapInfo);
await robotActionService.moveToPoint(
CursorRequest(
markerName: 'NAV_POI',
x: newPosition.x,
y: newPosition.y,
degree: offset.pose.theta - mapRotate,
),
);
ref.read(pointControllerProvider.notifier).removeLastPoint();
}
Offset convertToCanvasCoordinate(Offset offset) {
return Offset(
offset.dx - (widget.images.first.width / 2),
offset.dy - (widget.images.first.height / 2),
);
}
Offset? checkExistPoint(Offset offset, List<Point> listPoint) {
for (final point in listPoint) {
final pointRect = Rect.fromCircle(
center: Offset(point.pose.x, point.pose.y),
radius: 10,
);
if (pointRect.contains(offset)) {
return Offset(point.pose.x, point.pose.y);
}
}
return null;
}
void onPanDown(DragDownDetails po, WidgetRef ref) {
final mapAction = ref.read(mapControllerProvider);
final listAction = ref.read(actionPointsControllerProvider);
final actionListController =
ref.read(actionPointsControllerProvider.notifier);
if (mapAction == MapAction.rotateMap) {
if (listAction.isNotEmpty) {
actionListController.clearPoints();
}
final pointClicked = convertToCanvasCoordinate(po.localPosition);
actionListController.addPoint(
Point(
pose: Pose(
theta: ref.read(rotateMapController),
x: pointClicked.dx,
y: pointClicked.dy,
),
),
);
}
}
void onPanUpdate(DragUpdateDetails po, WidgetRef ref) {
final mapAction = ref.read(mapControllerProvider);
if (mapAction == MapAction.rotateMap) {
final listAction = ref.read(actionPointsControllerProvider);
final pointClicked = convertToCanvasCoordinate(po.localPosition);
final firstTouch = listAction.first;
final thetaPoint = calculateTheta(
currentPoint: Offset(firstTouch.pose.x, firstTouch.pose.y),
lastPoint: Offset.zero,
);
final theta = calculateTheta(
currentPoint: pointClicked,
lastPoint: Offset.zero,
) -
thetaPoint;
ref.read(rotateMapController.notifier).state =
firstTouch.pose.theta + theta;
final lastPoint = listAction.last;
final thetaLastPoint = calculateTheta(
currentPoint: Offset(lastPoint.pose.x, lastPoint.pose.y),
lastPoint: Offset.zero,
);
final newTheta = calculateTheta(
currentPoint: pointClicked,
lastPoint: Offset.zero,
) -
thetaLastPoint;
updatePosition(ref, newTheta, pointClicked);
ref.read(actionPointsControllerProvider.notifier).addPoint(
Point(
pose: Pose(x: pointClicked.dx, y: pointClicked.dy, theta: 0),
),
);
}
if (mapAction == MapAction.addPoint) {
final pointClicked = convertToCanvasCoordinate(po.localPosition);
final lastPoint = ref.read(actionPointsControllerProvider).last;
final theta = calculateTheta(
currentPoint: Offset(pointClicked.dx, pointClicked.dy),
lastPoint: Offset(lastPoint.pose.x, lastPoint.pose.y),
);
ref.read(actionPointsControllerProvider.notifier).updatePoint(
offset: Offset(lastPoint.pose.x, lastPoint.pose.y),
theta: theta,
pointType: PointType.shelter,
);
}
}
double calculateTheta({
required Offset currentPoint,
required Offset lastPoint,
}) {
return atan2(
currentPoint.dx - lastPoint.dx,
currentPoint.dy - lastPoint.dy,
) -
pi / 2;
}
void updatePosition(WidgetRef ref, double newTheta, Offset pointClicked) {
ref.read(pointControllerProvider.notifier).updatePointRotate(newTheta);
ref.read(pathControllerProvider.notifier).updatePathRotate(newTheta);
ref
.read(areaPathControllerProvider.notifier)
.updatePoint(pointClicked, newTheta);
ref.read(linkPathControllerProvider.notifier).updatePathRotate(newTheta);
ref
.read(zoneControllerProvider.notifier)
.updatePoint(pointClicked, newTheta);
final listRobotPosition = ref.watch(robotPositionsControllerProvider);
final newListRobot = <Point>[];
for (final e in listRobotPosition) {
final newE = CalculatePoint.rotatePosition(e, newTheta);
newListRobot.add(newE);
}
ref.read(robotPositionsControllerProvider.notifier).state = newListRobot;
}
Future<void> onPanEnd(
DragEndDetails po,
WidgetRef ref,
BuildContext context,
) async {
final mapAction = ref.read(mapControllerProvider);
if (mapAction == MapAction.rotateMap) {
ref.read(actionPointsControllerProvider.notifier).clearPoints();
}
if (mapAction == MapAction.addPoint &&
ref.read(initPoseControllerProvider) == true) {
final point = ref.read(actionPointsControllerProvider).first;
ref.read(mapControllerProvider.notifier).changeState(MapAction.view);
final confirmResult = await showDialog<bool>(
barrierDismissible: false,
context: context,
builder: (_) => ConfirmDialog(
title: S.of(context).setInitPose,
question: S.of(context).setInitPoseQuestion,
confirmText: S.of(context).set,
),
);
if (!(confirmResult ?? false)) {
ref.read(actionPointsControllerProvider.notifier).clearPoints();
return;
}
final mapInfo = ref.watch(mapInfoControllerProvider);
final robotActionService = RobotActionService(context: context, ref: ref);
final mapRotate = ref.read(rotateMapController);
final rotatePoint = CalculatePoint.rotatePosition(point, -mapRotate);
final newPosition = rotatePoint.pose.convertToRobotCoordinate(mapInfo);
await robotActionService.initPosition(
InitPoseRequest(
x: newPosition.x,
y: newPosition.y,
theta: point.pose.theta - mapRotate,
),
);
ref.read(actionPointsControllerProvider.notifier).clearPoints();
}
if (mapAction == MapAction.addPoint &&
ref.read(initPoseControllerProvider) == false) {
final point = ref.read(actionPointsControllerProvider).first;
final mapInfo = ref.read(mapInfoControllerProvider);
await addCursorPoint(
context,
ref,
point,
mapInfo,
);
ref.read(actionPointsControllerProvider.notifier).clearPoints();
}
}
void onPanStart(DragStartDetails po, WidgetRef ref) {
final mapAction = ref.read(mapControllerProvider);
if (mapAction == MapAction.addPoint) {
final pointClicked = convertToCanvasCoordinate(po.localPosition);
ref.read(actionPointsControllerProvider.notifier).addPoint(
Point(
pose: Pose(
theta: 0,
x: pointClicked.dx,
y: pointClicked.dy,
),
pointType: PointType.shelter,
),
);
}
}
@override
void dispose() {
super.dispose();
}
}
The solution that worked for me, with the aid of chatGPT -> Flutter's compute() function to resize images in background threads, plus caches resized images to avoid reprocessing the same files. Respecting the max image size setting from display settings, this automatically resizes large images to prevent crashes.