diff --git a/example/lib/misc/tile_providers.dart b/example/lib/misc/tile_providers.dart index 8c6db0b27..e0ba8ede2 100644 --- a/example/lib/misc/tile_providers.dart +++ b/example/lib/misc/tile_providers.dart @@ -1,6 +1,7 @@ import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_cancellable_tile_provider/flutter_map_cancellable_tile_provider.dart'; +// TODO: This causes unneccessary rebuilding TileLayer get openStreetMapTileLayer => TileLayer( urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', userAgentPackageName: 'dev.fleaflet.flutter_map.example', diff --git a/example/lib/pages/polygon.dart b/example/lib/pages/polygon.dart index 369b2eb18..8fc6bcacd 100644 --- a/example/lib/pages/polygon.dart +++ b/example/lib/pages/polygon.dart @@ -23,268 +23,277 @@ class _PolygonPageState extends State { List>? _hoverGons; bool _useInvertedFill = false; + PolygonLabelPlacementCalculator _labelPlacementCalculator = + const PolygonLabelPlacementCalculator.centroid(); - final _polygonsRaw = >[ - Polygon( - points: const [ - LatLng(51.5, -0.09), - LatLng(53.3498, -6.2603), - LatLng(48.8566, 2.3522), - ], - borderColor: Colors.red, - borderStrokeWidth: 4, - hitValue: ( - title: 'Basic Unfilled Polygon', - subtitle: 'Nothing really special here...', - ), - ), - Polygon( - points: const [ - LatLng(55.5, -0.09), - LatLng(54.3498, -6.2603), - LatLng(52.8566, 2.3522), - ], - color: Colors.purple, - borderColor: Colors.yellow, - borderStrokeWidth: 4, - hitValue: ( - title: 'Basic Filled Polygon', - subtitle: 'Nothing really special here...', - ), - ), - Polygon( - points: const [ - LatLng(46.35, 4.94), - LatLng(46.22, -0.11), - LatLng(44.399, 1.76), - ], - pattern: StrokePattern.dashed(segments: const [50, 20]), - borderStrokeWidth: 4, - borderColor: Colors.lightBlue, - color: Colors.yellow, - hitValue: ( - title: 'Polygon With Dashed Borders', - subtitle: '...', - ), - ), - Polygon( - points: const [ - LatLng(60.16, -9.38), - LatLng(60.16, -4.16), - LatLng(61.18, -4.16), - LatLng(61.18, -9.38), - ], - borderStrokeWidth: 4, - borderColor: Colors.purple, - label: 'Label!', - hitValue: ( - title: 'Polygon With Label', - subtitle: 'This is a very descriptive label!', - ), - ), - Polygon( - points: const [ - LatLng(59.77, -10.28), - LatLng(58.21, -10.28), - LatLng(58.21, -7.01), - LatLng(59.77, -7.01), - LatLng(60.77, -6.01), - ], - borderStrokeWidth: 4, - borderColor: Colors.purple, - label: 'Rotated!', - rotateLabel: true, - labelPlacement: PolygonLabelPlacement.polylabel, - hitValue: ( - title: 'Polygon With Rotated Label', - subtitle: "Now you don't have to turn your head so much", - ), - ), - Polygon( - points: const [ - LatLng(50, -18), - LatLng(50, -14), - LatLng(51.5, -12.5), - LatLng(54, -14), - LatLng(54, -18), - ].map((latlng) => LatLng(latlng.latitude, latlng.longitude + 8)).toList(), - pattern: const StrokePattern.dotted(), - holePointsList: [ - const [ - LatLng(52, -9), - LatLng(52, -8), - LatLng(51.5, -7.5), - LatLng(51, -8), - LatLng(51, -9), - ], - const [ - LatLng(53.5, -9), - LatLng(53.5, -8), - LatLng(53, -7), - LatLng(52.25, -7), - LatLng(52.25, -8), - LatLng(52.75, -9), - ], - const [ - LatLng(52.683614, -8.141285), - LatLng(51.663083, -8.684529), - LatLng(51.913924, -7.2193), - ], - ], - borderStrokeWidth: 4, - borderColor: Colors.orange, - color: Colors.orange.withAlpha(128), - label: 'This one is not\nperformantly rendered', - rotateLabel: true, - labelPlacement: PolygonLabelPlacement.centroid, - labelStyle: const TextStyle(color: Colors.black), - hitValue: ( - title: 'Polygon With Hole', - subtitle: 'A bit like Swiss cheese maybe?', - ), - ), - Polygon( - points: const [ - LatLng(50, -18), - LatLng(53, -16), - LatLng(51.5, -12.5), - LatLng(54, -14), - LatLng(54, -18), - ] - .map((latlng) => LatLng(latlng.latitude - 6, latlng.longitude + 8)) - .toList(), - pattern: const StrokePattern.dotted(), - holePointsList: [ - const [ - LatLng(46, -9), - LatLng(46, -8), - LatLng(45.5, -7.5), - LatLng(45, -8), - LatLng(45, -9), - ].reversed.toList(growable: false), // Testing winding consitency - const [ - LatLng(47.5, -9), - LatLng(47.5, -8), - LatLng(47, -7), - LatLng(46.25, -7), - LatLng(46.25, -8), - LatLng(46.75, -9), - ].reversed.toList(growable: false), - const [ - LatLng(46.683614, -8.141285), - LatLng(45.663083, -8.684529), - LatLng(45.913924, -7.2193), - ].reversed.toList(growable: false), - ], - borderStrokeWidth: 4, - borderColor: Colors.orange, - color: Colors.orange.withAlpha(128), - label: 'This one is not\nperformantly rendered', - rotateLabel: true, - labelPlacement: PolygonLabelPlacement.centroid, - labelStyle: const TextStyle(color: Colors.black), - hitValue: ( - title: 'Polygon With Hole & Self Intersection', - subtitle: 'This one still works with performant rendering', - ), - ), - Polygon( - points: const [ - LatLng(61.861042, 0.946502), - LatLng(61.861458, 0.949468), - LatLng(61.861427, 0.949626), - LatLng(61.859015, 0.951513), - LatLng(61.858129, 0.952652) - ], - holePointsList: [], - color: Colors.lightGreen.withAlpha(128), - borderColor: Colors.lightGreen.withAlpha(128), - borderStrokeWidth: 10, - hitValue: ( - title: 'Testing opacity treatment (small)', - subtitle: - "Holes shouldn't be cut, and colors should be mixed correctly", - ), - ), - Polygon( - points: const [ - LatLng(61.861042, 0.946502), - LatLng(61.861458, 0.949468), - LatLng(61.861427, 0.949626), - LatLng(61.859015, 0.951513), - LatLng(61.858129, 0.952652), - LatLng(61.857633, 0.953214), - LatLng(61.855842, 0.954683), - LatLng(61.855769, 0.954692), - LatLng(61.855679, 0.954565), - LatLng(61.855417, 0.953926), - LatLng(61.855268, 0.953431), - LatLng(61.855173, 0.952443), - LatLng(61.855161, 0.951147), - LatLng(61.855222, 0.950822), - LatLng(61.855928, 0.948422), - LatLng(61.856365, 0.946638), - LatLng(61.856456, 0.946586), - LatLng(61.856787, 0.946656), - LatLng(61.857578, 0.946675), - LatLng(61.859338, 0.946453), - LatLng(61.861042, 0.946502) - ], - holePointsList: const [ - [ - LatLng(61.858881, 0.947234), - LatLng(61.858728, 0.947126), - LatLng(61.858562, 0.947132), - LatLng(61.858458, 0.947192), - LatLng(61.85844, 0.947716), - LatLng(61.858488, 0.947819), - LatLng(61.858766, 0.947818), - LatLng(61.858893, 0.947779), - LatLng(61.858975, 0.947542), - LatLng(61.858881, 0.947234) - ] - ], - color: Colors.lightGreen.withAlpha(128), - borderColor: Colors.lightGreen.withAlpha(128), - borderStrokeWidth: 10, - hitValue: ( - title: 'Testing opacity treatment (large)', - subtitle: - "Holes shouldn't be cut, and colors should be mixed correctly", - ), - ), - Polygon( - points: const [ - LatLng(40, 150), - LatLng(45, 160), - LatLng(50, 170), - LatLng(55, 180), - LatLng(50, -170), - LatLng(45, -160), - LatLng(40, -150), - LatLng(35, -160), - LatLng(30, -170), - LatLng(25, -180), - LatLng(30, 170), - LatLng(35, 160), - ], - holePointsList: const [ - [ - LatLng(45, 175), - LatLng(45, -175), - LatLng(35, -175), - LatLng(35, 175), - ], - ], - color: const Color(0xFFFF0000), - hitValue: ( - title: 'Big Red Diamond', - subtitle: 'Across the anti-meridian boundary', - ), - ), - ]; - late final _polygons = - Map.fromEntries(_polygonsRaw.map((e) => MapEntry(e.hitValue, e))); + late var _polygonsRaw = generatePolygons(); + List> generatePolygons() => [ + Polygon( + points: const [ + LatLng(51.5, -0.09), + LatLng(53.3498, -6.2603), + LatLng(48.8566, 2.3522), + ], + borderColor: Colors.red, + borderStrokeWidth: 4, + hitValue: ( + title: 'Basic Unfilled Polygon', + subtitle: 'Nothing really special here...', + ), + ), + Polygon( + points: const [ + LatLng(55.5, -0.09), + LatLng(54.3498, -6.2603), + LatLng(52.8566, 2.3522), + ], + color: Colors.purple, + borderColor: Colors.yellow, + borderStrokeWidth: 4, + label: 'Label!', + labelPlacementCalculator: _labelPlacementCalculator, + hitValue: ( + title: 'Basic Filled Polygon', + subtitle: 'Nothing really special here...', + ), + ), + Polygon( + points: const [ + LatLng(46.35, 4.94), + LatLng(46.22, -0.11), + LatLng(44.399, 1.76), + ], + pattern: StrokePattern.dashed(segments: const [50, 20]), + borderStrokeWidth: 4, + borderColor: Colors.lightBlue, + color: Colors.yellow, + hitValue: ( + title: 'Polygon With Dashed Borders', + subtitle: '...', + ), + ), + Polygon( + points: const [ + LatLng(60.16, -9.38), + LatLng(60.16, -4.16), + LatLng(61.18, -4.16), + LatLng(61.18, -9.38), + ], + borderStrokeWidth: 4, + borderColor: Colors.purple, + label: 'Label!', + labelPlacementCalculator: _labelPlacementCalculator, + labelStyle: const TextStyle(color: Colors.black), + hitValue: ( + title: 'Polygon With Label', + subtitle: 'This is a very descriptive label!', + ), + ), + Polygon( + points: const [ + LatLng(59.77, -10.28), + LatLng(58.21, -10.28), + LatLng(58.21, -7.01), + LatLng(59.77, -7.01), + LatLng(60.77, -6.01), + ], + borderStrokeWidth: 4, + borderColor: Colors.purple, + label: 'Rotated!', + rotateLabel: true, + labelPlacementCalculator: _labelPlacementCalculator, + labelStyle: const TextStyle(color: Colors.black), + hitValue: ( + title: 'Polygon With Rotated Label', + subtitle: "Now you don't have to turn your head so much", + ), + ), + Polygon( + points: const [ + LatLng(50, -18), + LatLng(50, -14), + LatLng(51.5, -12.5), + LatLng(54, -14), + LatLng(54, -18), + ] + .map((latlng) => LatLng(latlng.latitude, latlng.longitude + 8)) + .toList(), + pattern: const StrokePattern.dotted(), + holePointsList: [ + const [ + LatLng(52, -9), + LatLng(52, -8), + LatLng(51.5, -7.5), + LatLng(51, -8), + LatLng(51, -9), + ], + const [ + LatLng(53.5, -9), + LatLng(53.5, -8), + LatLng(53, -7), + LatLng(52.25, -7), + LatLng(52.25, -8), + LatLng(52.75, -9), + ], + const [ + LatLng(52.683614, -8.141285), + LatLng(51.663083, -8.684529), + LatLng(51.913924, -7.2193), + ], + ], + borderStrokeWidth: 4, + borderColor: Colors.orange, + color: Colors.orange.withAlpha(128), + label: 'This one is not\nperformantly rendered', + rotateLabel: true, + labelPlacementCalculator: _labelPlacementCalculator, + labelStyle: const TextStyle(color: Colors.black), + hitValue: ( + title: 'Polygon With Hole', + subtitle: 'A bit like Swiss cheese maybe?', + ), + ), + Polygon( + points: const [ + LatLng(50, -18), + LatLng(53, -16), + LatLng(51.5, -12.5), + LatLng(54, -14), + LatLng(54, -18), + ] + .map( + (latlng) => LatLng(latlng.latitude - 6, latlng.longitude + 8)) + .toList(), + pattern: const StrokePattern.dotted(), + holePointsList: [ + const [ + LatLng(46, -9), + LatLng(46, -8), + LatLng(45.5, -7.5), + LatLng(45, -8), + LatLng(45, -9), + ].reversed.toList(growable: false), // Testing winding consitency + const [ + LatLng(47.5, -9), + LatLng(47.5, -8), + LatLng(47, -7), + LatLng(46.25, -7), + LatLng(46.25, -8), + LatLng(46.75, -9), + ].reversed.toList(growable: false), + const [ + LatLng(46.683614, -8.141285), + LatLng(45.663083, -8.684529), + LatLng(45.913924, -7.2193), + ].reversed.toList(growable: false), + ], + borderStrokeWidth: 4, + borderColor: Colors.orange, + color: Colors.orange.withAlpha(128), + label: 'This one is not\nperformantly rendered', + rotateLabel: true, + labelPlacementCalculator: _labelPlacementCalculator, + labelStyle: const TextStyle(color: Colors.black), + hitValue: ( + title: 'Polygon With Hole & Self Intersection', + subtitle: 'This one still works with performant rendering', + ), + ), + Polygon( + points: const [ + LatLng(61.861042, 0.946502), + LatLng(61.861458, 0.949468), + LatLng(61.861427, 0.949626), + LatLng(61.859015, 0.951513), + LatLng(61.858129, 0.952652) + ], + holePointsList: [], + color: Colors.lightGreen.withAlpha(128), + borderColor: Colors.lightGreen.withAlpha(128), + borderStrokeWidth: 10, + hitValue: ( + title: 'Testing opacity treatment (small)', + subtitle: + "Holes shouldn't be cut, and colors should be mixed correctly", + ), + ), + Polygon( + points: const [ + LatLng(61.861042, 0.946502), + LatLng(61.861458, 0.949468), + LatLng(61.861427, 0.949626), + LatLng(61.859015, 0.951513), + LatLng(61.858129, 0.952652), + LatLng(61.857633, 0.953214), + LatLng(61.855842, 0.954683), + LatLng(61.855769, 0.954692), + LatLng(61.855679, 0.954565), + LatLng(61.855417, 0.953926), + LatLng(61.855268, 0.953431), + LatLng(61.855173, 0.952443), + LatLng(61.855161, 0.951147), + LatLng(61.855222, 0.950822), + LatLng(61.855928, 0.948422), + LatLng(61.856365, 0.946638), + LatLng(61.856456, 0.946586), + LatLng(61.856787, 0.946656), + LatLng(61.857578, 0.946675), + LatLng(61.859338, 0.946453), + LatLng(61.861042, 0.946502) + ], + holePointsList: const [ + [ + LatLng(61.858881, 0.947234), + LatLng(61.858728, 0.947126), + LatLng(61.858562, 0.947132), + LatLng(61.858458, 0.947192), + LatLng(61.85844, 0.947716), + LatLng(61.858488, 0.947819), + LatLng(61.858766, 0.947818), + LatLng(61.858893, 0.947779), + LatLng(61.858975, 0.947542), + LatLng(61.858881, 0.947234) + ] + ], + color: Colors.lightGreen.withAlpha(128), + borderColor: Colors.lightGreen.withAlpha(128), + borderStrokeWidth: 10, + hitValue: ( + title: 'Testing opacity treatment (large)', + subtitle: + "Holes shouldn't be cut, and colors should be mixed correctly", + ), + ), + Polygon( + points: const [ + LatLng(40, 150), + LatLng(45, 160), + LatLng(50, 170), + LatLng(55, 180), + LatLng(50, -170), + LatLng(45, -160), + LatLng(40, -150), + LatLng(35, -160), + LatLng(30, -170), + LatLng(25, -180), + LatLng(30, 170), + LatLng(35, 160), + ], + holePointsList: const [ + [ + LatLng(45, 175), + LatLng(45, -175), + LatLng(35, -175), + LatLng(35, 175), + ], + ], + color: const Color(0xFFFF0000), + hitValue: ( + title: 'Big Red Diamond', + subtitle: 'Across the anti-meridian boundary', + ), + ), + ]; @override Widget build(BuildContext context) { @@ -311,7 +320,8 @@ class _PolygonPageState extends State { _prevHitValues = hitValues; final hoverLines = hitValues.map((v) { - final original = _polygons[v]!; + final original = + _polygonsRaw.singleWhere((p) => p.hitValue == v); return Polygon( points: original.points, @@ -441,70 +451,142 @@ class _PolygonPageState extends State { ), Positioned( top: 16, + left: 16, right: 16, - child: ClipRRect( - borderRadius: BorderRadius.circular(kIsWeb ? 16 : 32), - child: ColoredBox( - color: Theme.of(context).colorScheme.surface, - child: Column( - children: [ - Padding( - padding: const EdgeInsets.only( - left: 12, - right: 8, - top: 4, - bottom: 4, + child: Column( + spacing: 8, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(26), + ), + padding: + const EdgeInsets.symmetric(vertical: 4, horizontal: 16), + child: Wrap( + alignment: WrapAlignment.end, + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 8, + runSpacing: 8, + children: [ + const Tooltip( + message: 'Label Placement', + child: Icon(Icons.format_align_center), ), - child: Row( - mainAxisSize: MainAxisSize.max, - spacing: 8, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Tooltip( - message: 'Use Inverted Fill', - child: Icon(Icons.invert_colors), - ), - Switch.adaptive( - value: _useInvertedFill, - onChanged: (v) => - setState(() => _useInvertedFill = v), - ), - ], + ChoiceChip( + label: const Text('Simple Centroid'), + selected: _labelPlacementCalculator == + const PolygonLabelPlacementCalculator + .simpleCentroid(), + shape: const StadiumBorder(), + onSelected: (v) { + if (v) { + setState(() { + _labelPlacementCalculator = + const PolygonLabelPlacementCalculator + .simpleCentroid(); + _polygonsRaw = generatePolygons(); + }); + } + }, + ), + ChoiceChip( + label: const Text('Centroid'), + selected: _labelPlacementCalculator == + const PolygonLabelPlacementCalculator.centroid(), + shape: const StadiumBorder(), + onSelected: (v) { + if (v) { + setState(() { + _labelPlacementCalculator = + const PolygonLabelPlacementCalculator + .centroid(); + _polygonsRaw = generatePolygons(); + }); + } + }, ), + ChoiceChip( + label: const Text('Polylabel'), + selected: _labelPlacementCalculator == + const PolygonLabelPlacementCalculator.polylabel(), + shape: const StadiumBorder(), + onSelected: (v) { + if (v) { + setState(() { + _labelPlacementCalculator = + const PolygonLabelPlacementCalculator + .polylabel(); + _polygonsRaw = generatePolygons(); + }); + } + }, + ), + ], + ), + ), + FittedBox( + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(kIsWeb ? 16 : 32), + color: Theme.of(context).colorScheme.surface, ), - if (kIsWeb) - ColoredBox( - color: Colors.amber, - child: Padding( + child: Column( + children: [ + Padding( padding: const EdgeInsets.only( - left: 16, - right: 16, - top: 6, - bottom: 6, - ), + left: 12, right: 8, top: 4, bottom: 4), child: Row( - mainAxisSize: MainAxisSize.min, + mainAxisSize: MainAxisSize.max, spacing: 8, + mainAxisAlignment: MainAxisAlignment.end, children: [ - const Icon(Icons.warning), - const Icon(Icons.web_asset_off), - IconButton( - onPressed: () => launchUrl(Uri.parse( - 'https://docs.fleaflet.dev/layers/polygon-layer#inverted-filling', - )), - style: ButtonStyle( - backgroundColor: - WidgetStatePropertyAll(Colors.amber[100]), - ), - icon: const Icon(Icons.open_in_new), + const Tooltip( + message: 'Use Inverted Fill', + child: Icon(Icons.invert_colors), + ), + Switch.adaptive( + value: _useInvertedFill, + onChanged: (v) => + setState(() => _useInvertedFill = v), ), ], ), ), - ), - ], + if (kIsWeb) + Container( + decoration: BoxDecoration( + color: Colors.amber, + borderRadius: + BorderRadius.circular(kIsWeb ? 16 : 32), + ), + padding: const EdgeInsets.only( + left: 16, right: 16, top: 6, bottom: 6), + child: Row( + mainAxisSize: MainAxisSize.min, + spacing: 8, + children: [ + const Icon(Icons.warning), + const Icon(Icons.web_asset_off), + IconButton( + onPressed: () => launchUrl(Uri.parse( + 'https://docs.fleaflet.dev/layers/polygon-layer#inverted-filling', + )), + style: ButtonStyle( + backgroundColor: WidgetStatePropertyAll( + Colors.amber[100]), + ), + icon: const Icon(Icons.open_in_new), + ), + ], + ), + ), + ], + ), + ), ), - ), + ], ), ), ], diff --git a/example/lib/pages/polygon_perf_stress.dart b/example/lib/pages/polygon_perf_stress.dart index d2b0bd131..bbc90c2a4 100644 --- a/example/lib/pages/polygon_perf_stress.dart +++ b/example/lib/pages/polygon_perf_stress.dart @@ -129,9 +129,6 @@ class _PolygonPerfStressPageState extends State { ), ), ), - // Not ideal that we have to re-parse the GeoJson every - // time this is changed, but the library gives no easy - // way to change it after UnconstrainedBox( child: Container( decoration: BoxDecoration( diff --git a/example/lib/pages/polyline.dart b/example/lib/pages/polyline.dart index 94b38ba26..0c085bb95 100644 --- a/example/lib/pages/polyline.dart +++ b/example/lib/pages/polyline.dart @@ -173,68 +173,64 @@ class _PolylinePageState extends State { return Scaffold( appBar: AppBar(title: const Text('Polylines')), drawer: const MenuDrawer(PolylinePage.route), - body: Stack( + body: FlutterMap( + options: const MapOptions( + initialCenter: LatLng(51.5, -0.09), + initialZoom: 5, + ), children: [ - FlutterMap( - options: const MapOptions( - initialCenter: LatLng(51.5, -0.09), - initialZoom: 5, - ), - children: [ - openStreetMapTileLayer, - MouseRegion( - hitTestBehavior: HitTestBehavior.deferToChild, - cursor: SystemMouseCursors.click, - onHover: (_) { - final hitValues = _hitNotifier.value?.hitValues.toList(); - if (hitValues == null) return; + openStreetMapTileLayer, + MouseRegion( + hitTestBehavior: HitTestBehavior.deferToChild, + cursor: SystemMouseCursors.click, + onHover: (_) { + final hitValues = _hitNotifier.value?.hitValues.toList(); + if (hitValues == null) return; - if (listEquals(hitValues, _prevHitValues)) return; - _prevHitValues = hitValues; + if (listEquals(hitValues, _prevHitValues)) return; + _prevHitValues = hitValues; - final hoverLines = hitValues.map((v) { - final original = _polylines[v]!; + final hoverLines = hitValues.map((v) { + final original = _polylines[v]!; - return Polyline( - points: original.points, - strokeWidth: - original.strokeWidth + original.borderStrokeWidth, - color: Colors.transparent, - borderStrokeWidth: 15, - borderColor: Colors.green, - useStrokeWidthInMeter: original.useStrokeWidthInMeter, - ); - }).toList(); - setState(() => _hoverLines = hoverLines); - }, - onExit: (_) { - _prevHitValues = null; - setState(() => _hoverLines = null); - }, - child: GestureDetector( - onTap: () => _openTouchedLinesModal( - 'Tapped', - _hitNotifier.value!.hitValues, - _hitNotifier.value!.coordinate, - ), - onLongPress: () => _openTouchedLinesModal( - 'Long pressed', - _hitNotifier.value!.hitValues, - _hitNotifier.value!.coordinate, - ), - onSecondaryTap: () => _openTouchedLinesModal( - 'Secondary tapped', - _hitNotifier.value!.hitValues, - _hitNotifier.value!.coordinate, - ), - child: PolylineLayer( - hitNotifier: _hitNotifier, - simplificationTolerance: 0, - polylines: [..._polylinesRaw, ...?_hoverLines], - ), - ), + return Polyline( + points: original.points, + strokeWidth: + original.strokeWidth + original.borderStrokeWidth, + color: Colors.transparent, + borderStrokeWidth: 15, + borderColor: Colors.green, + useStrokeWidthInMeter: original.useStrokeWidthInMeter, + ); + }).toList(); + setState(() => _hoverLines = hoverLines); + }, + onExit: (_) { + _prevHitValues = null; + setState(() => _hoverLines = null); + }, + child: GestureDetector( + onTap: () => _openTouchedLinesModal( + 'Tapped', + _hitNotifier.value!.hitValues, + _hitNotifier.value!.coordinate, ), - ], + onLongPress: () => _openTouchedLinesModal( + 'Long pressed', + _hitNotifier.value!.hitValues, + _hitNotifier.value!.coordinate, + ), + onSecondaryTap: () => _openTouchedLinesModal( + 'Secondary tapped', + _hitNotifier.value!.hitValues, + _hitNotifier.value!.coordinate, + ), + child: PolylineLayer( + hitNotifier: _hitNotifier, + simplificationTolerance: 0, + polylines: [..._polylinesRaw, ...?_hoverLines], + ), + ), ), ], ), diff --git a/example/lib/pages/repeated_worlds.dart b/example/lib/pages/repeated_worlds.dart index ffd6fed90..ee74b74df 100644 --- a/example/lib/pages/repeated_worlds.dart +++ b/example/lib/pages/repeated_worlds.dart @@ -147,8 +147,9 @@ class _RepeatedWorldsPageState extends State { label: 'Aloha!', labelStyle: const TextStyle(color: Colors.green, fontSize: 40), - labelPlacement: - PolygonLabelPlacement.centroidWithMultiWorld, + labelPlacementCalculator: + const PolygonLabelPlacementCalculator + .simpleMultiWorldCentroid(), rotateLabel: false, points: const [ LatLng(40, 149), diff --git a/lib/flutter_map.dart b/lib/flutter_map.dart index acb68995f..91ca2397a 100644 --- a/lib/flutter_map.dart +++ b/lib/flutter_map.dart @@ -30,6 +30,8 @@ export 'package:flutter_map/src/layer/attribution_layer/simple.dart'; export 'package:flutter_map/src/layer/circle_layer/circle_layer.dart'; export 'package:flutter_map/src/layer/marker_layer/marker_layer.dart'; export 'package:flutter_map/src/layer/overlay_image_layer/overlay_image_layer.dart'; +export 'package:flutter_map/src/layer/polygon_layer/label/deprecated_placements.dart'; +export 'package:flutter_map/src/layer/polygon_layer/label/placement_calculators/placement_calculator.dart'; export 'package:flutter_map/src/layer/polygon_layer/polygon_layer.dart'; export 'package:flutter_map/src/layer/polyline_layer/polyline_layer.dart'; export 'package:flutter_map/src/layer/scalebar/scalebar.dart'; diff --git a/lib/src/layer/polygon_layer/label.dart b/lib/src/layer/polygon_layer/label.dart deleted file mode 100644 index e3de7d4fd..000000000 --- a/lib/src/layer/polygon_layer/label.dart +++ /dev/null @@ -1,150 +0,0 @@ -part of 'polygon_layer.dart'; - -void Function(Canvas canvas)? _buildLabelTextPainter({ - required Size mapSize, - required Offset placementPoint, - required ({Offset min, Offset max}) bounds, - required TextPainter textPainter, - required double rotationRad, - required bool rotate, - required double padding, -}) { - final dx = placementPoint.dx; - final dy = placementPoint.dy; - final width = textPainter.width; - final height = textPainter.height; - - // Cull labels where the polygon is still on the map but the label would not be. - // Currently this is only enabled when the map isn't rotated, since the placementOffset - // is relative to the MobileLayerTransformer rather than in actual screen coordinates. - final double textWidth; - final double textHeight; - final double mapWidth; - final double mapHeight; - if (rotationRad == 0) { - textWidth = width; - textHeight = height; - mapWidth = mapSize.width; - mapHeight = mapSize.height; - } else { - // lazily we imagine the worst case scenario regarding sizes, instead of - // computing the angles - textWidth = textHeight = max(width, height); - mapWidth = mapHeight = max(mapSize.width, mapSize.height); - } - if (dx + textWidth / 2 < 0 || dx - textWidth / 2 > mapWidth) { - return null; - } - if (dy + textHeight / 2 < 0 || dy - textHeight / 2 > mapHeight) { - return null; - } - - // Note: I'm pretty sure this doesn't work for concave shapes. It would be more - // correct to evaluate the width of the polygon at the height of the label. - if (bounds.max.dx - bounds.min.dx - padding > width) { - return (canvas) { - if (rotate) { - canvas.save(); - canvas.translate(dx, dy); - canvas.rotate(-rotationRad); - canvas.translate(-dx, -dy); - } - - textPainter.paint( - canvas, - Offset( - dx - width / 2, - dy - height / 2, - ), - ); - - if (rotate) { - canvas.restore(); - } - }; - } - return null; -} - -/// Calculate the [LatLng] position for the given [PolygonLabelPlacement]. -LatLng _computeLabelPosition( - PolygonLabelPlacement labelPlacement, - List points, -) { - return switch (labelPlacement) { - PolygonLabelPlacement.centroid => _computeCentroid(points), - PolygonLabelPlacement.centroidWithMultiWorld => - _computeCentroidWithMultiWorld(points), - PolygonLabelPlacement.polylabel => _computePolylabel(points), - }; -} - -/// Calculate the centroid of a given list of [LatLng] points. -LatLng _computeCentroid(List points) { - return LatLng( - points.map((e) => e.latitude).average, - points.map((e) => e.longitude).average, - ); -} - -/// Calculate the centroid of a given list of [LatLng] points with multiple worlds. -LatLng _computeCentroidWithMultiWorld(List points) { - if (points.isEmpty) return _computeCentroid(points); - const halfWorld = 180; - int count = 0; - double sum = 0; - late double lastLng; - for (final LatLng point in points) { - double lng = point.longitude; - count++; - if (count > 1) { - if (lng - lastLng > halfWorld) { - lng -= 2 * halfWorld; - } else if (lng - lastLng < -halfWorld) { - lng += 2 * halfWorld; - } - } - lastLng = lng; - sum += lastLng; - } - return LatLng(points.map((e) => e.latitude).average, sum / count); -} - -/// Use the Maxbox Polylabel algorithm to calculate the [LatLng] position for -/// a given list of points. -LatLng _computePolylabel(List points) { - final labelPosition = polylabel( - [ - List>.generate(points.length, - (i) => Point(points[i].longitude, points[i].latitude)), - ], - // "precision" is a bit of a misnomer. It's a threshold for when to stop - // dividing-and-conquering the polygon in the hopes of finding a better - // point with more distance to the polygon's outline. It's given in - // point-units, i.e. degrees here. A bigger number means less precision, - // i.e. cheaper at the expense off less optimal label placement. - // TODO: Make this an external option - precision: 0.0001, - ); - return LatLng( - labelPosition.point.y.toDouble(), - labelPosition.point.x.toDouble(), - ); -} - -/// Defines the algorithm used to calculate the position of the [Polygon] label. -/// -/// > [!IMPORTANT] -/// > If your project allows users to browse across multiple worlds, and your -/// > polygons may be over the anti-meridan boundary, [centroidWithMultiWorld] -/// > must be used - other algorithms will produce unexpected results. -enum PolygonLabelPlacement { - /// Use the centroid of the [Polygon] outline as position for the label. - centroid, - - /// Use the centroid in a multi-world as position for the label. - centroidWithMultiWorld, - - /// Use the Mapbox Polylabel algorithm as position for the label. - polylabel, -} diff --git a/lib/src/layer/polygon_layer/label/build_text_painter.dart b/lib/src/layer/polygon_layer/label/build_text_painter.dart new file mode 100644 index 000000000..82dfb31e4 --- /dev/null +++ b/lib/src/layer/polygon_layer/label/build_text_painter.dart @@ -0,0 +1,67 @@ +part of '../polygon_layer.dart'; + +void Function(Canvas canvas)? _buildLabelTextPainter({ + required Size mapSize, + required Offset placementPoint, + required ({Offset min, Offset max}) bounds, + required TextPainter textPainter, + required double rotationRad, + required bool rotate, + required double padding, +}) { + final dx = placementPoint.dx; + final dy = placementPoint.dy; + final width = textPainter.width; + final height = textPainter.height; + + // Cull labels where the polygon is still on the map but the label would not be. + // Currently this is only enabled when the map isn't rotated, since the placementOffset + // is relative to the MobileLayerTransformer rather than in actual screen coordinates. + final double textWidth; + final double textHeight; + final double mapWidth; + final double mapHeight; + if (rotationRad == 0) { + textWidth = width; + textHeight = height; + mapWidth = mapSize.width; + mapHeight = mapSize.height; + } else { + // lazily we imagine the worst case scenario regarding sizes, instead of + // computing the angles + textWidth = textHeight = max(width, height); + mapWidth = mapHeight = max(mapSize.width, mapSize.height); + } + if (dx + textWidth / 2 < 0 || dx - textWidth / 2 > mapWidth) { + return null; + } + if (dy + textHeight / 2 < 0 || dy - textHeight / 2 > mapHeight) { + return null; + } + + // Note: I'm pretty sure this doesn't work for concave shapes. It would be more + // correct to evaluate the width of the polygon at the height of the label. + if (bounds.max.dx - bounds.min.dx - padding > width) { + return (canvas) { + if (rotate) { + canvas.save(); + canvas.translate(dx, dy); + canvas.rotate(-rotationRad); + canvas.translate(-dx, -dy); + } + + textPainter.paint( + canvas, + Offset( + dx - width / 2, + dy - height / 2, + ), + ); + + if (rotate) { + canvas.restore(); + } + }; + } + return null; +} diff --git a/lib/src/layer/polygon_layer/label/deprecated_placements.dart b/lib/src/layer/polygon_layer/label/deprecated_placements.dart new file mode 100644 index 000000000..1214f7eed --- /dev/null +++ b/lib/src/layer/polygon_layer/label/deprecated_placements.dart @@ -0,0 +1,48 @@ +import 'package:flutter_map/flutter_map.dart'; + +/// Defines the algorithm used to calculate the position of the [Polygon] label. +/// +/// > [!IMPORTANT] +/// > If polygons may be over the anti-meridan boundary, +/// > [PolygonLabelPlacementCalculator.simpleMultiWorldCentroid] must be used - +/// > other calculators will produce unexpected results. +@Deprecated( + 'Use `Polygon.labelPlacementCalculator` with the equivalent calculator ' + 'instead. ' + 'This enables more flexibility and extensibility. ' + 'This was deprecated after v8.2.0, and will be removed in a future version.', +) +enum PolygonLabelPlacement { + /// Alias for [PolygonLabelPlacementCalculator.centroid] + /// + /// The new [PolygonLabelPlacementCalculator.centroid] algorithm has differing + /// behaviour than the old one. To remain using the existing behaviour, use + /// [PolygonLabelPlacementCalculator.simpleCentroid]. + @Deprecated( + 'Use `Polygon.labelPlacementCalculator` with ' + '`const PolygonLabelPlacementCalculator.simpleCentroid()` or `.centroid()` ' + 'instead. ' + 'This enables more flexibility and extensibility. ' + 'This was deprecated after v8.2.0, and will be removed in a future version.', + ) + centroid, + + /// Alias for [PolygonLabelPlacementCalculator.simpleMultiWorldCentroid] + @Deprecated( + 'Use `Polygon.labelPlacementCalculator` with ' + '`const PolygonLabelPlacementCalculator.simpleMultiWorldCentroid()` ' + 'instead. ' + 'This enables more flexibility and extensibility. ' + 'This was deprecated after v8.2.0, and will be removed in a future version.', + ) + centroidWithMultiWorld, + + /// Alias for [PolygonLabelPlacementCalculator.polylabel] + @Deprecated( + 'Use `Polygon.labelPlacementCalculator` with ' + '`const PolygonLabelPlacementCalculator.polylabel()` instead. ' + 'This enables more flexibility and extensibility. ' + 'This was deprecated after v8.2.0, and will be removed in a future version.', + ) + polylabel, +} diff --git a/lib/src/layer/polygon_layer/label/placement_calculators/centroid.dart b/lib/src/layer/polygon_layer/label/placement_calculators/centroid.dart new file mode 100644 index 000000000..de25f497a --- /dev/null +++ b/lib/src/layer/polygon_layer/label/placement_calculators/centroid.dart @@ -0,0 +1,75 @@ +part of 'placement_calculator.dart'; + +/// {@template fm.polygonLabelPlacementCalculator.centroid} +/// Places the [Polygon.label] at the centroid calculated using the +/// [signed area formula](https://en.wikipedia.org/wiki/Centroid#Of_a_polygon) +/// +/// This a little more computationally expensive than the simple centroid +/// calculator, but yields better results, especially for non-convex polygons. +/// {@endtemplate} +class CentroidCalculator implements PolygonLabelPlacementCalculator { + @literal // Required for equality purposes + const CentroidCalculator._(); + + @override + LatLng call(Polygon polygon) { + final points = polygon.points; + + if (points.isEmpty) { + throw ArgumentError('Polygon must contain at least one point'); + } + + if (points.length == 1) return points[0]; + + double signedArea = 0; + double centroidX = 0; + double centroidY = 0; + + // For all vertices except last + for (int i = 0; i < points.length - 1; i++) { + final double x0 = points[i].longitude; + final double y0 = points[i].latitude; + final double x1 = points[i + 1].longitude; + final double y1 = points[i + 1].latitude; + + // Calculate signed area contribution of current vertex + final double a = x0 * y1 - x1 * y0; + signedArea += a; + + // Accumulate centroid components weighted by signed area + centroidX += (x0 + x1) * a; + centroidY += (y0 + y1) * a; + } + + // Close the polygon by connecting last vertex to first + final double x0 = points.last.longitude; + final double y0 = points.last.latitude; + final double x1 = points.first.longitude; + final double y1 = points.first.latitude; + final double a = x0 * y1 - x1 * y0; + signedArea += a; + centroidX += (x0 + x1) * a; + centroidY += (y0 + y1) * a; + + // Complete the signed area calculation + signedArea *= 0.5; + + // Calculate centroid coordinates + centroidX /= 6 * signedArea; + centroidY /= 6 * signedArea; + + // Handle special case of zero area (collinear points) + if (signedArea == 0) { + // Default to average of all points + double sumX = 0; + double sumY = 0; + for (final point in points) { + sumX += point.longitude; + sumY += point.latitude; + } + return LatLng(sumY / points.length, sumX / points.length); + } + + return LatLng(centroidY, centroidX); + } +} diff --git a/lib/src/layer/polygon_layer/label/placement_calculators/placement_calculator.dart b/lib/src/layer/polygon_layer/label/placement_calculators/placement_calculator.dart new file mode 100644 index 000000000..39ca3033f --- /dev/null +++ b/lib/src/layer/polygon_layer/label/placement_calculators/placement_calculator.dart @@ -0,0 +1,53 @@ +import 'package:collection/collection.dart'; +import 'package:dart_polylabel2/dart_polylabel2.dart' as dart_polylabel2; +import 'package:flutter_map/flutter_map.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:meta/meta.dart'; + +part 'centroid.dart'; +part 'polylabel.dart'; +part 'simple_centroid.dart'; + +/// Calculates the position of a [Polygon.label] within its [Polygon] in +/// geographic (lat/lng) space +/// +/// > [!IMPORTANT] +/// > If the polygon may be over the anti-meridan boundary, +/// > [PolygonLabelPlacementCalculator.simpleMultiWorldCentroid] must be used - +/// > other calculators will produce unexpected results. +/// +/// --- +/// +/// Implementers: if the constructor is zero-argument, and there is no equality +/// implementation, constructors should always be invoked with the `const` +/// keyword, which may be enforced with the [literal] annotation. +@immutable +abstract interface class PolygonLabelPlacementCalculator { + /// {@macro fm.polygonLabelPlacementCalculator.simpleCentroid} + @literal + const factory PolygonLabelPlacementCalculator.simpleCentroid() = + SimpleCentroidCalculator._; + + /// Similar to [PolygonLabelPlacementCalculator.simpleCentroid], but supports + /// correct placement of the [Polygon.label] when the polygon lies across the + /// anti-meridian + @literal + const factory PolygonLabelPlacementCalculator.simpleMultiWorldCentroid() = + SimpleMultiWorldCentroidCalculator._; + + /// {@macro fm.polygonLabelPlacementCalculator.centroid} + @literal + const factory PolygonLabelPlacementCalculator.centroid() = + CentroidCalculator._; + + /// {@macro fm.polygonLabelPlacementCalculator.polylabel} + const factory PolygonLabelPlacementCalculator.polylabel({double precision}) = + PolylabelCalculator._; + + /// Given a polygon (and its points), calculate a single position at which + /// the center of the label should be placed + /// + /// [Polygon.points] is not guaranteed to be non-empty. If empty, this may + /// throw. + LatLng call(Polygon polygon); +} diff --git a/lib/src/layer/polygon_layer/label/placement_calculators/polylabel.dart b/lib/src/layer/polygon_layer/label/placement_calculators/polylabel.dart new file mode 100644 index 000000000..9df2dde91 --- /dev/null +++ b/lib/src/layer/polygon_layer/label/placement_calculators/polylabel.dart @@ -0,0 +1,58 @@ +part of 'placement_calculator.dart'; + +/// {@template fm.polygonLabelPlacementCalculator.polylabel} +/// Places the [Polygon.label] at the point furthest away from the outline, +/// calculated using a Dart implementation of +/// [Mapbox's 'polylabel' algorithm](https://github.com/JaffaKetchup/dart_polylabel2) +/// +/// This is more computationally expensive than other calculators but may yield +/// better results. +/// +/// The [precision] may be adjusted to change the computational expense and +/// result accuracy. See documentation on [precision] for more information. +/// {@endtemplate} +class PolylabelCalculator implements PolygonLabelPlacementCalculator { + // Does not need to be `@literal`, as equality is implemented based on state + const PolylabelCalculator._({ + this.precision = 0.0001, + }); + + /// Threshold for when to stop dividing-and-conquering the polygon in the + /// hopes of finding a better point with more distance to the polygon's + /// outline + /// + /// A higher number means less precision (less optimal placement), which is + /// computationally cheaper (requires fewer iterations). + /// + /// Specifying a number too small may result in program hangs. + /// + /// Specified in geographical space, i.e. degrees. Therefore, as the polygon + /// gets larger, this should also get larger. + /// + /// Defaults to 0.0001. + final double precision; + + @override + LatLng call(Polygon polygon) { + final (point: (:x, :y), distance: _) = dart_polylabel2.polylabel( + [ + List.generate( + polygon.points.length, + (i) => + (x: polygon.points[i].latitude, y: polygon.points[i].longitude), + growable: false, + ), + ], + precision: precision, + ); + return LatLng(x, y); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PolylabelCalculator && precision == other.precision); + + @override + int get hashCode => precision.hashCode; +} diff --git a/lib/src/layer/polygon_layer/label/placement_calculators/simple_centroid.dart b/lib/src/layer/polygon_layer/label/placement_calculators/simple_centroid.dart new file mode 100644 index 000000000..3a35bb4a3 --- /dev/null +++ b/lib/src/layer/polygon_layer/label/placement_calculators/simple_centroid.dart @@ -0,0 +1,59 @@ +part of 'placement_calculator.dart'; + +/// {@template fm.polygonLabelPlacementCalculator.simpleCentroid} +/// Places the [Polygon.label] at the approximate centroid calculated by +/// averaging all the points of the polygon +/// +/// This is computationally cheap and gives reasonable results for convex +/// polygons. However, for more complex or convex polygons, results may not be +/// as good (but they should still be acceptable). +/// +/// > [!IMPORTANT] +/// > If the polygon may be over the anti-meridan boundary, +/// > [SimpleMultiWorldCentroidCalculator] must be used - other +/// > calculators will produce unexpected results. +/// {@endtemplate} +class SimpleCentroidCalculator implements PolygonLabelPlacementCalculator { + @literal // Required for equality purposes + const SimpleCentroidCalculator._(); + + @override + LatLng call(Polygon polygon) => LatLng( + polygon.points.map((e) => e.latitude).average, + polygon.points.map((e) => e.longitude).average, + ); +} + +/// Similar to [SimpleCentroidCalculator], but supports correct placement of the +/// [Polygon.label] when the polygon lies across the anti-meridian +class SimpleMultiWorldCentroidCalculator + implements PolygonLabelPlacementCalculator { + @literal // Required for equality purposes + const SimpleMultiWorldCentroidCalculator._(); + + @override + LatLng call(Polygon polygon) { + if (polygon.points.isEmpty) { + throw ArgumentError('Polygon must contain at least one point'); + } + + const halfWorld = 180; + int count = 0; + double sum = 0; + late double lastLng; + for (final LatLng point in polygon.points) { + double lng = point.longitude; + count++; + if (count > 1) { + if (lng - lastLng > halfWorld) { + lng -= 2 * halfWorld; + } else if (lng - lastLng < -halfWorld) { + lng += 2 * halfWorld; + } + } + lastLng = lng; + sum += lastLng; + } + return LatLng(polygon.points.map((e) => e.latitude).average, sum / count); + } +} diff --git a/lib/src/layer/polygon_layer/polygon.dart b/lib/src/layer/polygon_layer/polygon.dart index ecedd7ac4..1f882e972 100644 --- a/lib/src/layer/polygon_layer/polygon.dart +++ b/lib/src/layer/polygon_layer/polygon.dart @@ -54,18 +54,37 @@ class Polygon with HitDetectableElement { /// The placement logic of the [Polygon.label] /// - /// [PolygonLabelPlacement.polylabel] can be expensive for some polygons. If - /// there is a large lag spike, try using [PolygonLabelPlacement.centroid]. - /// /// > [!IMPORTANT] - /// > If your project allows users to browse across multiple worlds, and your - /// > polygons may be over the anti-meridan boundary, - /// > [PolygonLabelPlacement.centroidWithMultiWorld] must be used - other - /// > algorithms will produce unexpected results. + /// > If polygons may be over the anti-meridan boundary, + /// > [SimpleMultiWorldCentroidCalculator] must be used - other + /// > calculators will produce unexpected results. /// - /// Labels will not be drawn if there is not enough space. + /// See [labelPlacementCalculator] for more information. + @Deprecated( + 'Use `labelPlacementCalculator` with the equivalent calculator instead. ' + 'Then, remove any arguments to this parameter and allow it to default. ' + 'This enables more flexibility and extensibility. ' + 'This was deprecated after v8.2.0, and will be removed in a future version.', + ) final PolygonLabelPlacement labelPlacement; + /// The calculator to use to determine the position of the [Polygon.label] + /// + /// Labels are not drawn if there is not enough space. + /// + /// > [!IMPORTANT] + /// > If polygons may be over the anti-meridan boundary, + /// > [PolygonLabelPlacementCalculator.simpleMultiWorldCentroid] must be + /// > used - other calculators will produce unexpected results. + /// + /// Pre-provided calculators are available as constructors on + /// [PolygonLabelPlacementCalculator]. See the documentation on each for + /// advantages & disadvantages of each implementation. + /// + /// Defaults to [PolygonLabelPlacementCalculator.centroid] + /// ([CentroidCalculator]). + final PolygonLabelPlacementCalculator labelPlacementCalculator; + /// Whether to rotate the label counter to the camera's rotation, to ensure /// it remains upright final bool rotateLabel; @@ -84,8 +103,7 @@ class Polygon with HitDetectableElement { LatLng? _labelPosition; /// Get the coordinates of the label position (cached). - LatLng get labelPosition => - _labelPosition ??= _computeLabelPosition(labelPlacement, points); + LatLng get labelPosition => _labelPosition ??= labelPlacementCalculator(this); LatLngBounds? _boundingBox; @@ -122,10 +140,35 @@ class Polygon with HitDetectableElement { this.strokeJoin = StrokeJoin.round, this.label, this.labelStyle = const TextStyle(), + // TODO: Remove `labelPlacement`, and make `labelPlacementCalculator` + // `this.` with default, then remove initialiser list + @Deprecated( + 'Use `labelPlacementCalculator` with the equivalent calculator instead. ' + 'Then, remove any arguments to this parameter and allow it to default. ' + 'This enables more flexibility and extensibility. ' + 'This was deprecated after v8.2.0, and will be removed in a future ' + 'version.', + ) this.labelPlacement = PolygonLabelPlacement.centroid, + + /// See [labelPlacementCalculator] + PolygonLabelPlacementCalculator? labelPlacementCalculator, this.rotateLabel = false, this.hitValue, - }) : _filledAndClockwise = color != null && isClockwise(points); + }) : _filledAndClockwise = color != null && isClockwise(points), + labelPlacementCalculator = labelPlacementCalculator ?? + switch (labelPlacement) { + // ignore: deprecated_member_use_from_same_package + PolygonLabelPlacement.centroid => + const PolygonLabelPlacementCalculator.centroid(), + // ignore: deprecated_member_use_from_same_package + PolygonLabelPlacement.centroidWithMultiWorld => + const PolygonLabelPlacementCalculator + .simpleMultiWorldCentroid(), + // ignore: deprecated_member_use_from_same_package + PolygonLabelPlacement.polylabel => + const PolygonLabelPlacementCalculator.polylabel(), + }; /// Checks if the [Polygon] points are ordered clockwise in the list. static bool isClockwise(List points) { @@ -152,7 +195,7 @@ class Polygon with HitDetectableElement { strokeJoin == other.strokeJoin && label == other.label && labelStyle == other.labelStyle && - labelPlacement == other.labelPlacement && + labelPlacementCalculator == other.labelPlacementCalculator && rotateLabel == other.rotateLabel && hitValue == other.hitValue && // Expensive computations last to take advantage of lazy logic gates @@ -183,7 +226,7 @@ class Polygon with HitDetectableElement { ...points, label, labelStyle, - labelPlacement, + labelPlacementCalculator, rotateLabel, renderHashCode, ]); diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index 4977ccd6c..b6149a65d 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -1,7 +1,6 @@ import 'dart:math'; import 'dart:ui'; -import 'package:collection/collection.dart'; import 'package:dart_earcut/dart_earcut.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; @@ -16,9 +15,8 @@ import 'package:flutter_map/src/misc/point_in_polygon.dart'; import 'package:flutter_map/src/misc/simplify.dart'; import 'package:latlong2/latlong.dart' hide Path; import 'package:logger/logger.dart'; -import 'package:polylabel/polylabel.dart'; -part 'label.dart'; +part 'label/build_text_painter.dart'; part 'painter.dart'; part 'polygon.dart'; part 'projected_polygon.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 3badb5085..ac66310f6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,13 +29,13 @@ dependencies: async: ^2.11.0 collection: ^1.18.0 dart_earcut: ^1.1.0 + dart_polylabel2: ^1.0.0 flutter: sdk: flutter http: ^1.2.1 latlong2: ^0.9.1 logger: ^2.0.0 meta: ^1.11.0 - polylabel: ^1.0.1 proj4dart: ^2.1.0 vector_math: ^2.1.4