import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; /// A marker bitmap plus a stable identity for it. /// /// MapLibre registers images on the style by name, so every distinct bitmap /// needs a key that is equal whenever the pixels are equal. @immutable class MarkerIcon { const MarkerIcon(this.key, this.bytes); final String key; final Uint8List bytes; @override bool operator ==(Object other) => other is MarkerIcon && other.key == key; @override int get hashCode => key.hashCode; } /// Marker bitmaps drawn at runtime with a canvas. /// /// MapLibre has no equivalent of Google's `BitmapDescriptor.defaultMarker*`, /// so the pins the app used to get for free are drawn here instead. Everything /// is generated in-process — no new image assets, no network fetch. class MapMarkerIcons { const MapMarkerIcons._(); static final Map _cache = {}; /// Classic teardrop pin, anchored at its tip. static Future pin(Color color) => _memo('pin_${color.toARGB32()}', () => _drawPin(color)); /// Filled circle with a white ring — used for the rider's own position. static Future dot(Color color) => _memo('dot_${color.toARGB32()}', () => _drawDot(color)); /// Numbered step marker for the multi-stop route screen. static Future numberedCircle( int number, { required Color primaryColor, bool isSkipped = false, bool isNext = false, }) => _memo( 'step_${number}_${primaryColor.toARGB32()}_${isSkipped}_$isNext', () => _drawNumberedCircle( number, primaryColor: primaryColor, isSkipped: isSkipped, isNext: isNext, ), ); static Future _memo( String key, Future Function() build, ) async { final cached = _cache[key]; if (cached != null) return cached; return _cache[key] = MarkerIcon(key, await build()); } static Future _toBytes( ui.PictureRecorder recorder, int width, int height, ) async { final image = await recorder.endRecording().toImage(width, height); final data = await image.toByteData(format: ui.ImageByteFormat.png); image.dispose(); return data!.buffer.asUint8List(); } static Future _drawPin(Color color) async { const double width = 72; const double height = 96; const double radius = 27; const center = Offset(width / 2, radius + 4); final recorder = ui.PictureRecorder(); final canvas = Canvas(recorder); final body = Paint() ..color = color ..isAntiAlias = true; final outline = Paint() ..color = Colors.white ..style = PaintingStyle.stroke ..strokeWidth = 4 ..isAntiAlias = true; // Tail: a curve from the circle's lower flanks down to the anchor point. final tail = Path() ..moveTo(center.dx - radius + 4, center.dy + radius - 9) ..quadraticBezierTo( center.dx - 6, height - 14, center.dx, height - 4, ) ..quadraticBezierTo( center.dx + 6, height - 14, center.dx + radius - 4, center.dy + radius - 9, ) ..close(); canvas.drawPath(tail, body); canvas.drawCircle(center, radius, body); canvas.drawCircle(center, radius, outline); canvas.drawCircle(center, 9, Paint()..color = Colors.white); return _toBytes(recorder, width.toInt(), height.toInt()); } static Future _drawDot(Color color) async { const double size = 48; const center = Offset(size / 2, size / 2); final recorder = ui.PictureRecorder(); final canvas = Canvas(recorder); canvas.drawCircle( center, 20, Paint() ..color = color.withValues(alpha: 0.25) ..isAntiAlias = true, ); canvas.drawCircle( center, 12, Paint() ..color = Colors.white ..isAntiAlias = true, ); canvas.drawCircle( center, 9, Paint() ..color = color ..isAntiAlias = true, ); return _toBytes(recorder, size.toInt(), size.toInt()); } /// Preserves the exact look of the previous Google Maps step marker. static Future _drawNumberedCircle( int number, { required Color primaryColor, required bool isSkipped, required bool isNext, }) async { const double size = 70; final recorder = ui.PictureRecorder(); final canvas = Canvas(recorder); const center = Offset(size / 2, size / 2); final paint = Paint()..color = isSkipped ? Colors.orange : primaryColor; canvas.drawCircle(center, 15, paint); final border = Paint() ..color = isSkipped ? Colors.orange.shade900 : Colors.white ..style = PaintingStyle.stroke ..strokeWidth = isSkipped ? 4 : 3; canvas.drawCircle(center, 15, border); final textPainter = TextPainter( text: TextSpan( text: number.toString(), style: const TextStyle( fontSize: 19, color: Colors.white, fontWeight: FontWeight.bold, ), ), textDirection: TextDirection.ltr, ); textPainter.layout(); textPainter.paint( canvas, Offset( center.dx - textPainter.width / 2, center.dy - textPainter.height / 2, ), ); if (isNext) { final signPaint = Paint() ..color = Colors.green ..style = PaintingStyle.fill; final signRect = RRect.fromRectAndRadius( Rect.fromCenter(center: const Offset(size / 2, 8), width: 45, height: 30), const Radius.circular(4), ); canvas.drawRRect(signRect, signPaint); final signBorder = Paint() ..color = Colors.white ..style = PaintingStyle.stroke ..strokeWidth = 1.5; canvas.drawRRect(signRect, signBorder); final nextTextPainter = TextPainter( text: const TextSpan( text: 'NEXT', style: TextStyle( fontSize: 12, color: Colors.white, fontWeight: FontWeight.bold, ), ), textDirection: TextDirection.ltr, ); nextTextPainter.layout(); nextTextPainter.paint( canvas, Offset(size / 2 - nextTextPainter.width / 2, 4), ); final arrowPath = Path() ..moveTo(size / 2, 18) ..lineTo(size / 2 - 4, 24) ..lineTo(size / 2 + 4, 24) ..close(); canvas.drawPath( arrowPath, Paint() ..color = Colors.white ..style = PaintingStyle.fill, ); } return _toBytes(recorder, size.toInt(), size.toInt()); } }