From 4ca0bcc4cfa507f71d81a6494152aaed998ad689 Mon Sep 17 00:00:00 2001 From: monsieurtanuki Date: Wed, 26 Feb 2025 11:14:21 +0100 Subject: [PATCH 01/18] feat: 2034 - "just holes" polygon feature New file: * `advanced_polygons.dart`: Example dedicated to polygons with advanced features. Impacted files: * `main.dart`: added the new `AdvancedPolygonsPage` page * `menu_drawer.dart`: added the new `AdvancedPolygonsPage` page * `painter.dart`: managed the `justHoles` case; refactored a bit `borderPaint` * `polygon.dart`: added the `bool justHoles` field * `polygon_layer.dart`: special case for `justHoles` * `projected_polygon.dart`: special case for `justHoles` --- example/lib/main.dart | 2 + example/lib/pages/advanced_polygons.dart | 125 ++++++++++ example/lib/widgets/drawer/menu_drawer.dart | 6 + lib/src/layer/polygon_layer/painter.dart | 216 ++++++++++++------ lib/src/layer/polygon_layer/polygon.dart | 4 + .../layer/polygon_layer/polygon_layer.dart | 7 +- .../polygon_layer/projected_polygon.dart | 4 +- 7 files changed, 285 insertions(+), 79 deletions(-) create mode 100644 example/lib/pages/advanced_polygons.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index 4eb4d17f2..39ab00ae0 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_map_example/pages/advanced_polygons.dart'; import 'package:flutter_map_example/pages/animated_map_controller.dart'; import 'package:flutter_map_example/pages/bundled_offline_map.dart'; import 'package:flutter_map_example/pages/cancellable_tile_provider.dart'; @@ -68,6 +69,7 @@ class MyApp extends StatelessWidget { OverlayImagePage.route: (context) => const OverlayImagePage(), PolygonPage.route: (context) => const PolygonPage(), MultiWorldsPage.route: (context) => const MultiWorldsPage(), + AdvancedPolygonsPage.route: (context) => const AdvancedPolygonsPage(), PolygonPerfStressPage.route: (context) => const PolygonPerfStressPage(), SlidingMapPage.route: (_) => const SlidingMapPage(), WMSLayerPage.route: (context) => const WMSLayerPage(), diff --git a/example/lib/pages/advanced_polygons.dart b/example/lib/pages/advanced_polygons.dart new file mode 100644 index 000000000..791c13f6e --- /dev/null +++ b/example/lib/pages/advanced_polygons.dart @@ -0,0 +1,125 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map_example/misc/tile_providers.dart'; +import 'package:flutter_map_example/widgets/drawer/menu_drawer.dart'; +import 'package:latlong2/latlong.dart'; + +/// Example dedicated to polygons with advanced features. +class AdvancedPolygonsPage extends StatefulWidget { + static const String route = '/advanced_polygons'; + + const AdvancedPolygonsPage({super.key}); + + @override + State createState() => _AdvancedPolygonsPageState(); +} + +class _AdvancedPolygonsPageState extends State { + final LayerHitNotifier _hitNotifier = ValueNotifier(null); + + final _customMarkers = []; + + Marker _buildPin(LatLng point) => Marker( + point: point, + width: 60, + height: 60, + child: GestureDetector( + onTap: () => ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Tapped existing marker'), + duration: Duration(seconds: 1), + showCloseIcon: true, + ), + ), + child: const Icon(Icons.location_pin, color: Colors.red), + ), + ); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Advanced polygons')), + drawer: const MenuDrawer(AdvancedPolygonsPage.route), + body: Stack( + children: [ + FlutterMap( + options: MapOptions( + initialCenter: const LatLng(45.5, 2), + initialZoom: 0, + initialRotation: 0, + onTap: (_, p) => setState(() => _customMarkers.add(_buildPin(p))), + ), + children: [ + openStreetMapTileLayer, + GestureDetector( + onTap: () => ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(_hitNotifier.value!.hitValues.join(', ')), + duration: const Duration(seconds: 1), + showCloseIcon: true, + ), + ), + child: PolygonLayer( + hitNotifier: _hitNotifier, + simplificationTolerance: 0, + useAltRendering: false, + drawLabelsLast: false, + polygons: [ + Polygon( + rotateLabel: false, + borderColor: Colors.blue, + borderStrokeWidth: 3, + justHoles: true, + points: const [], + holePointsList: const [ + [ + // France + // Calais 50° 56′ 53″ nord, 1° 51′ 23″ est + LatLng(50.948056, 1.856389), + // Brest 48° 23′ 27″ nord, 4° 29′ 08″ ouest + LatLng(48.390833, -4.485556), + // Biarritz 43° 28′ 54″ nord, 1° 33′ 22″ ouest + LatLng(43.481667, -1.556111), + // Perpignan 42° 41′ 55″ nord, 2° 53′ 44″ est + LatLng(42.698611, 2.895556), + // Menton 43° 46′ 33″ nord, 7° 30′ 10″ est + LatLng(43.775833, 7.502778), + // Strasbourg 48° 34′ 24″ nord, 7° 45′ 08″ est + LatLng(48.573333, 7.752222), + ], + [ + // Corsica + // Bonifacio 41° 23′ nord, 9° 09′ est + LatLng(41.383333, 9.15), + // Ajaccio 41° 55′ 36″ nord, 8° 44′ 13″ est + LatLng(41.926667, 8.736944), + // Calvi 42° 34′ 07″ nord, 8° 45′ 25″ est + LatLng(42.568611, 8.756944), + // Bastia 42° 42′ 03″ nord, 9° 27′ 01″ est + LatLng(42.700833, 9.450278), + ], + [ + // South America + // Ushuaia 54° 48′ 35″ sud, 68° 18′ 50″ ouest + LatLng(-54.809722, -68.313889), + // Fortaleza 3° 43′ 01″ sud, 38° 32′ 34″ ouest + LatLng(-3.716944, -38.542778), + // Panama 8° 58′ nord, 79° 32′ ouest + LatLng(8.966667, -79.533333), + // Quito 0° 14′ 18″ sud, 78° 31′ 02″ ouest + LatLng(-0.238333, -78.517222), + ], + ], + color: const Color(0x80FF0000), + hitValue: 'South America or France', + ), + ], + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/example/lib/widgets/drawer/menu_drawer.dart b/example/lib/widgets/drawer/menu_drawer.dart index 5d130c072..4756616d0 100644 --- a/example/lib/widgets/drawer/menu_drawer.dart +++ b/example/lib/widgets/drawer/menu_drawer.dart @@ -1,5 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_map_example/pages/advanced_polygons.dart'; import 'package:flutter_map_example/pages/animated_map_controller.dart'; import 'package:flutter_map_example/pages/bundled_offline_map.dart'; import 'package:flutter_map_example/pages/cancellable_tile_provider.dart'; @@ -115,6 +116,11 @@ class MenuDrawer extends StatelessWidget { routeName: MultiWorldsPage.route, currentRoute: currentRoute, ), + MenuItemWidget( + caption: 'Advanced polygons', + routeName: AdvancedPolygonsPage.route, + currentRoute: currentRoute, + ), const Divider(), MenuItemWidget( caption: 'Map Controller', diff --git a/lib/src/layer/polygon_layer/painter.dart b/lib/src/layer/polygon_layer/painter.dart index bbfbea9a0..626baea28 100644 --- a/lib/src/layer/polygon_layer/painter.dart +++ b/lib/src/layer/polygon_layer/painter.dart @@ -66,6 +66,41 @@ class _PolygonPainter extends CustomPainter // } WorldWorkControl checkIfHit(double shift) { + bool isInHole = false; + bool isHoleVisible = false; + for (final points in projectedPolygon.holePoints) { + final projectedHoleCoords = getOffsetsXY( + camera: camera, + origin: origin, + points: points, + shift: shift, + ); + if (projectedHoleCoords.first != projectedHoleCoords.last) { + projectedHoleCoords.add(projectedHoleCoords.first); + } + final isValidHolePolygon = projectedHoleCoords.length >= 3; + if (!isValidHolePolygon) continue; + + if (isPointInPolygon(point, projectedHoleCoords)) { + if (projectedPolygon.polygon.justHoles) { + return WorldWorkControl.hit; + } + isInHole = true; + break; + } + if (!isHoleVisible) { + if (areOffsetsVisible(projectedHoleCoords)) { + isHoleVisible = true; + } + } + } + + if (projectedPolygon.polygon.justHoles) { + return isHoleVisible + ? WorldWorkControl.visible + : WorldWorkControl.invisible; + } + final projectedCoords = getOffsetsXY( camera: camera, origin: origin, @@ -84,24 +119,6 @@ class _PolygonPainter extends CustomPainter final isInPolygon = isValidPolygon && isPointInPolygon(point, projectedCoords); - final isInHole = projectedPolygon.holePoints.any( - (points) { - final projectedHoleCoords = getOffsetsXY( - camera: camera, - origin: origin, - points: points, - shift: shift, - ); - if (projectedHoleCoords.first != projectedHoleCoords.last) { - projectedHoleCoords.add(projectedHoleCoords.first); - } - - final isValidHolePolygon = projectedHoleCoords.length >= 3; - return isValidHolePolygon && - isPointInPolygon(point, projectedHoleCoords); - }, - ); - // Second check handles case where polygon outline intersects a hole, // ensuring that the hit matches with the visual representation return (isInPolygon && !isInHole) || (!isInPolygon && isInHole) @@ -115,6 +132,8 @@ class _PolygonPainter extends CustomPainter @override Iterable<_ProjectedPolygon> get elements => polygons; + static const _minMaxLatitude = [LatLng(90, 0), LatLng(-90, 0)]; + @override void paint(Canvas canvas, Size size) { const checkOpacity = true; // for debugging purposes only, should be true @@ -126,6 +145,7 @@ class _PolygonPainter extends CustomPainter final borderPath = Path(); Polygon? lastPolygon; int? lastHash; + Paint? borderPaint; // This functions flushes the batched fill and border paths constructed below void drawPaths() { @@ -138,7 +158,7 @@ class _PolygonPainter extends CustomPainter ..style = PaintingStyle.fill ..color = color; - if (trianglePoints.isNotEmpty) { + if (trianglePoints.isNotEmpty && !polygon.justHoles) { final points = Float32List(trianglePoints.length * 2); for (int i = 0; i < trianglePoints.length; ++i) { points[i * 2] = trianglePoints[i].dx; @@ -196,8 +216,8 @@ class _PolygonPainter extends CustomPainter } // Draw polygon outline - if (polygon.borderStrokeWidth > 0) { - canvas.drawPath(borderPath, _getBorderPaint(polygon)); + if (borderPaint != null) { + canvas.drawPath(borderPath, borderPaint); } trianglePoints.clear(); @@ -241,22 +261,30 @@ class _PolygonPainter extends CustomPainter // Main loop constructing batched fill and border paths from given polygons. for (int i = 0; i <= polygons.length - 1; i++) { final projectedPolygon = polygons[i]; - if (projectedPolygon.points.isEmpty) continue; final polygon = projectedPolygon.polygon; + if (projectedPolygon.points.isEmpty && !polygon.justHoles) continue; + borderPaint = _getBorderPaint(polygon); final polygonTriangles = triangles?[i]; /// Draws on a "single-world" WorldWorkControl drawIfVisible(double shift) { - final fillOffsets = getOffsetsXY( - camera: camera, - origin: origin, - points: projectedPolygon.points, - holePoints: - polygonTriangles != null ? projectedPolygon.holePoints : null, - shift: shift, - ); - if (!areOffsetsVisible(fillOffsets)) return WorldWorkControl.invisible; + final fillOffsets = polygon.justHoles + ? null + : getOffsetsXY( + camera: camera, + origin: origin, + points: projectedPolygon.points, + holePoints: polygonTriangles != null + ? projectedPolygon.holePoints + : null, + shift: shift, + ); + if (fillOffsets != null) { + if (!areOffsetsVisible(fillOffsets)) { + return WorldWorkControl.invisible; + } + } if (debugAltRenderer) { const offsetsLabelStyle = TextStyle( @@ -264,16 +292,18 @@ class _PolygonPainter extends CustomPainter fontSize: 16, ); - for (int i = 0; i < fillOffsets.length; i++) { - TextPainter( - text: TextSpan( - text: i.toString(), - style: offsetsLabelStyle, - ), - textDirection: TextDirection.ltr, - ) - ..layout(maxWidth: 100) - ..paint(canvas, fillOffsets[i]); + if (fillOffsets != null) { + for (int i = 0; i < fillOffsets.length; i++) { + TextPainter( + text: TextSpan( + text: i.toString(), + style: offsetsLabelStyle, + ), + textDirection: TextDirection.ltr, + ) + ..layout(maxWidth: 100) + ..paint(canvas, fillOffsets[i]); + } } } @@ -286,38 +316,66 @@ class _PolygonPainter extends CustomPainter final hash = polygon.renderHashCode; final opacity = polygon.color?.a ?? 0; if (lastHash != hash || (checkOpacity && opacity > 0 && opacity < 1)) { - drawPaths(); + // we need all holes to be connected with the same full map. + if (!polygon.justHoles) { + drawPaths(); + } } lastPolygon = polygon; lastHash = hash; // First add fills and borders to path. if (polygon.color != null) { - if (polygonTriangles != null) { - final len = polygonTriangles.length; - for (int i = 0; i < len; ++i) { - trianglePoints.add(fillOffsets[polygonTriangles[i]]); + if (fillOffsets != null) { + if (polygonTriangles != null) { + final len = polygonTriangles.length; + for (int i = 0; i < len; ++i) { + trianglePoints.add(fillOffsets[polygonTriangles[i]]); + } + } else { + filledPath.addPolygon(fillOffsets, true); } - } else { - filledPath.addPolygon(fillOffsets, true); + } else if (shift == 0) { + // we display once the full map. + final minMaxProjected = + camera.crs.projection.projectList(_minMaxLatitude); + final minMaxY = getOffsetsXY( + camera: camera, + origin: origin, + points: minMaxProjected, + shift: shift, + ); + final maxX = viewportRect.right; + final minX = viewportRect.left; + final maxY = minMaxY[0].dy; + final minY = minMaxY[1].dy; + final rect = Rect.fromLTRB(minX, minY, maxX, maxY); + filledPath.addRect(rect); } } + void addBorderToPath(List offsets) => _addBorderToPath( + borderPath, + polygon, + offsets, + size, + canvas, + borderPaint!, + ); + if (polygon.borderStrokeWidth > 0.0) { - _addBorderToPath( - borderPath, - polygon, - getOffsetsXY( - camera: camera, - origin: origin, - points: projectedPolygon.points, - shift: shift, - ), - size, - canvas, - _getBorderPaint(polygon), - polygon.borderStrokeWidth, - ); + if (!polygon.justHoles) { + if (borderPaint != null) { + addBorderToPath( + getOffsetsXY( + camera: camera, + origin: origin, + points: projectedPolygon.points, + shift: shift, + ), + ); + } + } } // Afterwards deal with more complicated holes. @@ -325,6 +383,7 @@ class _PolygonPainter extends CustomPainter // polygons cutting holes into other polygons, when they should be mixing: // https://github.com/fleaflet/flutter_map/issues/1898. final holePointsList = polygon.holePointsList; + bool oneVisibleHole = false; if (holePointsList != null && holePointsList.isNotEmpty) { // See `Path.combine` comments below // Avoids failing to cut holes if the winding directions of the holes @@ -338,6 +397,9 @@ class _PolygonPainter extends CustomPainter points: singleHolePoints, shift: shift, ); + if (areOffsetsVisible(holeOffsets)) { + oneVisibleHole = true; + } filledPath.addPolygon(holeOffsets, true); // TODO: Potentially more efficient and may change the need to do @@ -351,8 +413,7 @@ class _PolygonPainter extends CustomPainter );*/ } - if (!polygon.disableHolesBorder && polygon.borderStrokeWidth > 0.0) { - final borderPaint = _getBorderPaint(polygon); + if (!polygon.disableHolesBorder && borderPaint != null) { for (final singleHolePoints in projectedPolygon.holePoints) { final holeOffsets = getOffsetsXY( camera: camera, @@ -360,24 +421,24 @@ class _PolygonPainter extends CustomPainter points: singleHolePoints, shift: shift, ); - _addBorderToPath( - borderPath, - polygon, - holeOffsets, - size, - canvas, - borderPaint, - polygon.borderStrokeWidth, - ); + addBorderToPath(holeOffsets); } } } + if (polygon.justHoles) { + return oneVisibleHole + ? WorldWorkControl.visible + : WorldWorkControl.invisible; + } return WorldWorkControl.visible; } workAcrossWorlds(drawIfVisible); + // specifically for "justHoles": we need to draw the holes at the end. + drawPaths(); + if (!drawLabelsLast && polygonLabels && polygon.textPainter != null) { // Labels are expensive because: // * they themselves cannot easily be pulled into our batched path @@ -411,7 +472,10 @@ class _PolygonPainter extends CustomPainter } } - Paint _getBorderPaint(Polygon polygon) { + Paint? _getBorderPaint(Polygon polygon) { + if (polygon.borderStrokeWidth <= 0) { + return null; + } final isDotted = polygon.pattern.spacingFactor != null; return Paint() ..color = polygon.borderColor @@ -428,11 +492,11 @@ class _PolygonPainter extends CustomPainter Size canvasSize, Canvas canvas, Paint paint, - double strokeWidth, ) { final isSolid = polygon.pattern == const StrokePattern.solid(); final isDashed = polygon.pattern.segments != null; final isDotted = polygon.pattern.spacingFactor != null; + final strokeWidth = polygon.borderStrokeWidth; if (isSolid) { final SolidPixelHiker hiker = SolidPixelHiker( @@ -445,7 +509,7 @@ class _PolygonPainter extends CustomPainter } else if (isDotted) { final DottedPixelHiker hiker = DottedPixelHiker( offsets: offsets, - stepLength: polygon.borderStrokeWidth * polygon.pattern.spacingFactor!, + stepLength: strokeWidth * polygon.pattern.spacingFactor!, patternFit: polygon.pattern.patternFit!, closePath: true, canvasSize: canvasSize, diff --git a/lib/src/layer/polygon_layer/polygon.dart b/lib/src/layer/polygon_layer/polygon.dart index ecedd7ac4..df96278fe 100644 --- a/lib/src/layer/polygon_layer/polygon.dart +++ b/lib/src/layer/polygon_layer/polygon.dart @@ -70,6 +70,9 @@ class Polygon with HitDetectableElement { /// it remains upright final bool rotateLabel; + /// True if we want to consider the whole map as the full polygon. + final bool justHoles; + @override final R? hitValue; @@ -125,6 +128,7 @@ class Polygon with HitDetectableElement { this.labelPlacement = PolygonLabelPlacement.centroid, this.rotateLabel = false, this.hitValue, + this.justHoles = false, }) : _filledAndClockwise = color != null && isClockwise(points); /// Checks if the [Polygon] points are ordered clockwise in the list. diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index 6cf7172b5..68d64145f 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -18,8 +18,11 @@ import 'package:latlong2/latlong.dart' hide Path; import 'package:polylabel/polylabel.dart'; part 'label.dart'; + part 'painter.dart'; + part 'polygon.dart'; + part 'projected_polygon.dart'; /// A polygon layer for [FlutterMap]. @@ -133,7 +136,9 @@ class _PolygonLayerState extends State> ? simplifiedElements.toList() : simplifiedElements .where( - (p) => p.polygon.boundingBox.isOverlapping(camera.visibleBounds), + (p) => + p.polygon.justHoles || + p.polygon.boundingBox.isOverlapping(camera.visibleBounds), ) .toList(); diff --git a/lib/src/layer/polygon_layer/projected_polygon.dart b/lib/src/layer/polygon_layer/projected_polygon.dart index 5796c42bc..93a39448f 100644 --- a/lib/src/layer/polygon_layer/projected_polygon.dart +++ b/lib/src/layer/polygon_layer/projected_polygon.dart @@ -23,7 +23,7 @@ class _ProjectedPolygon with HitDetectableElement { final holes = polygon.holePointsList; if (holes == null || holes.isEmpty || - polygon.points.isEmpty || + (polygon.points.isEmpty && !polygon.justHoles) || holes.every((e) => e.isEmpty)) { return >[]; } @@ -32,7 +32,7 @@ class _ProjectedPolygon with HitDetectableElement { holes.length, (j) => projection.projectList( holes[j], - referencePoint: polygon.points[0], + referencePoint: polygon.justHoles ? null : polygon.points[0], ), growable: false, ); From df23e77ecd16f682952c26549cbe1391d4153ec4 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 26 Feb 2025 21:50:18 +0000 Subject: [PATCH 02/18] Added `Polygon.inverted` constructor Renamed "just holes" to "inverted" --- example/lib/main.dart | 4 +-- ...d_polygons.dart => inverted_polygons.dart} | 27 ++++++++------ example/lib/widgets/drawer/menu_drawer.dart | 12 +++---- lib/src/layer/polygon_layer/painter.dart | 23 ++++++------ lib/src/layer/polygon_layer/polygon.dart | 35 ++++++++++++++++--- .../layer/polygon_layer/polygon_layer.dart | 5 +-- .../polygon_layer/projected_polygon.dart | 4 +-- 7 files changed, 69 insertions(+), 41 deletions(-) rename example/lib/pages/{advanced_polygons.dart => inverted_polygons.dart} (85%) diff --git a/example/lib/main.dart b/example/lib/main.dart index 39ab00ae0..ab778424d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter_map_example/pages/advanced_polygons.dart'; import 'package:flutter_map_example/pages/animated_map_controller.dart'; import 'package:flutter_map_example/pages/bundled_offline_map.dart'; import 'package:flutter_map_example/pages/cancellable_tile_provider.dart'; @@ -10,6 +9,7 @@ import 'package:flutter_map_example/pages/epsg4326_crs.dart'; import 'package:flutter_map_example/pages/fallback_url_page.dart'; import 'package:flutter_map_example/pages/home.dart'; import 'package:flutter_map_example/pages/interactive_test_page.dart'; +import 'package:flutter_map_example/pages/inverted_polygons.dart'; import 'package:flutter_map_example/pages/latlng_to_screen_point.dart'; import 'package:flutter_map_example/pages/many_circles.dart'; import 'package:flutter_map_example/pages/many_markers.dart'; @@ -69,7 +69,7 @@ class MyApp extends StatelessWidget { OverlayImagePage.route: (context) => const OverlayImagePage(), PolygonPage.route: (context) => const PolygonPage(), MultiWorldsPage.route: (context) => const MultiWorldsPage(), - AdvancedPolygonsPage.route: (context) => const AdvancedPolygonsPage(), + InvertedPolygonsPage.route: (context) => const InvertedPolygonsPage(), PolygonPerfStressPage.route: (context) => const PolygonPerfStressPage(), SlidingMapPage.route: (_) => const SlidingMapPage(), WMSLayerPage.route: (context) => const WMSLayerPage(), diff --git a/example/lib/pages/advanced_polygons.dart b/example/lib/pages/inverted_polygons.dart similarity index 85% rename from example/lib/pages/advanced_polygons.dart rename to example/lib/pages/inverted_polygons.dart index 791c13f6e..da0ff1b3a 100644 --- a/example/lib/pages/advanced_polygons.dart +++ b/example/lib/pages/inverted_polygons.dart @@ -4,17 +4,17 @@ import 'package:flutter_map_example/misc/tile_providers.dart'; import 'package:flutter_map_example/widgets/drawer/menu_drawer.dart'; import 'package:latlong2/latlong.dart'; -/// Example dedicated to polygons with advanced features. -class AdvancedPolygonsPage extends StatefulWidget { - static const String route = '/advanced_polygons'; +/// Example dedicated to [Polygon.inverted] +class InvertedPolygonsPage extends StatefulWidget { + static const String route = '/inverted_polygons'; - const AdvancedPolygonsPage({super.key}); + const InvertedPolygonsPage({super.key}); @override - State createState() => _AdvancedPolygonsPageState(); + State createState() => _InvertedPolygonsPageState(); } -class _AdvancedPolygonsPageState extends State { +class _InvertedPolygonsPageState extends State { final LayerHitNotifier _hitNotifier = ValueNotifier(null); final _customMarkers = []; @@ -39,7 +39,7 @@ class _AdvancedPolygonsPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Advanced polygons')), - drawer: const MenuDrawer(AdvancedPolygonsPage.route), + drawer: const MenuDrawer(InvertedPolygonsPage.route), body: Stack( children: [ FlutterMap( @@ -65,12 +65,10 @@ class _AdvancedPolygonsPageState extends State { useAltRendering: false, drawLabelsLast: false, polygons: [ - Polygon( + Polygon.inverted( rotateLabel: false, borderColor: Colors.blue, borderStrokeWidth: 3, - justHoles: true, - points: const [], holePointsList: const [ [ // France @@ -109,9 +107,16 @@ class _AdvancedPolygonsPageState extends State { // Quito 0° 14′ 18″ sud, 78° 31′ 02″ ouest LatLng(-0.238333, -78.517222), ], + [ + // Across the border + LatLng(26.69, -137.39), + LatLng(37.91, 150.65), + LatLng(-7.67, 151.20), + LatLng(-7.32, -140.78), + ], ], color: const Color(0x80FF0000), - hitValue: 'South America or France', + hitValue: 'Hit within hole', ), ], ), diff --git a/example/lib/widgets/drawer/menu_drawer.dart b/example/lib/widgets/drawer/menu_drawer.dart index 4756616d0..ad6563476 100644 --- a/example/lib/widgets/drawer/menu_drawer.dart +++ b/example/lib/widgets/drawer/menu_drawer.dart @@ -1,6 +1,5 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_map_example/pages/advanced_polygons.dart'; import 'package:flutter_map_example/pages/animated_map_controller.dart'; import 'package:flutter_map_example/pages/bundled_offline_map.dart'; import 'package:flutter_map_example/pages/cancellable_tile_provider.dart'; @@ -11,6 +10,7 @@ import 'package:flutter_map_example/pages/epsg4326_crs.dart'; import 'package:flutter_map_example/pages/fallback_url_page.dart'; import 'package:flutter_map_example/pages/home.dart'; import 'package:flutter_map_example/pages/interactive_test_page.dart'; +import 'package:flutter_map_example/pages/inverted_polygons.dart'; import 'package:flutter_map_example/pages/latlng_to_screen_point.dart'; import 'package:flutter_map_example/pages/many_circles.dart'; import 'package:flutter_map_example/pages/many_markers.dart'; @@ -116,11 +116,6 @@ class MenuDrawer extends StatelessWidget { routeName: MultiWorldsPage.route, currentRoute: currentRoute, ), - MenuItemWidget( - caption: 'Advanced polygons', - routeName: AdvancedPolygonsPage.route, - currentRoute: currentRoute, - ), const Divider(), MenuItemWidget( caption: 'Map Controller', @@ -184,6 +179,11 @@ class MenuDrawer extends StatelessWidget { routeName: ManyCirclesPage.route, currentRoute: currentRoute, ), + MenuItemWidget( + caption: 'Inverted Polygons', + routeName: InvertedPolygonsPage.route, + currentRoute: currentRoute, + ), const Divider(), MenuItemWidget( caption: 'Zoom Buttons Plugin', diff --git a/lib/src/layer/polygon_layer/painter.dart b/lib/src/layer/polygon_layer/painter.dart index 626baea28..ce7eef752 100644 --- a/lib/src/layer/polygon_layer/painter.dart +++ b/lib/src/layer/polygon_layer/painter.dart @@ -82,20 +82,19 @@ class _PolygonPainter extends CustomPainter if (!isValidHolePolygon) continue; if (isPointInPolygon(point, projectedHoleCoords)) { - if (projectedPolygon.polygon.justHoles) { + if (projectedPolygon.polygon.inverted) { return WorldWorkControl.hit; } isInHole = true; break; } - if (!isHoleVisible) { - if (areOffsetsVisible(projectedHoleCoords)) { - isHoleVisible = true; - } + if (!isHoleVisible && areOffsetsVisible(projectedHoleCoords)) { + // ^ Expensive condition gated & last (lazy logic gates) + isHoleVisible = true; } } - if (projectedPolygon.polygon.justHoles) { + if (projectedPolygon.polygon.inverted) { return isHoleVisible ? WorldWorkControl.visible : WorldWorkControl.invisible; @@ -158,7 +157,7 @@ class _PolygonPainter extends CustomPainter ..style = PaintingStyle.fill ..color = color; - if (trianglePoints.isNotEmpty && !polygon.justHoles) { + if (trianglePoints.isNotEmpty && !polygon.inverted) { final points = Float32List(trianglePoints.length * 2); for (int i = 0; i < trianglePoints.length; ++i) { points[i * 2] = trianglePoints[i].dx; @@ -262,14 +261,14 @@ class _PolygonPainter extends CustomPainter for (int i = 0; i <= polygons.length - 1; i++) { final projectedPolygon = polygons[i]; final polygon = projectedPolygon.polygon; - if (projectedPolygon.points.isEmpty && !polygon.justHoles) continue; + if (projectedPolygon.points.isEmpty && !polygon.inverted) continue; borderPaint = _getBorderPaint(polygon); final polygonTriangles = triangles?[i]; /// Draws on a "single-world" WorldWorkControl drawIfVisible(double shift) { - final fillOffsets = polygon.justHoles + final fillOffsets = polygon.inverted ? null : getOffsetsXY( camera: camera, @@ -317,7 +316,7 @@ class _PolygonPainter extends CustomPainter final opacity = polygon.color?.a ?? 0; if (lastHash != hash || (checkOpacity && opacity > 0 && opacity < 1)) { // we need all holes to be connected with the same full map. - if (!polygon.justHoles) { + if (!polygon.inverted) { drawPaths(); } } @@ -364,7 +363,7 @@ class _PolygonPainter extends CustomPainter ); if (polygon.borderStrokeWidth > 0.0) { - if (!polygon.justHoles) { + if (!polygon.inverted) { if (borderPaint != null) { addBorderToPath( getOffsetsXY( @@ -426,7 +425,7 @@ class _PolygonPainter extends CustomPainter } } - if (polygon.justHoles) { + if (polygon.inverted) { return oneVisibleHole ? WorldWorkControl.visible : WorldWorkControl.invisible; diff --git a/lib/src/layer/polygon_layer/polygon.dart b/lib/src/layer/polygon_layer/polygon.dart index df96278fe..35427e5f8 100644 --- a/lib/src/layer/polygon_layer/polygon.dart +++ b/lib/src/layer/polygon_layer/polygon.dart @@ -70,8 +70,11 @@ class Polygon with HitDetectableElement { /// it remains upright final bool rotateLabel; - /// True if we want to consider the whole map as the full polygon. - final bool justHoles; + /// Whether this polygon is inverted: the entire world is filled with polygon + /// except the [holePointsList] + /// + /// Construct inverted polygons using the [Polygon.inverted] constructor. + final bool inverted; @override final R? hitValue; @@ -128,8 +131,32 @@ class Polygon with HitDetectableElement { this.labelPlacement = PolygonLabelPlacement.centroid, this.rotateLabel = false, this.hitValue, - this.justHoles = false, - }) : _filledAndClockwise = color != null && isClockwise(points); + }) : inverted = false, + _filledAndClockwise = color != null && isClockwise(points); + + /// Create a new inverted [Polygon] + /// + /// Inverted polygons cover the entire world surface across all visible worlds + /// (with no border), except for the [holePointsList] + /// + /// Hitting and interaction is also inverted: [hitValue] is returned for holes + Polygon.inverted({ + required List> this.holePointsList, + this.color, + this.borderStrokeWidth = 0, + this.borderColor = const Color(0xFFFFFF00), + this.pattern = const StrokePattern.solid(), + this.strokeCap = StrokeCap.round, + this.strokeJoin = StrokeJoin.round, + this.label, + this.labelStyle = const TextStyle(), + this.labelPlacement = PolygonLabelPlacement.centroid, + this.rotateLabel = false, + this.hitValue, + }) : points = const [], + inverted = true, + disableHolesBorder = false, + _filledAndClockwise = color != null; /// Checks if the [Polygon] points are ordered clockwise in the list. static bool isClockwise(List points) { diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index 68d64145f..20fdc8afb 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -18,11 +18,8 @@ import 'package:latlong2/latlong.dart' hide Path; import 'package:polylabel/polylabel.dart'; part 'label.dart'; - part 'painter.dart'; - part 'polygon.dart'; - part 'projected_polygon.dart'; /// A polygon layer for [FlutterMap]. @@ -137,7 +134,7 @@ class _PolygonLayerState extends State> : simplifiedElements .where( (p) => - p.polygon.justHoles || + p.polygon.inverted || p.polygon.boundingBox.isOverlapping(camera.visibleBounds), ) .toList(); diff --git a/lib/src/layer/polygon_layer/projected_polygon.dart b/lib/src/layer/polygon_layer/projected_polygon.dart index 93a39448f..8f603de61 100644 --- a/lib/src/layer/polygon_layer/projected_polygon.dart +++ b/lib/src/layer/polygon_layer/projected_polygon.dart @@ -23,7 +23,7 @@ class _ProjectedPolygon with HitDetectableElement { final holes = polygon.holePointsList; if (holes == null || holes.isEmpty || - (polygon.points.isEmpty && !polygon.justHoles) || + (polygon.points.isEmpty && !polygon.inverted) || holes.every((e) => e.isEmpty)) { return >[]; } @@ -32,7 +32,7 @@ class _ProjectedPolygon with HitDetectableElement { holes.length, (j) => projection.projectList( holes[j], - referencePoint: polygon.justHoles ? null : polygon.points[0], + referencePoint: polygon.inverted ? null : polygon.points[0], ), growable: false, ); From e477e1a1135dc9929b9751132f625bfb0327cf47 Mon Sep 17 00:00:00 2001 From: monsieurtanuki Date: Fri, 7 Mar 2025 12:26:32 +0100 Subject: [PATCH 03/18] moved inverted feature to PolygonLayer Impacted files: * `inverted_polygons.dart`: moved the "inverted" effect to PolygonLayer; additional border and fill display tests * `painter.dart`: moved the "inverted" effect to PolygonLayer * `polygon.dart`: rolled back the "inverted" side-effects * `polygon_layer.dart`: new `Color? invertedFill` field; rolled back the "inverted" side-effects * `projected_polygon.dart`: rolled back the "inverted" side-effects --- example/lib/pages/inverted_polygons.dart | 104 ++-- lib/src/layer/polygon_layer/painter.dart | 477 +++++++++--------- lib/src/layer/polygon_layer/polygon.dart | 33 +- .../layer/polygon_layer/polygon_layer.dart | 12 +- .../polygon_layer/projected_polygon.dart | 4 +- 5 files changed, 298 insertions(+), 332 deletions(-) diff --git a/example/lib/pages/inverted_polygons.dart b/example/lib/pages/inverted_polygons.dart index da0ff1b3a..86aa53380 100644 --- a/example/lib/pages/inverted_polygons.dart +++ b/example/lib/pages/inverted_polygons.dart @@ -4,7 +4,7 @@ import 'package:flutter_map_example/misc/tile_providers.dart'; import 'package:flutter_map_example/widgets/drawer/menu_drawer.dart'; import 'package:latlong2/latlong.dart'; -/// Example dedicated to [Polygon.inverted] +/// Example dedicated to [PolygonLayer.invertedFill] class InvertedPolygonsPage extends StatefulWidget { static const String route = '/inverted_polygons'; @@ -64,59 +64,63 @@ class _InvertedPolygonsPageState extends State { simplificationTolerance: 0, useAltRendering: false, drawLabelsLast: false, + invertedFill: const Color(0x20FF0000), polygons: [ - Polygon.inverted( + Polygon( rotateLabel: false, - borderColor: Colors.blue, + borderColor: const Color(0xFF0000FF), borderStrokeWidth: 3, - holePointsList: const [ - [ - // France - // Calais 50° 56′ 53″ nord, 1° 51′ 23″ est - LatLng(50.948056, 1.856389), - // Brest 48° 23′ 27″ nord, 4° 29′ 08″ ouest - LatLng(48.390833, -4.485556), - // Biarritz 43° 28′ 54″ nord, 1° 33′ 22″ ouest - LatLng(43.481667, -1.556111), - // Perpignan 42° 41′ 55″ nord, 2° 53′ 44″ est - LatLng(42.698611, 2.895556), - // Menton 43° 46′ 33″ nord, 7° 30′ 10″ est - LatLng(43.775833, 7.502778), - // Strasbourg 48° 34′ 24″ nord, 7° 45′ 08″ est - LatLng(48.573333, 7.752222), - ], - [ - // Corsica - // Bonifacio 41° 23′ nord, 9° 09′ est - LatLng(41.383333, 9.15), - // Ajaccio 41° 55′ 36″ nord, 8° 44′ 13″ est - LatLng(41.926667, 8.736944), - // Calvi 42° 34′ 07″ nord, 8° 45′ 25″ est - LatLng(42.568611, 8.756944), - // Bastia 42° 42′ 03″ nord, 9° 27′ 01″ est - LatLng(42.700833, 9.450278), - ], - [ - // South America - // Ushuaia 54° 48′ 35″ sud, 68° 18′ 50″ ouest - LatLng(-54.809722, -68.313889), - // Fortaleza 3° 43′ 01″ sud, 38° 32′ 34″ ouest - LatLng(-3.716944, -38.542778), - // Panama 8° 58′ nord, 79° 32′ ouest - LatLng(8.966667, -79.533333), - // Quito 0° 14′ 18″ sud, 78° 31′ 02″ ouest - LatLng(-0.238333, -78.517222), - ], - [ - // Across the border - LatLng(26.69, -137.39), - LatLng(37.91, 150.65), - LatLng(-7.67, 151.20), - LatLng(-7.32, -140.78), - ], + color: const Color(0x4000FFFF), + hitValue: 'France', + points: const [ + // France + // Calais 50° 56′ 53″ nord, 1° 51′ 23″ est + LatLng(50.948056, 1.856389), + // Brest 48° 23′ 27″ nord, 4° 29′ 08″ ouest + LatLng(48.390833, -4.485556), + // Biarritz 43° 28′ 54″ nord, 1° 33′ 22″ ouest + LatLng(43.481667, -1.556111), + // Perpignan 42° 41′ 55″ nord, 2° 53′ 44″ est + LatLng(42.698611, 2.895556), + // Menton 43° 46′ 33″ nord, 7° 30′ 10″ est + LatLng(43.775833, 7.502778), + // Strasbourg 48° 34′ 24″ nord, 7° 45′ 08″ est + LatLng(48.573333, 7.752222), + ], + ), + Polygon( + rotateLabel: false, + borderColor: const Color(0xFFFF0000), + borderStrokeWidth: 3, + pattern: StrokePattern.dashed(segments: [20, 10, 10, 10]), + color: null, + hitValue: 'South America', + points: const [ + // South America + // Ushuaia 54° 48′ 35″ sud, 68° 18′ 50″ ouest + LatLng(-54.809722, -68.313889), + // Fortaleza 3° 43′ 01″ sud, 38° 32′ 34″ ouest + LatLng(-3.716944, -38.542778), + // Panama 8° 58′ nord, 79° 32′ ouest + LatLng(8.966667, -79.533333), + // Quito 0° 14′ 18″ sud, 78° 31′ 02″ ouest + LatLng(-0.238333, -78.517222), + ], + ), + Polygon( + rotateLabel: false, + borderColor: const Color(0xFFFF0000), + borderStrokeWidth: 3, + pattern: const StrokePattern.dotted(spacingFactor: 3), + color: const Color(0x20000000), + hitValue: 'Across the border', + points: const [ + // Across the border + LatLng(26.69, -137.39), + LatLng(37.91, 150.65), + LatLng(-7.67, 151.20), + LatLng(-7.32, -140.78), ], - color: const Color(0x80FF0000), - hitValue: 'Hit within hole', ), ], ), diff --git a/lib/src/layer/polygon_layer/painter.dart b/lib/src/layer/polygon_layer/painter.dart index ce7eef752..f7e4cf99f 100644 --- a/lib/src/layer/polygon_layer/painter.dart +++ b/lib/src/layer/polygon_layer/painter.dart @@ -35,6 +35,9 @@ class _PolygonPainter extends CustomPainter /// See [PolygonLayer.debugAltRenderer] final bool debugAltRenderer; + /// See [PolygonLayer.invertedFill] + final Color? invertedFill; + @override final MapCamera camera; @@ -49,6 +52,7 @@ class _PolygonPainter extends CustomPainter required this.drawLabelsLast, required this.debugAltRenderer, required this.camera, + required this.invertedFill, required this.hitNotifier, }) : bounds = camera.visibleBounds; @@ -66,40 +70,6 @@ class _PolygonPainter extends CustomPainter // } WorldWorkControl checkIfHit(double shift) { - bool isInHole = false; - bool isHoleVisible = false; - for (final points in projectedPolygon.holePoints) { - final projectedHoleCoords = getOffsetsXY( - camera: camera, - origin: origin, - points: points, - shift: shift, - ); - if (projectedHoleCoords.first != projectedHoleCoords.last) { - projectedHoleCoords.add(projectedHoleCoords.first); - } - final isValidHolePolygon = projectedHoleCoords.length >= 3; - if (!isValidHolePolygon) continue; - - if (isPointInPolygon(point, projectedHoleCoords)) { - if (projectedPolygon.polygon.inverted) { - return WorldWorkControl.hit; - } - isInHole = true; - break; - } - if (!isHoleVisible && areOffsetsVisible(projectedHoleCoords)) { - // ^ Expensive condition gated & last (lazy logic gates) - isHoleVisible = true; - } - } - - if (projectedPolygon.polygon.inverted) { - return isHoleVisible - ? WorldWorkControl.visible - : WorldWorkControl.invisible; - } - final projectedCoords = getOffsetsXY( camera: camera, origin: origin, @@ -118,6 +88,24 @@ class _PolygonPainter extends CustomPainter final isInPolygon = isValidPolygon && isPointInPolygon(point, projectedCoords); + final isInHole = projectedPolygon.holePoints.any( + (points) { + final projectedHoleCoords = getOffsetsXY( + camera: camera, + origin: origin, + points: points, + shift: shift, + ); + if (projectedHoleCoords.first != projectedHoleCoords.last) { + projectedHoleCoords.add(projectedHoleCoords.first); + } + + final isValidHolePolygon = projectedHoleCoords.length >= 3; + return isValidHolePolygon && + isPointInPolygon(point, projectedHoleCoords); + }, + ); + // Second check handles case where polygon outline intersects a hole, // ensuring that the hit matches with the visual representation return (isInPolygon && !isInHole) || (!isInPolygon && isInHole) @@ -142,90 +130,95 @@ class _PolygonPainter extends CustomPainter final filledPath = Path(); final borderPath = Path(); - Polygon? lastPolygon; + Color? lastColor; int? lastHash; Paint? borderPaint; + late bool currentlyInvertedFill; + + // Draw polygon outline + void drawBorders() { + if (!currentlyInvertedFill && borderPaint != null) { + canvas.drawPath(borderPath, borderPaint); + } + borderPath.reset(); + lastHash = null; + } // This functions flushes the batched fill and border paths constructed below void drawPaths() { - if (lastPolygon == null) return; - final polygon = lastPolygon!; + final Color? color = currentlyInvertedFill ? invertedFill! : lastColor; + if (color == null) { + drawBorders(); + return; + } // Draw filled polygon - if (polygon.color case final color?) { - final paint = Paint() - ..style = PaintingStyle.fill - ..color = color; - - if (trianglePoints.isNotEmpty && !polygon.inverted) { - final points = Float32List(trianglePoints.length * 2); - for (int i = 0; i < trianglePoints.length; ++i) { - points[i * 2] = trianglePoints[i].dx; - points[i * 2 + 1] = trianglePoints[i].dy; - } - final vertices = Vertices.raw(VertexMode.triangles, points); - canvas.drawVertices(vertices, BlendMode.src, paint); - - if (debugAltRenderer) { - for (int i = 0; i < trianglePoints.length; i += 3) { - canvas.drawCircle( - trianglePoints[i], - 5, - Paint()..color = const Color(0x7EFF0000), - ); - canvas.drawCircle( - trianglePoints[i + 1], - 5, - Paint()..color = const Color(0x7E00FF00), - ); - canvas.drawCircle( - trianglePoints[i + 2], - 5, - Paint()..color = const Color(0x7E0000FF), - ); + final paint = Paint() + ..style = PaintingStyle.fill + ..color = color; + + if (trianglePoints.isNotEmpty && !currentlyInvertedFill) { + final points = Float32List(trianglePoints.length * 2); + for (int i = 0; i < trianglePoints.length; ++i) { + points[i * 2] = trianglePoints[i].dx; + points[i * 2 + 1] = trianglePoints[i].dy; + } + final vertices = Vertices.raw(VertexMode.triangles, points); + canvas.drawVertices(vertices, BlendMode.src, paint); - final path = Path() - ..addPolygon( - [ - trianglePoints[i], - trianglePoints[i + 1], - trianglePoints[i + 2], - ], - true, - ); + if (debugAltRenderer) { + for (int i = 0; i < trianglePoints.length; i += 3) { + canvas.drawCircle( + trianglePoints[i], + 5, + Paint()..color = const Color(0x7EFF0000), + ); + canvas.drawCircle( + trianglePoints[i + 1], + 5, + Paint()..color = const Color(0x7E00FF00), + ); + canvas.drawCircle( + trianglePoints[i + 2], + 5, + Paint()..color = const Color(0x7E0000FF), + ); - canvas.drawPath( - path, - Paint() - ..color = const Color(0x7EFFFFFF) - ..style = PaintingStyle.fill, + final path = Path() + ..addPolygon( + [ + trianglePoints[i], + trianglePoints[i + 1], + trianglePoints[i + 2], + ], + true, ); - canvas.drawPath( - path, - Paint() - ..color = const Color(0xFF000000) - ..style = PaintingStyle.stroke, - ); - } + canvas.drawPath( + path, + Paint() + ..color = const Color(0x7EFFFFFF) + ..style = PaintingStyle.fill, + ); + + canvas.drawPath( + path, + Paint() + ..color = const Color(0xFF000000) + ..style = PaintingStyle.stroke, + ); } - } else { - canvas.drawPath(filledPath, paint); } - } - - // Draw polygon outline - if (borderPaint != null) { - canvas.drawPath(borderPath, borderPaint); + } else { + canvas.drawPath(filledPath, paint); } trianglePoints.clear(); filledPath.reset(); - borderPath.reset(); + lastColor = null; - lastPolygon = null; - lastHash = null; + drawBorders(); } /// Draws labels on a "single-world" @@ -258,40 +251,59 @@ class _PolygonPainter extends CustomPainter } // Main loop constructing batched fill and border paths from given polygons. - for (int i = 0; i <= polygons.length - 1; i++) { - final projectedPolygon = polygons[i]; - final polygon = projectedPolygon.polygon; - if (projectedPolygon.points.isEmpty && !polygon.inverted) continue; - borderPaint = _getBorderPaint(polygon); - - final polygonTriangles = triangles?[i]; + final List currentlyInvertedFills = [ + if (invertedFill != null) true, + false, + ]; + for (final item in currentlyInvertedFills) { + currentlyInvertedFill = item; + + if (currentlyInvertedFill) { + // we display once the full map. + final minMaxProjected = + camera.crs.projection.projectList(_minMaxLatitude); + final minMaxY = getOffsetsXY( + camera: camera, + origin: origin, + points: minMaxProjected, + ); + final maxX = viewportRect.right; + final minX = viewportRect.left; + final maxY = minMaxY[0].dy; + final minY = minMaxY[1].dy; + final rect = Rect.fromLTRB(minX, minY, maxX, maxY); + filledPath.addRect(rect); + filledPath.fillType = PathFillType.evenOdd; + } - /// Draws on a "single-world" - WorldWorkControl drawIfVisible(double shift) { - final fillOffsets = polygon.inverted - ? null - : getOffsetsXY( - camera: camera, - origin: origin, - points: projectedPolygon.points, - holePoints: polygonTriangles != null - ? projectedPolygon.holePoints - : null, - shift: shift, - ); - if (fillOffsets != null) { + for (int i = 0; i <= polygons.length - 1; i++) { + final projectedPolygon = polygons[i]; + final polygon = projectedPolygon.polygon; + if (projectedPolygon.points.isEmpty) continue; + borderPaint = _getBorderPaint(polygon); + + final polygonTriangles = triangles?[i]; + + /// Draws on a "single-world" + WorldWorkControl drawIfVisible(double shift) { + final fillOffsets = getOffsetsXY( + camera: camera, + origin: origin, + points: projectedPolygon.points, + holePoints: + polygonTriangles != null ? projectedPolygon.holePoints : null, + shift: shift, + ); if (!areOffsetsVisible(fillOffsets)) { return WorldWorkControl.invisible; } - } - if (debugAltRenderer) { - const offsetsLabelStyle = TextStyle( - color: Color(0xFF000000), - fontSize: 16, - ); + if (debugAltRenderer && !currentlyInvertedFill) { + const offsetsLabelStyle = TextStyle( + color: Color(0xFF000000), + fontSize: 16, + ); - if (fillOffsets != null) { for (int i = 0; i < fillOffsets.length; i++) { TextPainter( text: TextSpan( @@ -304,28 +316,28 @@ class _PolygonPainter extends CustomPainter ..paint(canvas, fillOffsets[i]); } } - } - // The hash is based on the polygons visual properties. If the hash from - // the current and the previous polygon no longer match, we need to flush - // the batch previous polygons. - // We also need to flush if the opacity is not 1 or 0, so that they get - // mixed properly. Otherwise, holes get cut, or colors aren't mixed, - // depending on the holes handler. - final hash = polygon.renderHashCode; - final opacity = polygon.color?.a ?? 0; - if (lastHash != hash || (checkOpacity && opacity > 0 && opacity < 1)) { - // we need all holes to be connected with the same full map. - if (!polygon.inverted) { - drawPaths(); + // The hash is based on the polygons visual properties. If the hash from + // the current and the previous polygon no longer match, we need to flush + // the batch previous polygons. + // We also need to flush if the opacity is not 1 or 0, so that they get + // mixed properly. Otherwise, holes get cut, or colors aren't mixed, + // depending on the holes handler. + if (!currentlyInvertedFill) { + final hash = polygon.renderHashCode; + final opacity = polygon.color?.a ?? 0; + if (lastHash != hash || + (checkOpacity && opacity > 0 && opacity < 1)) { + drawPaths(); + } + lastColor = polygon.color; + lastHash = hash; + } else { + lastColor = invertedFill; } - } - lastPolygon = polygon; - lastHash = hash; - // First add fills and borders to path. - if (polygon.color != null) { - if (fillOffsets != null) { + // First add fills and borders to path. + if (polygon.color != null || currentlyInvertedFill) { if (polygonTriangles != null) { final len = polygonTriangles.length; for (int i = 0; i < len; ++i) { @@ -334,85 +346,39 @@ class _PolygonPainter extends CustomPainter } else { filledPath.addPolygon(fillOffsets, true); } - } else if (shift == 0) { - // we display once the full map. - final minMaxProjected = - camera.crs.projection.projectList(_minMaxLatitude); - final minMaxY = getOffsetsXY( - camera: camera, - origin: origin, - points: minMaxProjected, - shift: shift, - ); - final maxX = viewportRect.right; - final minX = viewportRect.left; - final maxY = minMaxY[0].dy; - final minY = minMaxY[1].dy; - final rect = Rect.fromLTRB(minX, minY, maxX, maxY); - filledPath.addRect(rect); } - } - - void addBorderToPath(List offsets) => _addBorderToPath( - borderPath, - polygon, - offsets, - size, - canvas, - borderPaint!, - ); - if (polygon.borderStrokeWidth > 0.0) { - if (!polygon.inverted) { - if (borderPaint != null) { - addBorderToPath( - getOffsetsXY( - camera: camera, - origin: origin, - points: projectedPolygon.points, - shift: shift, - ), + void addBorderToPath(List offsets) => _addBorderToPath( + borderPath, + polygon, + offsets, + size, + canvas, + borderPaint!, ); - } - } - } - // Afterwards deal with more complicated holes. - // Improper handling of opacity and fill methods may result in normal - // polygons cutting holes into other polygons, when they should be mixing: - // https://github.com/fleaflet/flutter_map/issues/1898. - final holePointsList = polygon.holePointsList; - bool oneVisibleHole = false; - if (holePointsList != null && holePointsList.isNotEmpty) { - // See `Path.combine` comments below - // Avoids failing to cut holes if the winding directions of the holes - // and the normal points are the same - filledPath.fillType = PathFillType.evenOdd; - - for (final singleHolePoints in projectedPolygon.holePoints) { - final holeOffsets = getOffsetsXY( - camera: camera, - origin: origin, - points: singleHolePoints, - shift: shift, + if (borderPaint != null && !currentlyInvertedFill) { + addBorderToPath( + getOffsetsXY( + camera: camera, + origin: origin, + points: projectedPolygon.points, + shift: shift, + ), ); - if (areOffsetsVisible(holeOffsets)) { - oneVisibleHole = true; - } - filledPath.addPolygon(holeOffsets, true); - - // TODO: Potentially more efficient and may change the need to do - // opacity checking - needs testing. Also need to verify if `xor` or - // `difference` is preferred. - // No longer blocked by lack of HTML support in Flutter 3.29 - /*filledPath = Path.combine( - PathOperation.xor, - filledPath, - Path()..addPolygon(holeOffsets, true), - );*/ } - if (!polygon.disableHolesBorder && borderPaint != null) { + // Afterwards deal with more complicated holes. + // Improper handling of opacity and fill methods may result in normal + // polygons cutting holes into other polygons, when they should be mixing: + // https://github.com/fleaflet/flutter_map/issues/1898. + final holePointsList = polygon.holePointsList; + if (holePointsList != null && holePointsList.isNotEmpty) { + // See `Path.combine` comments below + // Avoids failing to cut holes if the winding directions of the holes + // and the normal points are the same + filledPath.fillType = PathFillType.evenOdd; + for (final singleHolePoints in projectedPolygon.holePoints) { final holeOffsets = getOffsetsXY( camera: camera, @@ -420,43 +386,63 @@ class _PolygonPainter extends CustomPainter points: singleHolePoints, shift: shift, ); - addBorderToPath(holeOffsets); + filledPath.addPolygon(holeOffsets, true); + + // TODO: Potentially more efficient and may change the need to do + // opacity checking - needs testing. Also need to verify if `xor` or + // `difference` is preferred. + // No longer blocked by lack of HTML support in Flutter 3.29 + /*filledPath = Path.combine( + PathOperation.xor, + filledPath, + Path()..addPolygon(holeOffsets, true), + );*/ + } + + if (!polygon.disableHolesBorder && borderPaint != null) { + for (final singleHolePoints in projectedPolygon.holePoints) { + final holeOffsets = getOffsetsXY( + camera: camera, + origin: origin, + points: singleHolePoints, + shift: shift, + ); + addBorderToPath(holeOffsets); + } } } + + return WorldWorkControl.visible; } - if (polygon.inverted) { - return oneVisibleHole - ? WorldWorkControl.visible - : WorldWorkControl.invisible; + workAcrossWorlds(drawIfVisible); + + if (!currentlyInvertedFill && + !drawLabelsLast && + polygonLabels && + polygon.textPainter != null) { + // Labels are expensive because: + // * they themselves cannot easily be pulled into our batched path + // painting with the given text APIs + // * therefore, they require us to flush the batch of polygon draws to + // ensure polygons and labels are stacked correctly, i.e.: + // p1, p1_label, p2, p2_label, ... . + + // The painter will be null if the layOuting algorithm determined that + // there isn't enough space. + workAcrossWorlds( + (double shift) => drawLabelIfVisible(shift, projectedPolygon), + ); + } + if (!currentlyInvertedFill) { + drawPaths(); } - return WorldWorkControl.visible; } - workAcrossWorlds(drawIfVisible); - - // specifically for "justHoles": we need to draw the holes at the end. drawPaths(); - - if (!drawLabelsLast && polygonLabels && polygon.textPainter != null) { - // Labels are expensive because: - // * they themselves cannot easily be pulled into our batched path - // painting with the given text APIs - // * therefore, they require us to flush the batch of polygon draws to - // ensure polygons and labels are stacked correctly, i.e.: - // p1, p1_label, p2, p2_label, ... . - - // The painter will be null if the layOuting algorithm determined that - // there isn't enough space. - workAcrossWorlds( - (double shift) => drawLabelIfVisible(shift, projectedPolygon), - ); - } } - drawPaths(); - - if (polygonLabels && drawLabelsLast) { + if (!currentlyInvertedFill && polygonLabels && drawLabelsLast) { for (final projectedPolygon in polygons) { if (projectedPolygon.points.isEmpty) { continue; @@ -548,6 +534,7 @@ class _PolygonPainter extends CustomPainter triangles != oldDelegate.triangles || camera != oldDelegate.camera || bounds != oldDelegate.bounds || + invertedFill != oldDelegate.invertedFill || drawLabelsLast != oldDelegate.drawLabelsLast || polygonLabels != oldDelegate.polygonLabels || hitNotifier != oldDelegate.hitNotifier; diff --git a/lib/src/layer/polygon_layer/polygon.dart b/lib/src/layer/polygon_layer/polygon.dart index 35427e5f8..ecedd7ac4 100644 --- a/lib/src/layer/polygon_layer/polygon.dart +++ b/lib/src/layer/polygon_layer/polygon.dart @@ -70,12 +70,6 @@ class Polygon with HitDetectableElement { /// it remains upright final bool rotateLabel; - /// Whether this polygon is inverted: the entire world is filled with polygon - /// except the [holePointsList] - /// - /// Construct inverted polygons using the [Polygon.inverted] constructor. - final bool inverted; - @override final R? hitValue; @@ -131,32 +125,7 @@ class Polygon with HitDetectableElement { this.labelPlacement = PolygonLabelPlacement.centroid, this.rotateLabel = false, this.hitValue, - }) : inverted = false, - _filledAndClockwise = color != null && isClockwise(points); - - /// Create a new inverted [Polygon] - /// - /// Inverted polygons cover the entire world surface across all visible worlds - /// (with no border), except for the [holePointsList] - /// - /// Hitting and interaction is also inverted: [hitValue] is returned for holes - Polygon.inverted({ - required List> this.holePointsList, - this.color, - this.borderStrokeWidth = 0, - this.borderColor = const Color(0xFFFFFF00), - this.pattern = const StrokePattern.solid(), - this.strokeCap = StrokeCap.round, - this.strokeJoin = StrokeJoin.round, - this.label, - this.labelStyle = const TextStyle(), - this.labelPlacement = PolygonLabelPlacement.centroid, - this.rotateLabel = false, - this.hitValue, - }) : points = const [], - inverted = true, - disableHolesBorder = false, - _filledAndClockwise = color != null; + }) : _filledAndClockwise = color != null && isClockwise(points); /// Checks if the [Polygon] points are ordered clockwise in the list. static bool isClockwise(List points) { diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index 20fdc8afb..c48ec0f76 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -18,8 +18,11 @@ import 'package:latlong2/latlong.dart' hide Path; import 'package:polylabel/polylabel.dart'; part 'label.dart'; + part 'painter.dart'; + part 'polygon.dart'; + part 'projected_polygon.dart'; /// A polygon layer for [FlutterMap]. @@ -66,6 +69,9 @@ base class PolygonLayer /// Defaults to `false`. final bool drawLabelsLast; + /// Color to apply to the whole map - except for polygons + final Color? invertedFill; + /// {@macro fm.lhn.layerHitNotifier.usage} final LayerHitNotifier? hitNotifier; @@ -78,6 +84,7 @@ base class PolygonLayer this.polygonCulling = true, this.polygonLabels = true, this.drawLabelsLast = false, + this.invertedFill, this.hitNotifier, super.simplificationTolerance, }) : super(); @@ -133,9 +140,7 @@ class _PolygonLayerState extends State> ? simplifiedElements.toList() : simplifiedElements .where( - (p) => - p.polygon.inverted || - p.polygon.boundingBox.isOverlapping(camera.visibleBounds), + (p) => p.polygon.boundingBox.isOverlapping(camera.visibleBounds), ) .toList(); @@ -176,6 +181,7 @@ class _PolygonLayerState extends State> camera: camera, polygonLabels: widget.polygonLabels, drawLabelsLast: widget.drawLabelsLast, + invertedFill: widget.invertedFill, debugAltRenderer: widget.debugAltRenderer, hitNotifier: widget.hitNotifier, ), diff --git a/lib/src/layer/polygon_layer/projected_polygon.dart b/lib/src/layer/polygon_layer/projected_polygon.dart index 8f603de61..5796c42bc 100644 --- a/lib/src/layer/polygon_layer/projected_polygon.dart +++ b/lib/src/layer/polygon_layer/projected_polygon.dart @@ -23,7 +23,7 @@ class _ProjectedPolygon with HitDetectableElement { final holes = polygon.holePointsList; if (holes == null || holes.isEmpty || - (polygon.points.isEmpty && !polygon.inverted) || + polygon.points.isEmpty || holes.every((e) => e.isEmpty)) { return >[]; } @@ -32,7 +32,7 @@ class _ProjectedPolygon with HitDetectableElement { holes.length, (j) => projection.projectList( holes[j], - referencePoint: polygon.inverted ? null : polygon.points[0], + referencePoint: polygon.points[0], ), growable: false, ); From cb510a4079d4837e310e9b2dde78a51801914b96 Mon Sep 17 00:00:00 2001 From: monsieurtanuki Date: Fri, 7 Mar 2025 12:31:31 +0100 Subject: [PATCH 04/18] minor color change --- example/lib/pages/inverted_polygons.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/lib/pages/inverted_polygons.dart b/example/lib/pages/inverted_polygons.dart index 86aa53380..b030ecfb2 100644 --- a/example/lib/pages/inverted_polygons.dart +++ b/example/lib/pages/inverted_polygons.dart @@ -109,7 +109,7 @@ class _InvertedPolygonsPageState extends State { ), Polygon( rotateLabel: false, - borderColor: const Color(0xFFFF0000), + borderColor: const Color(0xFF00FF00), borderStrokeWidth: 3, pattern: const StrokePattern.dotted(spacingFactor: 3), color: const Color(0x20000000), From 75a09f88334316d560c4766aecbad8b6a5d760f2 Mon Sep 17 00:00:00 2001 From: monsieurtanuki Date: Mon, 10 Mar 2025 18:51:19 +0100 Subject: [PATCH 05/18] refactored coding everything about invertedFill in a preliminary step --- example/lib/pages/polygon.dart | 2 + lib/src/layer/polygon_layer/painter.dart | 318 ++++++++++++----------- 2 files changed, 166 insertions(+), 154 deletions(-) diff --git a/example/lib/pages/polygon.dart b/example/lib/pages/polygon.dart index 02717514e..ed3782b42 100644 --- a/example/lib/pages/polygon.dart +++ b/example/lib/pages/polygon.dart @@ -320,6 +320,8 @@ class _PolygonPageState extends State { child: PolygonLayer( hitNotifier: _hitNotifier, simplificationTolerance: 0, + // TODO temporarily, just for the tests + invertedFill: Colors.orangeAccent.withAlpha(64), polygons: [..._polygonsRaw, ...?_hoverGons], ), ), diff --git a/lib/src/layer/polygon_layer/painter.dart b/lib/src/layer/polygon_layer/painter.dart index f7e4cf99f..f63b85028 100644 --- a/lib/src/layer/polygon_layer/painter.dart +++ b/lib/src/layer/polygon_layer/painter.dart @@ -133,11 +133,10 @@ class _PolygonPainter extends CustomPainter Color? lastColor; int? lastHash; Paint? borderPaint; - late bool currentlyInvertedFill; // Draw polygon outline void drawBorders() { - if (!currentlyInvertedFill && borderPaint != null) { + if (borderPaint != null) { canvas.drawPath(borderPath, borderPaint); } borderPath.reset(); @@ -146,7 +145,7 @@ class _PolygonPainter extends CustomPainter // This functions flushes the batched fill and border paths constructed below void drawPaths() { - final Color? color = currentlyInvertedFill ? invertedFill! : lastColor; + final Color? color = lastColor; if (color == null) { drawBorders(); return; @@ -157,7 +156,7 @@ class _PolygonPainter extends CustomPainter ..style = PaintingStyle.fill ..color = color; - if (trianglePoints.isNotEmpty && !currentlyInvertedFill) { + if (trianglePoints.isNotEmpty) { final points = Float32List(trianglePoints.length * 2); for (int i = 0; i < trianglePoints.length; ++i) { points[i * 2] = trianglePoints[i].dx; @@ -250,135 +249,175 @@ class _PolygonPainter extends CustomPainter return WorldWorkControl.visible; } - // Main loop constructing batched fill and border paths from given polygons. - final List currentlyInvertedFills = [ - if (invertedFill != null) true, - false, - ]; - for (final item in currentlyInvertedFills) { - currentlyInvertedFill = item; - - if (currentlyInvertedFill) { - // we display once the full map. - final minMaxProjected = - camera.crs.projection.projectList(_minMaxLatitude); - final minMaxY = getOffsetsXY( - camera: camera, - origin: origin, - points: minMaxProjected, - ); - final maxX = viewportRect.right; - final minX = viewportRect.left; - final maxY = minMaxY[0].dy; - final minY = minMaxY[1].dy; - final rect = Rect.fromLTRB(minX, minY, maxX, maxY); - filledPath.addRect(rect); - filledPath.fillType = PathFillType.evenOdd; - } + // Specific map treatment with `invertFill`. + if (invertedFill != null) { + filledPath.reset(); + final minMaxProjected = + camera.crs.projection.projectList(_minMaxLatitude); + final minMaxY = getOffsetsXY( + camera: camera, + origin: origin, + points: minMaxProjected, + ); + final maxX = viewportRect.right; + final minX = viewportRect.left; + final maxY = minMaxY[0].dy; + final minY = minMaxY[1].dy; + final rect = Rect.fromLTRB(minX, minY, maxX, maxY); + filledPath.addRect(rect); + filledPath.fillType = PathFillType.evenOdd; for (int i = 0; i <= polygons.length - 1; i++) { final projectedPolygon = polygons[i]; - final polygon = projectedPolygon.polygon; if (projectedPolygon.points.isEmpty) continue; - borderPaint = _getBorderPaint(polygon); - final polygonTriangles = triangles?[i]; - - /// Draws on a "single-world" - WorldWorkControl drawIfVisible(double shift) { + /// Draws each full polygon as a hole on a "single-world" + WorldWorkControl drawPolygonAsHole(double shift) { final fillOffsets = getOffsetsXY( camera: camera, origin: origin, points: projectedPolygon.points, - holePoints: - polygonTriangles != null ? projectedPolygon.holePoints : null, shift: shift, ); if (!areOffsetsVisible(fillOffsets)) { return WorldWorkControl.invisible; } + filledPath.addPolygon(fillOffsets, true); + return WorldWorkControl.visible; + } - if (debugAltRenderer && !currentlyInvertedFill) { - const offsetsLabelStyle = TextStyle( - color: Color(0xFF000000), - fontSize: 16, - ); + workAcrossWorlds(drawPolygonAsHole); + } - for (int i = 0; i < fillOffsets.length; i++) { - TextPainter( - text: TextSpan( - text: i.toString(), - style: offsetsLabelStyle, - ), - textDirection: TextDirection.ltr, - ) - ..layout(maxWidth: 100) - ..paint(canvas, fillOffsets[i]); - } + // Draw filled map with polygons as holes + final paint = Paint() + ..style = PaintingStyle.fill + ..color = invertedFill!; + + canvas.drawPath(filledPath, paint); + + filledPath.reset(); + } + + for (int i = 0; i <= polygons.length - 1; i++) { + final projectedPolygon = polygons[i]; + final polygon = projectedPolygon.polygon; + if (projectedPolygon.points.isEmpty) continue; + borderPaint = _getBorderPaint(polygon); + + final polygonTriangles = triangles?[i]; + + /// Draws on a "single-world" + WorldWorkControl drawIfVisible(double shift) { + final fillOffsets = getOffsetsXY( + camera: camera, + origin: origin, + points: projectedPolygon.points, + holePoints: + polygonTriangles != null ? projectedPolygon.holePoints : null, + shift: shift, + ); + if (!areOffsetsVisible(fillOffsets)) { + return WorldWorkControl.invisible; + } + + if (debugAltRenderer) { + const offsetsLabelStyle = TextStyle( + color: Color(0xFF000000), + fontSize: 16, + ); + + for (int i = 0; i < fillOffsets.length; i++) { + TextPainter( + text: TextSpan( + text: i.toString(), + style: offsetsLabelStyle, + ), + textDirection: TextDirection.ltr, + ) + ..layout(maxWidth: 100) + ..paint(canvas, fillOffsets[i]); } + } - // The hash is based on the polygons visual properties. If the hash from - // the current and the previous polygon no longer match, we need to flush - // the batch previous polygons. - // We also need to flush if the opacity is not 1 or 0, so that they get - // mixed properly. Otherwise, holes get cut, or colors aren't mixed, - // depending on the holes handler. - if (!currentlyInvertedFill) { - final hash = polygon.renderHashCode; - final opacity = polygon.color?.a ?? 0; - if (lastHash != hash || - (checkOpacity && opacity > 0 && opacity < 1)) { - drawPaths(); + // The hash is based on the polygons visual properties. If the hash from + // the current and the previous polygon no longer match, we need to flush + // the batch previous polygons. + // We also need to flush if the opacity is not 1 or 0, so that they get + // mixed properly. Otherwise, holes get cut, or colors aren't mixed, + // depending on the holes handler. + final hash = polygon.renderHashCode; + final opacity = polygon.color?.a ?? 0; + if (lastHash != hash || (checkOpacity && opacity > 0 && opacity < 1)) { + drawPaths(); + } + lastColor = polygon.color; + lastHash = hash; + + // First add fills and borders to path. + if (polygon.color != null) { + if (polygonTriangles != null) { + final len = polygonTriangles.length; + for (int i = 0; i < len; ++i) { + trianglePoints.add(fillOffsets[polygonTriangles[i]]); } - lastColor = polygon.color; - lastHash = hash; } else { - lastColor = invertedFill; + filledPath.addPolygon(fillOffsets, true); } + } - // First add fills and borders to path. - if (polygon.color != null || currentlyInvertedFill) { - if (polygonTriangles != null) { - final len = polygonTriangles.length; - for (int i = 0; i < len; ++i) { - trianglePoints.add(fillOffsets[polygonTriangles[i]]); - } - } else { - filledPath.addPolygon(fillOffsets, true); - } - } + void addBorderToPath(List offsets) => _addBorderToPath( + borderPath, + polygon, + offsets, + size, + canvas, + borderPaint!, + ); - void addBorderToPath(List offsets) => _addBorderToPath( - borderPath, - polygon, - offsets, - size, - canvas, - borderPaint!, - ); + if (borderPaint != null) { + addBorderToPath( + getOffsetsXY( + camera: camera, + origin: origin, + points: projectedPolygon.points, + shift: shift, + ), + ); + } - if (borderPaint != null && !currentlyInvertedFill) { - addBorderToPath( - getOffsetsXY( - camera: camera, - origin: origin, - points: projectedPolygon.points, - shift: shift, - ), + // Afterwards deal with more complicated holes. + // Improper handling of opacity and fill methods may result in normal + // polygons cutting holes into other polygons, when they should be mixing: + // https://github.com/fleaflet/flutter_map/issues/1898. + final holePointsList = polygon.holePointsList; + if (holePointsList != null && holePointsList.isNotEmpty) { + // See `Path.combine` comments below + // Avoids failing to cut holes if the winding directions of the holes + // and the normal points are the same + filledPath.fillType = PathFillType.evenOdd; + + for (final singleHolePoints in projectedPolygon.holePoints) { + final holeOffsets = getOffsetsXY( + camera: camera, + origin: origin, + points: singleHolePoints, + shift: shift, ); - } + filledPath.addPolygon(holeOffsets, true); - // Afterwards deal with more complicated holes. - // Improper handling of opacity and fill methods may result in normal - // polygons cutting holes into other polygons, when they should be mixing: - // https://github.com/fleaflet/flutter_map/issues/1898. - final holePointsList = polygon.holePointsList; - if (holePointsList != null && holePointsList.isNotEmpty) { - // See `Path.combine` comments below - // Avoids failing to cut holes if the winding directions of the holes - // and the normal points are the same - filledPath.fillType = PathFillType.evenOdd; + // TODO: Potentially more efficient and may change the need to do + // opacity checking - needs testing. Also need to verify if `xor` or + // `difference` is preferred. + // No longer blocked by lack of HTML support in Flutter 3.29 + /*filledPath = Path.combine( + PathOperation.xor, + filledPath, + Path()..addPolygon(holeOffsets, true), + );*/ + } + if (!polygon.disableHolesBorder && borderPaint != null) { for (final singleHolePoints in projectedPolygon.holePoints) { final holeOffsets = getOffsetsXY( camera: camera, @@ -386,63 +425,34 @@ class _PolygonPainter extends CustomPainter points: singleHolePoints, shift: shift, ); - filledPath.addPolygon(holeOffsets, true); - - // TODO: Potentially more efficient and may change the need to do - // opacity checking - needs testing. Also need to verify if `xor` or - // `difference` is preferred. - // No longer blocked by lack of HTML support in Flutter 3.29 - /*filledPath = Path.combine( - PathOperation.xor, - filledPath, - Path()..addPolygon(holeOffsets, true), - );*/ - } - - if (!polygon.disableHolesBorder && borderPaint != null) { - for (final singleHolePoints in projectedPolygon.holePoints) { - final holeOffsets = getOffsetsXY( - camera: camera, - origin: origin, - points: singleHolePoints, - shift: shift, - ); - addBorderToPath(holeOffsets); - } + addBorderToPath(holeOffsets); } } - - return WorldWorkControl.visible; } - workAcrossWorlds(drawIfVisible); - - if (!currentlyInvertedFill && - !drawLabelsLast && - polygonLabels && - polygon.textPainter != null) { - // Labels are expensive because: - // * they themselves cannot easily be pulled into our batched path - // painting with the given text APIs - // * therefore, they require us to flush the batch of polygon draws to - // ensure polygons and labels are stacked correctly, i.e.: - // p1, p1_label, p2, p2_label, ... . - - // The painter will be null if the layOuting algorithm determined that - // there isn't enough space. - workAcrossWorlds( - (double shift) => drawLabelIfVisible(shift, projectedPolygon), - ); - } - if (!currentlyInvertedFill) { - drawPaths(); - } + return WorldWorkControl.visible; } + workAcrossWorlds(drawIfVisible); + + if (!drawLabelsLast && polygonLabels && polygon.textPainter != null) { + // Labels are expensive because: + // * they themselves cannot easily be pulled into our batched path + // painting with the given text APIs + // * therefore, they require us to flush the batch of polygon draws to + // ensure polygons and labels are stacked correctly, i.e.: + // p1, p1_label, p2, p2_label, ... . + + // The painter will be null if the layOuting algorithm determined that + // there isn't enough space. + workAcrossWorlds( + (double shift) => drawLabelIfVisible(shift, projectedPolygon), + ); + } drawPaths(); } - if (!currentlyInvertedFill && polygonLabels && drawLabelsLast) { + if (polygonLabels && drawLabelsLast) { for (final projectedPolygon in polygons) { if (projectedPolygon.points.isEmpty) { continue; From 55f3a985f81bfbacbe4de304f010d48e0e688680 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Mon, 10 Mar 2025 20:41:52 +0000 Subject: [PATCH 06/18] Test `Path.combine` --- example/lib/pages/polygon.dart | 2 +- lib/src/layer/polygon_layer/painter.dart | 28 ++++++++++-------------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/example/lib/pages/polygon.dart b/example/lib/pages/polygon.dart index ed3782b42..293739a3e 100644 --- a/example/lib/pages/polygon.dart +++ b/example/lib/pages/polygon.dart @@ -321,7 +321,7 @@ class _PolygonPageState extends State { hitNotifier: _hitNotifier, simplificationTolerance: 0, // TODO temporarily, just for the tests - invertedFill: Colors.orangeAccent.withAlpha(64), + invertedFill: Colors.pink.withAlpha(255 ~/ 3 * 2), polygons: [..._polygonsRaw, ...?_hoverGons], ), ), diff --git a/lib/src/layer/polygon_layer/painter.dart b/lib/src/layer/polygon_layer/painter.dart index f63b85028..a398727d5 100644 --- a/lib/src/layer/polygon_layer/painter.dart +++ b/lib/src/layer/polygon_layer/painter.dart @@ -128,7 +128,7 @@ class _PolygonPainter extends CustomPainter final trianglePoints = []; - final filledPath = Path(); + Path filledPath = Path(); final borderPath = Path(); Color? lastColor; int? lastHash; @@ -265,7 +265,6 @@ class _PolygonPainter extends CustomPainter final minY = minMaxY[1].dy; final rect = Rect.fromLTRB(minX, minY, maxX, maxY); filledPath.addRect(rect); - filledPath.fillType = PathFillType.evenOdd; for (int i = 0; i <= polygons.length - 1; i++) { final projectedPolygon = polygons[i]; @@ -282,7 +281,12 @@ class _PolygonPainter extends CustomPainter if (!areOffsetsVisible(fillOffsets)) { return WorldWorkControl.invisible; } - filledPath.addPolygon(fillOffsets, true); + + filledPath = Path.combine( + PathOperation.difference, + filledPath, + Path()..addPolygon(fillOffsets, true), + ); return WorldWorkControl.visible; } @@ -392,11 +396,6 @@ class _PolygonPainter extends CustomPainter // https://github.com/fleaflet/flutter_map/issues/1898. final holePointsList = polygon.holePointsList; if (holePointsList != null && holePointsList.isNotEmpty) { - // See `Path.combine` comments below - // Avoids failing to cut holes if the winding directions of the holes - // and the normal points are the same - filledPath.fillType = PathFillType.evenOdd; - for (final singleHolePoints in projectedPolygon.holePoints) { final holeOffsets = getOffsetsXY( camera: camera, @@ -404,17 +403,12 @@ class _PolygonPainter extends CustomPainter points: singleHolePoints, shift: shift, ); - filledPath.addPolygon(holeOffsets, true); - - // TODO: Potentially more efficient and may change the need to do - // opacity checking - needs testing. Also need to verify if `xor` or - // `difference` is preferred. - // No longer blocked by lack of HTML support in Flutter 3.29 - /*filledPath = Path.combine( - PathOperation.xor, + + filledPath = Path.combine( + PathOperation.difference, filledPath, Path()..addPolygon(holeOffsets, true), - );*/ + ); } if (!polygon.disableHolesBorder && borderPaint != null) { From ee840808f19ced0c2b9082a18931c6b68d5327b8 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Mon, 10 Mar 2025 21:30:46 +0000 Subject: [PATCH 07/18] Updated polygons example to include more edge cases and reduce confusion --- example/lib/pages/polygon.dart | 190 ++++++++++++++++----------------- 1 file changed, 92 insertions(+), 98 deletions(-) diff --git a/example/lib/pages/polygon.dart b/example/lib/pages/polygon.dart index 293739a3e..772ce0c66 100644 --- a/example/lib/pages/polygon.dart +++ b/example/lib/pages/polygon.dart @@ -108,27 +108,26 @@ class _PolygonPageState extends State { pattern: const StrokePattern.dotted(), holePointsList: [ const [ - LatLng(52, -17), - LatLng(52, -16), - LatLng(51.5, -15.5), - LatLng(51, -16), - LatLng(51, -17), + LatLng(52, -9), + LatLng(52, -8), + LatLng(51.5, -7.5), + LatLng(51, -8), + LatLng(51, -9), ], const [ - LatLng(53.5, -17), - LatLng(53.5, -16), - LatLng(53, -15), - LatLng(52.25, -15), - LatLng(52.25, -16), - LatLng(52.75, -17), + LatLng(53.5, -9), + LatLng(53.5, -8), + LatLng(53, -7), + LatLng(52.25, -7), + LatLng(52.25, -8), + LatLng(52.75, -9), ], - ] - .map( - (latlngs) => latlngs - .map((latlng) => LatLng(latlng.latitude, latlng.longitude + 8)) - .toList(), - ) - .toList(), + 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), @@ -154,30 +153,26 @@ class _PolygonPageState extends State { pattern: const StrokePattern.dotted(), holePointsList: [ const [ - LatLng(52, -17), - LatLng(52, -16), - LatLng(51.5, -15.5), - LatLng(51, -16), - LatLng(51, -17), - ], + 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(53.5, -17), - LatLng(53.5, -16), - LatLng(53, -15), - LatLng(52.25, -15), - LatLng(52.25, -16), - LatLng(52.75, -17), - ], - ] - .map( - (latlngs) => latlngs - .map((latlng) => - LatLng(latlng.latitude - 6, latlng.longitude + 8)) - .toList() - .reversed // Test that holes are always cut, no matter winding - .toList(), - ) - .toList(), + 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), @@ -255,6 +250,35 @@ class _PolygonPageState extends State { "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: 'Red Line', + subtitle: 'Across the universe...', + ), + ), ]; late final _polygons = Map.fromEntries(_polygonsRaw.map((e) => MapEntry(e.hitValue, e))); @@ -330,35 +354,6 @@ class _PolygonPageState extends State { simplificationTolerance: 0, useAltRendering: true, polygons: [ - 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: 'Red Line', - subtitle: 'Across the universe...', - ), - ), Polygon( points: const [ LatLng(50, -18), @@ -387,45 +382,44 @@ class _PolygonPageState extends State { borderStrokeWidth: 4, borderColor: Colors.black, color: Colors.green, + label: + 'This one is performantly rendered\n& non-interactive', ), 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)) - .toList(), + LatLng(44, -18), + LatLng(47, -16), + LatLng(45.5, -12.5), + LatLng(48, -14), + LatLng(48, -18), + ], holePointsList: [ const [ - LatLng(52, -17), - LatLng(52, -16), - LatLng(51.5, -15.5), - LatLng(51, -16), - LatLng(51, -17), + LatLng(46, -17), + LatLng(46, -16), + LatLng(45.5, -15.5), + LatLng(45, -16), + LatLng(45, -17), ], const [ - LatLng(53.5, -17), - LatLng(53.5, -16), - LatLng(53, -15), - LatLng(52.25, -15), - LatLng(52.25, -16), - LatLng(52.75, -17), + LatLng(47.5, -17), + LatLng(47.5, -16), + LatLng(47, -15), + LatLng(46.25, -15), + LatLng(46.25, -16), + LatLng(46.75, -17), ], - ] - .map( - (latlngs) => latlngs - .map((latlng) => - LatLng(latlng.latitude - 6, latlng.longitude)) - .toList(), - ) - .toList(), + const [ + LatLng(46.683614, -16.141285), + LatLng(45.663083, -16.684529), + LatLng(45.913924, -15.2193), + ].reversed.toList(growable: false), + ], borderStrokeWidth: 4, borderColor: Colors.black, color: Colors.green, + label: + "Performant-rendering doesn't\nhandle malformed polygons", ), ], ), From 8806c38f7130062bc4a9bdcfe9a988f159b7ad8e Mon Sep 17 00:00:00 2001 From: monsieurtanuki Date: Thu, 13 Mar 2025 10:06:01 +0100 Subject: [PATCH 08/18] introduced test variables "useEvenOdd" and "invertedHoles" Impacted files: * `painter.dart`: introduced test variables `useEvenOdd` and `invertedHoles`; minor refactoring * `polygon.dart`: re-centered for consistent screenshots --- example/lib/pages/polygon.dart | 2 +- lib/src/layer/polygon_layer/painter.dart | 77 ++++++++++++++---------- 2 files changed, 47 insertions(+), 32 deletions(-) diff --git a/example/lib/pages/polygon.dart b/example/lib/pages/polygon.dart index 772ce0c66..cfb055fb9 100644 --- a/example/lib/pages/polygon.dart +++ b/example/lib/pages/polygon.dart @@ -292,7 +292,7 @@ class _PolygonPageState extends State { children: [ FlutterMap( options: const MapOptions( - initialCenter: LatLng(51.5, -0.09), + initialCenter: LatLng(51.5, -2), initialZoom: 5, ), children: [ diff --git a/lib/src/layer/polygon_layer/painter.dart b/lib/src/layer/polygon_layer/painter.dart index a398727d5..56c5d17c6 100644 --- a/lib/src/layer/polygon_layer/painter.dart +++ b/lib/src/layer/polygon_layer/painter.dart @@ -249,6 +249,30 @@ class _PolygonPainter extends CustomPainter return WorldWorkControl.visible; } + // Use evenOdd (true) or Path.combine (false) + // unfortunately Path.combine isn't stable on web + // TODO decide when to use what + bool useEvenOdd = true; + + // Do we also remove the holes from the inverted map? + // TODO probably should always be true + bool invertedHoles = true; + + print('path parameters: evenOdd $useEvenOdd, invertedHoles $invertedHoles'); + + void removePolygon(List offsets) { + if (useEvenOdd) { + filledPath.fillType = PathFillType.evenOdd; + filledPath.addPolygon(offsets, true); + return; + } + filledPath = Path.combine( + PathOperation.difference, + filledPath, + Path()..addPolygon(offsets, true), + ); + } + // Specific map treatment with `invertFill`. if (invertedFill != null) { filledPath.reset(); @@ -282,11 +306,19 @@ class _PolygonPainter extends CustomPainter return WorldWorkControl.invisible; } - filledPath = Path.combine( - PathOperation.difference, - filledPath, - Path()..addPolygon(fillOffsets, true), - ); + removePolygon(fillOffsets); + + if (invertedHoles) { + for (final singleHolePoints in projectedPolygon.holePoints) { + final holeOffsets = getOffsetsXY( + camera: camera, + origin: origin, + points: singleHolePoints, + shift: shift, + ); + removePolygon(holeOffsets); + } + } return WorldWorkControl.visible; } @@ -394,33 +426,16 @@ class _PolygonPainter extends CustomPainter // Improper handling of opacity and fill methods may result in normal // polygons cutting holes into other polygons, when they should be mixing: // https://github.com/fleaflet/flutter_map/issues/1898. - final holePointsList = polygon.holePointsList; - if (holePointsList != null && holePointsList.isNotEmpty) { - for (final singleHolePoints in projectedPolygon.holePoints) { - final holeOffsets = getOffsetsXY( - camera: camera, - origin: origin, - points: singleHolePoints, - shift: shift, - ); - - filledPath = Path.combine( - PathOperation.difference, - filledPath, - Path()..addPolygon(holeOffsets, true), - ); - } - + for (final singleHolePoints in projectedPolygon.holePoints) { + final holeOffsets = getOffsetsXY( + camera: camera, + origin: origin, + points: singleHolePoints, + shift: shift, + ); + removePolygon(holeOffsets); if (!polygon.disableHolesBorder && borderPaint != null) { - for (final singleHolePoints in projectedPolygon.holePoints) { - final holeOffsets = getOffsetsXY( - camera: camera, - origin: origin, - points: singleHolePoints, - shift: shift, - ); - addBorderToPath(holeOffsets); - } + addBorderToPath(holeOffsets); } } From f2c9ba551547d7a4891c2f2310406515c5288d91 Mon Sep 17 00:00:00 2001 From: monsieurtanuki Date: Thu, 13 Mar 2025 20:56:36 +0100 Subject: [PATCH 09/18] now filling holes --- lib/src/layer/polygon_layer/painter.dart | 30 ++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/lib/src/layer/polygon_layer/painter.dart b/lib/src/layer/polygon_layer/painter.dart index 56c5d17c6..02b9bbd60 100644 --- a/lib/src/layer/polygon_layer/painter.dart +++ b/lib/src/layer/polygon_layer/painter.dart @@ -252,13 +252,35 @@ class _PolygonPainter extends CustomPainter // Use evenOdd (true) or Path.combine (false) // unfortunately Path.combine isn't stable on web // TODO decide when to use what - bool useEvenOdd = true; + bool useEvenOdd = false; // Do we also remove the holes from the inverted map? // TODO probably should always be true bool invertedHoles = true; - print('path parameters: evenOdd $useEvenOdd, invertedHoles $invertedHoles'); + // Do we also fill the holes with inverted fill? + // TODO probably should always be true + bool fillInvertedHoles = true; + + print( + 'path parameters: evenOdd $useEvenOdd, invertedHoles $invertedHoles, fillInvertedHoles $fillInvertedHoles'); + + Path holePaths = Path(); + + void addPolygon(List offsets) { + if (!fillInvertedHoles) { + return; + } + if (useEvenOdd) { + holePaths.addPolygon(offsets, true); + return; + } + holePaths = Path.combine( + PathOperation.union, + holePaths, + Path()..addPolygon(offsets, true), + ); + } void removePolygon(List offsets) { if (useEvenOdd) { @@ -317,6 +339,7 @@ class _PolygonPainter extends CustomPainter shift: shift, ); removePolygon(holeOffsets); + addPolygon(holeOffsets); } } return WorldWorkControl.visible; @@ -331,6 +354,9 @@ class _PolygonPainter extends CustomPainter ..color = invertedFill!; canvas.drawPath(filledPath, paint); + if (fillInvertedHoles) { + canvas.drawPath(holePaths, paint); + } filledPath.reset(); } From 1ba0368aa8f0294b1a0045a8129de2ad482cc481 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sat, 15 Mar 2025 14:52:48 +0000 Subject: [PATCH 10/18] Made even/odd mode dependent on whether running on web Added logging for inverted filling when on web Added documentation to `PolygonLayer.invertedFill` Adjusted other logging Minor refactoring --- .../Flutter/ephemeral/flutter_lldb_helper.py | 32 ++++++++++++ .../ios/Flutter/ephemeral/flutter_lldbinit | 5 ++ lib/src/layer/polygon_layer/painter.dart | 50 ++++++++++--------- .../layer/polygon_layer/polygon_layer.dart | 25 +++++++++- lib/src/layer/tile_layer/tile_layer.dart | 15 +----- 5 files changed, 90 insertions(+), 37 deletions(-) create mode 100644 example/ios/Flutter/ephemeral/flutter_lldb_helper.py create mode 100644 example/ios/Flutter/ephemeral/flutter_lldbinit diff --git a/example/ios/Flutter/ephemeral/flutter_lldb_helper.py b/example/ios/Flutter/ephemeral/flutter_lldb_helper.py new file mode 100644 index 000000000..a88caf99d --- /dev/null +++ b/example/ios/Flutter/ephemeral/flutter_lldb_helper.py @@ -0,0 +1,32 @@ +# +# Generated file, do not edit. +# + +import lldb + +def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict): + """Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages.""" + base = frame.register["x0"].GetValueAsAddress() + page_len = frame.register["x1"].GetValueAsUnsigned() + + # Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the + # first page to see if handled it correctly. This makes diagnosing + # misconfiguration (e.g. missing breakpoint) easier. + data = bytearray(page_len) + data[0:8] = b'IHELPED!' + + error = lldb.SBError() + frame.GetThread().GetProcess().WriteMemory(base, data, error) + if not error.Success(): + print(f'Failed to write into {base}[+{page_len}]', error) + return + +def __lldb_init_module(debugger: lldb.SBDebugger, _): + target = debugger.GetDummyTarget() + # Caveat: must use BreakpointCreateByRegEx here and not + # BreakpointCreateByName. For some reasons callback function does not + # get carried over from dummy target for the later. + bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$") + bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__)) + bp.SetAutoContinue(True) + print("-- LLDB integration loaded --") diff --git a/example/ios/Flutter/ephemeral/flutter_lldbinit b/example/ios/Flutter/ephemeral/flutter_lldbinit new file mode 100644 index 000000000..e3ba6fbed --- /dev/null +++ b/example/ios/Flutter/ephemeral/flutter_lldbinit @@ -0,0 +1,5 @@ +# +# Generated file, do not edit. +# + +command script import --relative-to-command-file flutter_lldb_helper.py diff --git a/lib/src/layer/polygon_layer/painter.dart b/lib/src/layer/polygon_layer/painter.dart index 02b9bbd60..6a87c5673 100644 --- a/lib/src/layer/polygon_layer/painter.dart +++ b/lib/src/layer/polygon_layer/painter.dart @@ -121,6 +121,26 @@ class _PolygonPainter extends CustomPainter static const _minMaxLatitude = [LatLng(90, 0), LatLng(-90, 0)]; + /// Whether to use `PathFillType.evenOdd` (true) or `Path.combine` (false) + /// + /// * `Path.combine` doesn't work & isn't stable/consistent on web + /// * `evenOdd` gives broken results when polygons intersect when inverted + /// + /// The best option is to use `evenOdd` on web, as it at least works + /// sometimes, and `Path.combine` otherwise, as it gives correct results on + /// native platforms. + /// + /// See https://github.com/fleaflet/flutter_map/pull/2046. + static const _useEvenOdd = kIsWeb; + + // Do we also remove the holes from the inverted map? + // Should be `true` + static const _invertedHoles = true; + + // Do we also fill the holes with inverted fill? + // Should be `true` + static const _fillInvertedHoles = true; + @override void paint(Canvas canvas, Size size) { const checkOpacity = true; // for debugging purposes only, should be true @@ -249,29 +269,13 @@ class _PolygonPainter extends CustomPainter return WorldWorkControl.visible; } - // Use evenOdd (true) or Path.combine (false) - // unfortunately Path.combine isn't stable on web - // TODO decide when to use what - bool useEvenOdd = false; - - // Do we also remove the holes from the inverted map? - // TODO probably should always be true - bool invertedHoles = true; - - // Do we also fill the holes with inverted fill? - // TODO probably should always be true - bool fillInvertedHoles = true; - - print( - 'path parameters: evenOdd $useEvenOdd, invertedHoles $invertedHoles, fillInvertedHoles $fillInvertedHoles'); - Path holePaths = Path(); void addPolygon(List offsets) { - if (!fillInvertedHoles) { - return; - } - if (useEvenOdd) { + // ignore: dead_code + if (!_fillInvertedHoles) return; + + if (_useEvenOdd) { holePaths.addPolygon(offsets, true); return; } @@ -283,7 +287,7 @@ class _PolygonPainter extends CustomPainter } void removePolygon(List offsets) { - if (useEvenOdd) { + if (_useEvenOdd) { filledPath.fillType = PathFillType.evenOdd; filledPath.addPolygon(offsets, true); return; @@ -330,7 +334,7 @@ class _PolygonPainter extends CustomPainter removePolygon(fillOffsets); - if (invertedHoles) { + if (_invertedHoles) { for (final singleHolePoints in projectedPolygon.holePoints) { final holeOffsets = getOffsetsXY( camera: camera, @@ -354,7 +358,7 @@ class _PolygonPainter extends CustomPainter ..color = invertedFill!; canvas.drawPath(filledPath, paint); - if (fillInvertedHoles) { + if (_fillInvertedHoles) { canvas.drawPath(holePaths, paint); } diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index c48ec0f76..1b1c6b823 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -15,6 +15,7 @@ import 'package:flutter_map/src/misc/offsets.dart'; 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'; @@ -69,7 +70,14 @@ base class PolygonLayer /// Defaults to `false`. final bool drawLabelsLast; - /// Color to apply to the whole map - except for polygons + /// Color to apply to the map where not covered by a polygon + /// + /// > [!WARNING] + /// > On the web, inverted filling may not work as expected in some cases. + /// > It will not match the behaviour seen on native platforms. Avoid allowing + /// > polygons to intersect, and avoid using holes within polygons. + /// > This is due to multiple limitations/bugs within Flutter. See online + /// > documentation for more info. final Color? invertedFill; /// {@macro fm.lhn.layerHitNotifier.usage} @@ -97,6 +105,21 @@ class _PolygonLayerState extends State> with ProjectionSimplificationManagement<_ProjectedPolygon, Polygon, PolygonLayer> { + @override + void initState() { + if (kDebugMode && kIsWeb && widget.invertedFill != null) { + Logger(printer: PrettyPrinter(methodCount: 0)).w( + '\x1B[1m\x1B[3mflutter_map\x1B[0m\nOn the web, inverted filling may ' + 'not work as expected in some cases. It will not match the behaviour\n' + 'seen on native platforms.\nAvoid allowing polygons to intersect, and ' + 'avoid using holes within polygons.\nThis is due to multiple ' + 'limitations/bugs within Flutter. See online documentation for more ' + 'info.', + ); + } + super.initState(); + } + @override _ProjectedPolygon projectElement({ required Projection projection, diff --git a/lib/src/layer/tile_layer/tile_layer.dart b/lib/src/layer/tile_layer/tile_layer.dart index 0bee21485..f980257ac 100644 --- a/lib/src/layer/tile_layer/tile_layer.dart +++ b/lib/src/layer/tile_layer/tile_layer.dart @@ -277,21 +277,11 @@ class TileLayer extends StatefulWidget { tileProvider = tileProvider ?? NetworkTileProvider(), tileUpdateTransformer = tileUpdateTransformer ?? TileUpdateTransformers.ignoreTapEvents { - // Debug Logging - if (kDebugMode && - urlTemplate != null && - urlTemplate!.contains('{s}.tile.openstreetmap.org')) { - Logger(printer: PrettyPrinter(methodCount: 0)).w( - '\x1B[1m\x1B[3mflutter_map\x1B[0m\nAvoid using subdomains with OSM\'s tile ' - 'server. Support may be become slow or be removed in future.\nSee ' - 'https://github.com/openstreetmap/operations/issues/737 for more info.', - ); - } if (kDebugMode && retinaMode == null && urlTemplate != null && urlTemplate!.contains('{r}')) { - Logger(printer: PrettyPrinter(methodCount: 0)).w( + Logger(printer: PrettyPrinter(methodCount: 0)).i( '\x1B[1m\x1B[3mflutter_map\x1B[0m\nThe URL template includes a retina ' "mode placeholder ('{r}') to retrieve native high-resolution\ntiles, " 'which improve appearance especially on high-density displays.\n' @@ -299,8 +289,7 @@ class TileLayer extends StatefulWidget { 'will never retrieve these tiles.\nConsider using ' '`RetinaMode.isHighDensity` to toggle this property automatically, ' 'otherwise ensure\nit is set appropriately.\n' - 'See https://docs.fleaflet.dev/layers/tile-layer#retina-mode for ' - 'more info.', + 'See https://docs.fleaflet.dev/layers/tile-layer for more info.', ); } if (kDebugMode && kIsWeb && tileProvider is NetworkTileProvider?) { From b5344fe3ee31121fd1e4a011e4795ff5747e4eba Mon Sep 17 00:00:00 2001 From: Luka S Date: Sun, 16 Mar 2025 15:10:49 +0000 Subject: [PATCH 11/18] Delete example/ios/Flutter/ephemeral/flutter_lldb_helper.py --- .../Flutter/ephemeral/flutter_lldb_helper.py | 32 ------------------- 1 file changed, 32 deletions(-) delete mode 100644 example/ios/Flutter/ephemeral/flutter_lldb_helper.py diff --git a/example/ios/Flutter/ephemeral/flutter_lldb_helper.py b/example/ios/Flutter/ephemeral/flutter_lldb_helper.py deleted file mode 100644 index a88caf99d..000000000 --- a/example/ios/Flutter/ephemeral/flutter_lldb_helper.py +++ /dev/null @@ -1,32 +0,0 @@ -# -# Generated file, do not edit. -# - -import lldb - -def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict): - """Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages.""" - base = frame.register["x0"].GetValueAsAddress() - page_len = frame.register["x1"].GetValueAsUnsigned() - - # Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the - # first page to see if handled it correctly. This makes diagnosing - # misconfiguration (e.g. missing breakpoint) easier. - data = bytearray(page_len) - data[0:8] = b'IHELPED!' - - error = lldb.SBError() - frame.GetThread().GetProcess().WriteMemory(base, data, error) - if not error.Success(): - print(f'Failed to write into {base}[+{page_len}]', error) - return - -def __lldb_init_module(debugger: lldb.SBDebugger, _): - target = debugger.GetDummyTarget() - # Caveat: must use BreakpointCreateByRegEx here and not - # BreakpointCreateByName. For some reasons callback function does not - # get carried over from dummy target for the later. - bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$") - bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__)) - bp.SetAutoContinue(True) - print("-- LLDB integration loaded --") From ade2c0e6070665d10f065c7ce211d3e91124ae83 Mon Sep 17 00:00:00 2001 From: Luka S Date: Sun, 16 Mar 2025 15:11:01 +0000 Subject: [PATCH 12/18] Delete example/ios/Flutter/ephemeral/flutter_lldbinit --- example/ios/Flutter/ephemeral/flutter_lldbinit | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 example/ios/Flutter/ephemeral/flutter_lldbinit diff --git a/example/ios/Flutter/ephemeral/flutter_lldbinit b/example/ios/Flutter/ephemeral/flutter_lldbinit deleted file mode 100644 index e3ba6fbed..000000000 --- a/example/ios/Flutter/ephemeral/flutter_lldbinit +++ /dev/null @@ -1,5 +0,0 @@ -# -# Generated file, do not edit. -# - -command script import --relative-to-command-file flutter_lldb_helper.py From 9ba5d02b2668f6f1bb2fb744a11e2cdb8d24ae1c Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sun, 16 Mar 2025 15:15:09 +0000 Subject: [PATCH 13/18] Updated example app iOS config --- example/.metadata | 14 +- example/ios/.gitignore | 2 + example/ios/Flutter/AppFrameworkInfo.plist | 2 +- example/ios/Flutter/Debug.xcconfig | 1 - example/ios/Flutter/Release.xcconfig | 1 - example/ios/Podfile | 41 ---- example/ios/Runner.xcodeproj/project.pbxproj | 223 ++++++++++++------ .../xcshareddata/xcschemes/Runner.xcscheme | 24 +- .../contents.xcworkspacedata | 3 - example/ios/Runner/AppDelegate.swift | 4 +- .../AppIcon.appiconset/Icon-App-50x50@1x.png | Bin 1943 -> 0 bytes .../AppIcon.appiconset/Icon-App-50x50@2x.png | Bin 6588 -> 0 bytes .../AppIcon.appiconset/Icon-App-57x57@1x.png | Bin 2164 -> 0 bytes .../AppIcon.appiconset/Icon-App-57x57@2x.png | Bin 8092 -> 0 bytes .../AppIcon.appiconset/Icon-App-72x72@1x.png | Bin 3741 -> 0 bytes .../AppIcon.appiconset/Icon-App-72x72@2x.png | Bin 9132 -> 0 bytes example/ios/Runner/Info.plist | 8 +- example/ios/RunnerTests/RunnerTests.swift | 12 + 18 files changed, 197 insertions(+), 138 deletions(-) delete mode 100644 example/ios/Podfile delete mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png delete mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png delete mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png delete mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png delete mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png delete mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png create mode 100644 example/ios/RunnerTests/RunnerTests.swift diff --git a/example/.metadata b/example/.metadata index b8041ab28..31ae8bd5c 100644 --- a/example/.metadata +++ b/example/.metadata @@ -4,8 +4,8 @@ # This file should be version controlled and should not be manually edited. version: - revision: "53c27e519d33b4e13b01a8710b38a3591d6ca6f1" - channel: "beta" + revision: "463e7516406b4ba7814703616da0e475523db54c" + channel: "master" project_type: app @@ -13,11 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: 53c27e519d33b4e13b01a8710b38a3591d6ca6f1 - base_revision: 53c27e519d33b4e13b01a8710b38a3591d6ca6f1 - - platform: windows - create_revision: 53c27e519d33b4e13b01a8710b38a3591d6ca6f1 - base_revision: 53c27e519d33b4e13b01a8710b38a3591d6ca6f1 + create_revision: 463e7516406b4ba7814703616da0e475523db54c + base_revision: 463e7516406b4ba7814703616da0e475523db54c + - platform: ios + create_revision: 463e7516406b4ba7814703616da0e475523db54c + base_revision: 463e7516406b4ba7814703616da0e475523db54c # User provided section diff --git a/example/ios/.gitignore b/example/ios/.gitignore index e96ef602b..7a7f9873a 100644 --- a/example/ios/.gitignore +++ b/example/ios/.gitignore @@ -1,3 +1,4 @@ +**/dgph *.mode1v3 *.mode2v3 *.moved-aside @@ -18,6 +19,7 @@ Flutter/App.framework Flutter/Flutter.framework Flutter/Flutter.podspec Flutter/Generated.xcconfig +Flutter/ephemeral/ Flutter/app.flx Flutter/app.zip Flutter/flutter_assets/ diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 9367d483e..7c5696400 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 8.0 + 12.0 diff --git a/example/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig index ec97fc6f3..592ceee85 100644 --- a/example/ios/Flutter/Debug.xcconfig +++ b/example/ios/Flutter/Debug.xcconfig @@ -1,2 +1 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/example/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig index c4855bfe2..592ceee85 100644 --- a/example/ios/Flutter/Release.xcconfig +++ b/example/ios/Flutter/Release.xcconfig @@ -1,2 +1 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/example/ios/Podfile b/example/ios/Podfile deleted file mode 100644 index 88359b225..000000000 --- a/example/ios/Podfile +++ /dev/null @@ -1,41 +0,0 @@ -# Uncomment this line to define a global platform for your project -# platform :ios, '11.0' - -# CocoaPods analytics sends network stats synchronously affecting flutter build latency. -ENV['COCOAPODS_DISABLE_STATS'] = 'true' - -project 'Runner', { - 'Debug' => :debug, - 'Profile' => :release, - 'Release' => :release, -} - -def flutter_root - generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) - unless File.exist?(generated_xcode_build_settings_path) - raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" - end - - File.foreach(generated_xcode_build_settings_path) do |line| - matches = line.match(/FLUTTER_ROOT\=(.*)/) - return matches[1].strip if matches - end - raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" -end - -require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) - -flutter_ios_podfile_setup - -target 'Runner' do - use_frameworks! - use_modular_headers! - - flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) -end - -post_install do |installer| - installer.pods_project.targets.each do |target| - flutter_additional_ios_build_settings(target) - end -end diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index ab9fc9ecf..ee8e72be9 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,12 +3,12 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ - 127B2300FA5CE87549434CC2 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3A72EECE5117AE803156E244 /* Pods_Runner.framework */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; @@ -16,6 +16,16 @@ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; @@ -32,9 +42,9 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 3A72EECE5117AE803156E244 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 4928082F7E2BC0BADA4A6955 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; @@ -45,8 +55,6 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - CF9AFCBF84244C1D44C2E795 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - E1DF330ADC8450B8B567025E /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -54,30 +62,18 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 127B2300FA5CE87549434CC2 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 13FB0627975E846BA76A343C /* Pods */ = { + 331C8082294A63A400263BE5 /* RunnerTests */ = { isa = PBXGroup; children = ( - CF9AFCBF84244C1D44C2E795 /* Pods-Runner.debug.xcconfig */, - 4928082F7E2BC0BADA4A6955 /* Pods-Runner.release.xcconfig */, - E1DF330ADC8450B8B567025E /* Pods-Runner.profile.xcconfig */, + 331C807B294A618700263BE5 /* RunnerTests.swift */, ); - name = Pods; - path = Pods; - sourceTree = ""; - }; - 1DB0744A0D67D7FBC7C14ADA /* Frameworks */ = { - isa = PBXGroup; - children = ( - 3A72EECE5117AE803156E244 /* Pods_Runner.framework */, - ); - name = Frameworks; + path = RunnerTests; sourceTree = ""; }; 9740EEB11CF90186004384FC /* Flutter */ = { @@ -97,8 +93,7 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, - 13FB0627975E846BA76A343C /* Pods */, - 1DB0744A0D67D7FBC7C14ADA /* Frameworks */, + 331C8082294A63A400263BE5 /* RunnerTests */, ); sourceTree = ""; }; @@ -106,6 +101,7 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; @@ -128,18 +124,33 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 2655651DCB6AEB0882B90CC1 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - C1C85694F6D3CCB2E45E23F9 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -156,9 +167,14 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1020; + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 1100; @@ -179,11 +195,19 @@ projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -198,34 +222,14 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 2655651DCB6AEB0882B90CC1 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -236,6 +240,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -248,26 +253,17 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - C1C85694F6D3CCB2E45E23F9 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -279,6 +275,14 @@ }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -303,6 +307,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -332,6 +337,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -340,7 +346,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -358,8 +364,11 @@ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = me.jpryan.flutterMapExample; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.fleaflet.fluttermap.flutterMapExample; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -367,10 +376,58 @@ }; name = Profile; }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.fleaflet.fluttermap.flutterMapExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.fleaflet.fluttermap.flutterMapExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.fleaflet.fluttermap.flutterMapExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -400,6 +457,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -414,7 +472,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -426,6 +484,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -455,6 +514,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -463,11 +523,12 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -482,8 +543,11 @@ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = me.jpryan.flutterMapExample; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.fleaflet.fluttermap.flutterMapExample; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -501,8 +565,11 @@ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = me.jpryan.flutterMapExample; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.fleaflet.fluttermap.flutterMapExample; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -513,6 +580,16 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a28140cfd..e3773d42e 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ - - - - + + + + + + @@ -61,8 +73,6 @@ ReferencedContainer = "container:Runner.xcodeproj"> - - - - diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift index 70693e4a8..626664468 100644 --- a/example/ios/Runner/AppDelegate.swift +++ b/example/ios/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ -import UIKit import Flutter +import UIKit -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png deleted file mode 100644 index 861963cdac20bffab77d5e0fb7b19ef87fa45058..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1943 zcmV;I2Wa?-P)^h$4c|XaIp0X$yVOmOi#S`<}N-8;XY z^FQa#RzMT4wv}ZbH&#MJj1X0Cs-yQB+xB#H#C}AWXr>GI&UL#vpui~1GctO$DEsw- z?CB?e`~R>`RaDrGGC>$)?L`<94xO(o7c9NB;sj!SGUrZ=;`2lb$mZKpE-o%t6FQ7lvS1k|x3-a%%!nv-Kp4Yj7%M!wbbsNr z`lb+8>HMXHq6EU7kRBHNV_{Ok5Rye341|*T)f7|BnDYYf${Ze3q;$0*N_F;K!UZ1S z17n05M#C>pog`pPk+u)a)oUGV*1H7SDRbf>~=rabd)?Nywmwi&7EWg36SjyE~GGqrlnJhR*<<9>v?kL}lC0?p-u2JP9C10KLq#}~Z*#i`bU zS74Y1ONMZH&@&8bVSbv|YPQqn#j9I8V@VF8~&0}O2 z80n&Pw6~-`fAW?taxSQB`sMV%M{gse>5h$?`BaBGJGghd4io54YRpFd*!$26pzgrq zQTO4dh3A-CjfN$}y>EL9QM})Lfs&GwHf%z+bVNhW@HHHjTQ2D^g|LWWCC|VDK`(&1 z3mYHeq|+dvVaLRfkimUBup`6t$_~!vk|`0JucB}FE6o@6nLy`>@S|d^^+#S_8sZ1ctOg{Y)+I3K~xieMU z(hfffVI`s{CRL6q!FQPs{?0xnVZ6>789slT1L@u zx0Gj>>0lA33z!(2-wspNT!wXeU}@zJmKvK6t5{mG zD(T3fsE-8JK2B}+alq2?6T~TW^bE;rP4RdJMXwm14OT2P)*X!u7|G0%XU*1pP~uD+ z*7Q@*R2eia3}3=G_sq;mot^C?WP*X@Oyw^(hSx~}9Nzm)gNfyXX2talaFIC4$gNF+kp%*YzbX16?ZmT>m( z5LWi&v14h~9WXuAw?;o+Dx=8MY~Cm>8^+4WM7q+%(&+`Y-qKj6 zup0dXhkKi&e1?XyDb6TX$JA^d3l`MfJ1?;PR3FHL-05%1^cln~V@7e8*+RfxNv2!I z0;Wytq$TCiOufXk#j^mINW6bQ>iEp@*1G{I;Ny5g^j^iHu^@L_Uyzo@FoA#xN>3js zl8osyLwQV$@yjP%ns^JElIm{trd8 z#*vq-(KVRa9L5UOody%L7bGqpQFeEn8_MQA$*iuy%%*^D<}Q{A#p?x$tI6;F^2U{? zU4>EiwX!LUyPveQzo%)1eTrR(?i!HI&g&ihQSb5x-t-scM8_%dUXZw;HlJv0Sm!LP zNn`j7qfsZfkd?;B-4;(p;=SOO$aZ(0tLta@%w$d;g(XVZY92-Qbs+cn1TcjKK0AEl z%2N?O3kKzL|Nc7)YvZn-=T?_5o^d6~fLajrf;qr6j=5uMPuB(bU6Yo}%S05#9Xoe^ dP+J?h;6FQRX_6!9%9;QG002ovPDHLkV1jc~#o7P> diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png deleted file mode 100644 index 0d83bfc31a1df2a42536f9f7ddffb696531a546f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6588 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 ZB((MlkYx8gPk-S6l;t(#s$?ua|37t CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + flutter_map Demo CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -39,7 +41,9 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + diff --git a/example/ios/RunnerTests/RunnerTests.swift b/example/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 000000000..86a7c3b1b --- /dev/null +++ b/example/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} From fa471c34f574bbb222668d7dda48cb7b4343bb8e Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sun, 16 Mar 2025 15:58:55 +0000 Subject: [PATCH 14/18] Added toggle for inverted polygon fill to example app Minor refactoring --- example/lib/pages/polygon.dart | 75 +++++++++++++++++++++++- lib/src/layer/polygon_layer/painter.dart | 62 +++++++++++--------- 2 files changed, 108 insertions(+), 29 deletions(-) diff --git a/example/lib/pages/polygon.dart b/example/lib/pages/polygon.dart index cfb055fb9..02f646041 100644 --- a/example/lib/pages/polygon.dart +++ b/example/lib/pages/polygon.dart @@ -4,6 +4,7 @@ import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_example/misc/tile_providers.dart'; import 'package:flutter_map_example/widgets/drawer/menu_drawer.dart'; import 'package:latlong2/latlong.dart'; +import 'package:url_launcher/url_launcher.dart'; typedef HitValue = ({String title, String subtitle}); @@ -21,6 +22,8 @@ class _PolygonPageState extends State { List? _prevHitValues; List>? _hoverGons; + bool _useInvertedFill = false; + final _polygonsRaw = >[ Polygon( points: const [ @@ -344,8 +347,8 @@ class _PolygonPageState extends State { child: PolygonLayer( hitNotifier: _hitNotifier, simplificationTolerance: 0, - // TODO temporarily, just for the tests - invertedFill: Colors.pink.withAlpha(255 ~/ 3 * 2), + invertedFill: + _useInvertedFill ? Colors.pink.withAlpha(170) : null, polygons: [..._polygonsRaw, ...?_hoverGons], ), ), @@ -425,6 +428,74 @@ class _PolygonPageState extends State { ), ], ), + Positioned( + top: 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: 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), + ), + ], + ), + ), + if (kIsWeb) + ColoredBox( + color: Colors.amber, + child: Padding( + 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/lib/src/layer/polygon_layer/painter.dart b/lib/src/layer/polygon_layer/painter.dart index 6a87c5673..444401edb 100644 --- a/lib/src/layer/polygon_layer/painter.dart +++ b/lib/src/layer/polygon_layer/painter.dart @@ -8,6 +8,9 @@ class _PolygonPainter extends CustomPainter /// Reference to the list of [_ProjectedPolygon]s final List<_ProjectedPolygon> polygons; + @override + Iterable<_ProjectedPolygon> get elements => polygons; + /// Triangulated [polygons] if available /// /// Expected to be in same/corresponding order as [polygons]. @@ -56,6 +59,36 @@ class _PolygonPainter extends CustomPainter required this.hitNotifier, }) : bounds = camera.visibleBounds; + // Corner coordinates of the polygon painted onto the entire world when using + // inverted fill. + static const _minMaxLatitude = [LatLng(90, 0), LatLng(-90, 0)]; + + // Whether to use `PathFillType.evenOdd` (true) or `Path.combine` (false). + // + // * `Path.combine` doesn't work & isn't stable/consistent on web + // * `evenOdd` gives broken results when polygons intersect when inverted + // + // The best option is to use `evenOdd` on web, as it at least works sometimes, + // and `Path.combine` otherwise, as it gives correct results on native + // platforms. + // + // See https://github.com/fleaflet/flutter_map/pull/2046. + static const _useEvenOdd = kIsWeb; + + // Do we also remove the holes from the inverted map? + // Should be `true`. + static const _invertedHoles = true; + + // Do we also fill the holes with inverted fill? + // Should be `true`. + static const _fillInvertedHoles = true; + + // Whether to draw the batch of polygons when a polygon with translucency is + // encountered. + // Should be `true`. + // TODO: Verify if still necessary. + static const _flushBatchOnTranslucency = true; + @override bool elementHitTest( _ProjectedPolygon projectedPolygon, { @@ -116,34 +149,8 @@ class _PolygonPainter extends CustomPainter return workAcrossWorlds(checkIfHit); } - @override - Iterable<_ProjectedPolygon> get elements => polygons; - - static const _minMaxLatitude = [LatLng(90, 0), LatLng(-90, 0)]; - - /// Whether to use `PathFillType.evenOdd` (true) or `Path.combine` (false) - /// - /// * `Path.combine` doesn't work & isn't stable/consistent on web - /// * `evenOdd` gives broken results when polygons intersect when inverted - /// - /// The best option is to use `evenOdd` on web, as it at least works - /// sometimes, and `Path.combine` otherwise, as it gives correct results on - /// native platforms. - /// - /// See https://github.com/fleaflet/flutter_map/pull/2046. - static const _useEvenOdd = kIsWeb; - - // Do we also remove the holes from the inverted map? - // Should be `true` - static const _invertedHoles = true; - - // Do we also fill the holes with inverted fill? - // Should be `true` - static const _fillInvertedHoles = true; - @override void paint(Canvas canvas, Size size) { - const checkOpacity = true; // for debugging purposes only, should be true super.paint(canvas, size); final trianglePoints = []; @@ -414,7 +421,8 @@ class _PolygonPainter extends CustomPainter // depending on the holes handler. final hash = polygon.renderHashCode; final opacity = polygon.color?.a ?? 0; - if (lastHash != hash || (checkOpacity && opacity > 0 && opacity < 1)) { + if (lastHash != hash || + (_flushBatchOnTranslucency && opacity > 0 && opacity < 1)) { drawPaths(); } lastColor = polygon.color; From c579ffdc021bc37a4eb77062a94051f1f9de4812 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sun, 16 Mar 2025 16:10:46 +0000 Subject: [PATCH 15/18] Minor formatting fix --- lib/src/layer/polygon_layer/polygon_layer.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index 1b1c6b823..f238395a2 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -19,11 +19,8 @@ import 'package:logger/logger.dart'; import 'package:polylabel/polylabel.dart'; part 'label.dart'; - part 'painter.dart'; - part 'polygon.dart'; - part 'projected_polygon.dart'; /// A polygon layer for [FlutterMap]. From f373f7e559b6aac6f33e0aaec0a62438def96f3f Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sun, 16 Mar 2025 16:35:56 +0000 Subject: [PATCH 16/18] Make `evenOdd` dependent on `invertedFill` on non-web (for performance reasons) Applied review suggestions --- lib/src/layer/polygon_layer/painter.dart | 46 ++++++++++++------------ 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/lib/src/layer/polygon_layer/painter.dart b/lib/src/layer/polygon_layer/painter.dart index 444401edb..88de18c65 100644 --- a/lib/src/layer/polygon_layer/painter.dart +++ b/lib/src/layer/polygon_layer/painter.dart @@ -59,33 +59,34 @@ class _PolygonPainter extends CustomPainter required this.hitNotifier, }) : bounds = camera.visibleBounds; - // Corner coordinates of the polygon painted onto the entire world when using - // inverted fill. + /// Corner coordinates of the polygon painted onto the entire world when using + /// inverted fill. static const _minMaxLatitude = [LatLng(90, 0), LatLng(-90, 0)]; - // Whether to use `PathFillType.evenOdd` (true) or `Path.combine` (false). - // - // * `Path.combine` doesn't work & isn't stable/consistent on web - // * `evenOdd` gives broken results when polygons intersect when inverted - // - // The best option is to use `evenOdd` on web, as it at least works sometimes, - // and `Path.combine` otherwise, as it gives correct results on native - // platforms. - // - // See https://github.com/fleaflet/flutter_map/pull/2046. - static const _useEvenOdd = kIsWeb; - - // Do we also remove the holes from the inverted map? - // Should be `true`. + /// Whether to use `PathFillType.evenOdd` (true) or `Path.combine` (false). + /// + /// * `Path.combine` doesn't work & isn't stable/consistent on web + /// * `evenOdd` gives broken results when polygons intersect when inverted + /// * `Path.combine` has slightly worse performance than `evenOdd` + /// + /// The best option is to use `evenOdd` on web, as it at least works + /// sometimes. On native, we use `Path.combine` when inverted filling, or + /// `evenOdd` otherwise. + /// + /// See https://github.com/fleaflet/flutter_map/pull/2046. + late final _useEvenOdd = kIsWeb || invertedFill == null; + + /// Do we also remove the holes from the inverted map? + /// Should be `true`. static const _invertedHoles = true; - // Do we also fill the holes with inverted fill? - // Should be `true`. + /// Do we also fill the holes with inverted fill? + /// Should be `true`. static const _fillInvertedHoles = true; - // Whether to draw the batch of polygons when a polygon with translucency is - // encountered. - // Should be `true`. + /// Whether to draw the batch of polygons when a polygon with translucency is + /// encountered. + /// Should be `true`. // TODO: Verify if still necessary. static const _flushBatchOnTranslucency = true; @@ -161,7 +162,7 @@ class _PolygonPainter extends CustomPainter int? lastHash; Paint? borderPaint; - // Draw polygon outline + /// Draw polygon outline void drawBorders() { if (borderPaint != null) { canvas.drawPath(borderPath, borderPaint); @@ -279,6 +280,7 @@ class _PolygonPainter extends CustomPainter Path holePaths = Path(); void addPolygon(List offsets) { + // For debugging purposes, should be compiled out // ignore: dead_code if (!_fillInvertedHoles) return; From 5ad7bd5f92ec664cff14ffb2f17f803a1985d379 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sun, 16 Mar 2025 16:51:09 +0000 Subject: [PATCH 17/18] Added links to online documentation where necessary Fixed console warning logging --- .../layer/polygon_layer/polygon_layer.dart | 19 ++++++++++++------- .../layer/polyline_layer/polyline_layer.dart | 5 +++-- .../layer_hit_notifier.dart | 2 +- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index f238395a2..dfddb6d31 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -73,8 +73,9 @@ base class PolygonLayer /// > On the web, inverted filling may not work as expected in some cases. /// > It will not match the behaviour seen on native platforms. Avoid allowing /// > polygons to intersect, and avoid using holes within polygons. - /// > This is due to multiple limitations/bugs within Flutter. See online - /// > documentation for more info. + /// > This is due to multiple limitations/bugs within Flutter. See the + /// > [online documentation](docs.fleaflet.dev/layers/polygon-layer#inverted-filling) + /// > for more info. final Color? invertedFill; /// {@macro fm.lhn.layerHitNotifier.usage} @@ -103,18 +104,22 @@ class _PolygonLayerState extends State> ProjectionSimplificationManagement<_ProjectedPolygon, Polygon, PolygonLayer> { @override - void initState() { - if (kDebugMode && kIsWeb && widget.invertedFill != null) { + void didUpdateWidget(covariant PolygonLayer oldWidget) { + super.didUpdateWidget(oldWidget); + + if (kDebugMode && + kIsWeb && + oldWidget.invertedFill == null && + widget.invertedFill != null) { Logger(printer: PrettyPrinter(methodCount: 0)).w( '\x1B[1m\x1B[3mflutter_map\x1B[0m\nOn the web, inverted filling may ' 'not work as expected in some cases. It will not match the behaviour\n' 'seen on native platforms.\nAvoid allowing polygons to intersect, and ' 'avoid using holes within polygons.\nThis is due to multiple ' - 'limitations/bugs within Flutter. See online documentation for more ' - 'info.', + 'limitations/bugs within Flutter.\nSee ' + 'https://docs.fleaflet.dev/layers/polyline-layer#culling for more info.', ); } - super.initState(); } @override diff --git a/lib/src/layer/polyline_layer/polyline_layer.dart b/lib/src/layer/polyline_layer/polyline_layer.dart index f46b981b4..75d7df43d 100644 --- a/lib/src/layer/polyline_layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer/polyline_layer.dart @@ -29,8 +29,9 @@ base class PolylineLayer /// Acceptable extent outside of viewport before culling polyline segments /// /// May need to be increased if the [Polyline.strokeWidth] + - /// [Polyline.borderStrokeWidth] is large. See online documentation for more - /// information. + /// [Polyline.borderStrokeWidth] is large. See the + /// [online documentation](https://docs.fleaflet.dev/layers/polyline-layer#culling) + /// for more info. /// /// Defaults to 10. Set to `null` to disable culling. final double? cullingMargin; diff --git a/lib/src/layer/shared/layer_interactivity/layer_hit_notifier.dart b/lib/src/layer/shared/layer_interactivity/layer_hit_notifier.dart index 076d118bc..50b2d1474 100644 --- a/lib/src/layer/shared/layer_interactivity/layer_hit_notifier.dart +++ b/lib/src/layer/shared/layer_interactivity/layer_hit_notifier.dart @@ -21,7 +21,7 @@ typedef LayerHitNotifier = ValueNotifier?>; /// /// Hit testing still occurs even if this is `null`. /// -/// See online documentation for more detailed usage instructions. See the +/// See the online documentation for more detailed usage instructions. See the /// example project for an example implementation. /// {@endtemplate} // ignore: unused_element, constant_identifier_names From f9fd0c3f679187081a355cc09cfe28c61508073e Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sun, 16 Mar 2025 19:28:04 +0000 Subject: [PATCH 18/18] Minor renaming --- lib/src/layer/polygon_layer/painter.dart | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/lib/src/layer/polygon_layer/painter.dart b/lib/src/layer/polygon_layer/painter.dart index 88de18c65..8406fa299 100644 --- a/lib/src/layer/polygon_layer/painter.dart +++ b/lib/src/layer/polygon_layer/painter.dart @@ -157,6 +157,7 @@ class _PolygonPainter extends CustomPainter final trianglePoints = []; Path filledPath = Path(); + Path invertedHolePaths = Path(); final borderPath = Path(); Color? lastColor; int? lastHash; @@ -277,25 +278,23 @@ class _PolygonPainter extends CustomPainter return WorldWorkControl.visible; } - Path holePaths = Path(); - - void addPolygon(List offsets) { + void invertFillPolygonHole(List offsets) { // For debugging purposes, should be compiled out // ignore: dead_code if (!_fillInvertedHoles) return; if (_useEvenOdd) { - holePaths.addPolygon(offsets, true); + invertedHolePaths.addPolygon(offsets, true); return; } - holePaths = Path.combine( + invertedHolePaths = Path.combine( PathOperation.union, - holePaths, + invertedHolePaths, Path()..addPolygon(offsets, true), ); } - void removePolygon(List offsets) { + void unfillPolygon(List offsets) { if (_useEvenOdd) { filledPath.fillType = PathFillType.evenOdd; filledPath.addPolygon(offsets, true); @@ -341,7 +340,7 @@ class _PolygonPainter extends CustomPainter return WorldWorkControl.invisible; } - removePolygon(fillOffsets); + unfillPolygon(fillOffsets); if (_invertedHoles) { for (final singleHolePoints in projectedPolygon.holePoints) { @@ -351,8 +350,8 @@ class _PolygonPainter extends CustomPainter points: singleHolePoints, shift: shift, ); - removePolygon(holeOffsets); - addPolygon(holeOffsets); + unfillPolygon(holeOffsets); + invertFillPolygonHole(holeOffsets); } } return WorldWorkControl.visible; @@ -368,7 +367,7 @@ class _PolygonPainter extends CustomPainter canvas.drawPath(filledPath, paint); if (_fillInvertedHoles) { - canvas.drawPath(holePaths, paint); + canvas.drawPath(invertedHolePaths, paint); } filledPath.reset(); @@ -473,7 +472,7 @@ class _PolygonPainter extends CustomPainter points: singleHolePoints, shift: shift, ); - removePolygon(holeOffsets); + unfillPolygon(holeOffsets); if (!polygon.disableHolesBorder && borderPaint != null) { addBorderToPath(holeOffsets); }