From d590be261f81bc4cff61397b54cc8bd0fea92dad Mon Sep 17 00:00:00 2001 From: Leon Date: Tue, 3 Jun 2025 09:44:10 -0300 Subject: [PATCH 01/29] feat: parameters to use marker in meters --- lib/src/layer/marker_layer/marker.dart | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/src/layer/marker_layer/marker.dart b/lib/src/layer/marker_layer/marker.dart index 764770342..95358cfc9 100644 --- a/lib/src/layer/marker_layer/marker.dart +++ b/lib/src/layer/marker_layer/marker.dart @@ -48,6 +48,18 @@ class Marker { /// marker. Use a widget inside [child] to perform this. final bool? rotate; + /// Parameter to enable or not the feature to use markers dimensions in meters. + /// + /// A good way to use that feature is using a LayoutBuilder and building according the + /// maxHeight and minWidth values. + final bool useSizeInMeters; + + /// TODO: Documentation + final double? maxWidthUsingMetersPixels; + final double? maxHeightUsingMetersPixels; + final double? minWidthUsingMetersPixels; + final double? minHeightUsingMetersPixels; + /// Creates a container for a [child] widget located at a geographic coordinate /// [point] /// @@ -61,6 +73,11 @@ class Marker { this.height = 30, this.alignment, this.rotate, + this.useSizeInMeters = false, + this.maxWidthUsingMetersPixels, + this.maxHeightUsingMetersPixels, + this.minHeightUsingMetersPixels, + this.minWidthUsingMetersPixels, }); /// Returns the alignment of a [width]x[height] rectangle by [left]x[top] pixels. From 3bc637f73d74d9f97ba3b4862a72a2b782a314f9 Mon Sep 17 00:00:00 2001 From: Leon Date: Tue, 3 Jun 2025 10:14:08 -0300 Subject: [PATCH 02/29] refactor: change positioned left, top, bottom and right variables creating that only in the needed function --- lib/src/layer/marker_layer/marker_layer.dart | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/src/layer/marker_layer/marker_layer.dart b/lib/src/layer/marker_layer/marker_layer.dart index fe4693d46..32ed910d0 100644 --- a/lib/src/layer/marker_layer/marker_layer.dart +++ b/lib/src/layer/marker_layer/marker_layer.dart @@ -47,19 +47,19 @@ class MarkerLayer extends StatelessWidget { child: Stack( children: (List markers) sync* { for (final m in markers) { - // Resolve real alignment - // TODO: maybe just using Size, Offset, and Rect? - final left = 0.5 * m.width * ((m.alignment ?? alignment).x + 1); - final top = 0.5 * m.height * ((m.alignment ?? alignment).y + 1); - final right = m.width - left; - final bottom = m.height - top; - // Perform projection final pxPoint = map.projectAtZoom(m.point); Positioned? getPositioned(double worldShift) { final shiftedX = pxPoint.dx + worldShift; + // Resolve real alignment + // TODO: maybe just using Size, Offset, and Rect? + final left = 0.5 * m.width * ((m.alignment ?? alignment).x + 1); + final top = 0.5 * m.height * ((m.alignment ?? alignment).y + 1); + final right = m.width - left; + final bottom = m.height - top; + // Cull if out of bounds if (!map.pixelBounds.overlaps( Rect.fromPoints( From f1f8394807639112da5920e5f01eb4f0712e2c42 Mon Sep 17 00:00:00 2001 From: Leon Date: Tue, 3 Jun 2025 10:56:29 -0300 Subject: [PATCH 03/29] feat: width and height according the using meters implementation --- lib/src/layer/marker_layer/marker_layer.dart | 51 +++++++++++++++++--- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/lib/src/layer/marker_layer/marker_layer.dart b/lib/src/layer/marker_layer/marker_layer.dart index 32ed910d0..91d230940 100644 --- a/lib/src/layer/marker_layer/marker_layer.dart +++ b/lib/src/layer/marker_layer/marker_layer.dart @@ -53,12 +53,51 @@ class MarkerLayer extends StatelessWidget { Positioned? getPositioned(double worldShift) { final shiftedX = pxPoint.dx + worldShift; + double height = m.height; + double width = m.width; + + if (m.useSizeInMeters) { + final basePoint = m.point; + final baseOffset = map.getOffsetFromOrigin(basePoint); + final rHeight = + const Distance().offset(basePoint, height / 2, 0); + final rWidth = const Distance().offset(basePoint, width / 2, 0); + + height = + (baseOffset - map.getOffsetFromOrigin(rHeight)).distance * + 2; + width = + (baseOffset - map.getOffsetFromOrigin(rWidth)).distance * 2; + + final maxHeightUsingMetersPixels = m.maxHeightUsingMetersPixels; + final maxWidthUsingMetersPixels = m.maxWidthUsingMetersPixels; + if (maxHeightUsingMetersPixels != null && + height > maxHeightUsingMetersPixels) { + height = maxHeightUsingMetersPixels; + } + if (maxWidthUsingMetersPixels != null && + width > maxWidthUsingMetersPixels) { + width = maxWidthUsingMetersPixels; + } + + final minHeightUsingMetersPixels = m.minHeightUsingMetersPixels; + final minWidthUsingMetersPixels = m.minWidthUsingMetersPixels; + if (minHeightUsingMetersPixels != null && + height < minHeightUsingMetersPixels) { + height = minHeightUsingMetersPixels; + } + if (minWidthUsingMetersPixels != null && + width < minWidthUsingMetersPixels) { + width = minWidthUsingMetersPixels; + } + } + // Resolve real alignment // TODO: maybe just using Size, Offset, and Rect? - final left = 0.5 * m.width * ((m.alignment ?? alignment).x + 1); - final top = 0.5 * m.height * ((m.alignment ?? alignment).y + 1); - final right = m.width - left; - final bottom = m.height - top; + final left = 0.5 * width * ((m.alignment ?? alignment).x + 1); + final top = 0.5 * height * ((m.alignment ?? alignment).y + 1); + final right = width - left; + final bottom = height - top; // Cull if out of bounds if (!map.pixelBounds.overlaps( @@ -77,8 +116,8 @@ class MarkerLayer extends StatelessWidget { return Positioned( key: m.key, - width: m.width, - height: m.height, + width: width, + height: height, left: shiftedLocalPoint.dx - right, top: shiftedLocalPoint.dy - bottom, child: (m.rotate ?? rotate) From 5f88ba825fdd514c0703f063730f31a4dec15ff5 Mon Sep 17 00:00:00 2001 From: Leon Date: Tue, 3 Jun 2025 11:34:46 -0300 Subject: [PATCH 04/29] feat: marker using size in meters example --- example/lib/pages/markers.dart | 44 ++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/example/lib/pages/markers.dart b/example/lib/pages/markers.dart index 2f652096b..d71a0d3d7 100644 --- a/example/lib/pages/markers.dart +++ b/example/lib/pages/markers.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_example/misc/tile_providers.dart'; @@ -117,8 +119,11 @@ class _MarkerPageState extends State { Flexible( child: FlutterMap( options: MapOptions( - initialCenter: const LatLng(51.5, -0.09), - initialZoom: 5, + initialCenter: const LatLng( + 51.51868093513547, + -0.12835376940892318, + ), + initialZoom: 15, onTap: (_, p) => setState(() => customMarkers.add(buildPin(p))), interactionOptions: const InteractionOptions( flags: ~InteractiveFlag.doubleTapZoom, @@ -167,6 +172,41 @@ class _MarkerPageState extends State { rotate: counterRotate, alignment: selectedAlignment, ), + MarkerLayer( + markers: [ + Marker( + point: const LatLng( + 51.51868093513547, + -0.12835376940892318, + ), + height: 20, + width: 20, + maxHeightUsingMetersPixels: 200, + maxWidthUsingMetersPixels: 200, + child: LayoutBuilder( + builder: (context, constraints) { + final minDimension = min( + constraints.maxHeight, + constraints.maxWidth, + ); + + return Transform.scale( + scale: minDimension / 30, + child: const SizedBox( + width: 30, + height: 30, + child: Icon( + Icons.map, + color: Colors.amber, + ), + ), + ); + }, + ), + useSizeInMeters: true, + ), + ], + ), ], ), ), From dbb1f99b6297d197054efcb12ae4091f3e467da1 Mon Sep 17 00:00:00 2001 From: Leon Date: Tue, 3 Jun 2025 12:10:10 -0300 Subject: [PATCH 05/29] feat: documentation of the new parameters --- lib/src/layer/marker_layer/marker.dart | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/src/layer/marker_layer/marker.dart b/lib/src/layer/marker_layer/marker.dart index 95358cfc9..9e724c987 100644 --- a/lib/src/layer/marker_layer/marker.dart +++ b/lib/src/layer/marker_layer/marker.dart @@ -54,10 +54,24 @@ class Marker { /// maxHeight and minWidth values. final bool useSizeInMeters; - /// TODO: Documentation + /// Parameter to control the max width in pixels of the marker region when the parameter + /// [useSizeInMeters] is enabled. That size is optional and when existent will limited + /// the marker size pixel width final double? maxWidthUsingMetersPixels; + + /// Parameter to control the max height in pixels of the marker region when the parameter + /// [useSizeInMeters] is enabled. That size is optional and when existent will limited + /// the marker size pixel height final double? maxHeightUsingMetersPixels; + + /// Parameter to control the min width in pixels of the marker region when the parameter + /// [useSizeInMeters] is enabled. That size is optional and when existent will be the + /// minimal width for the marker pixel region final double? minWidthUsingMetersPixels; + + /// Parameter to control the min height in pixels of the marker region when the parameter + /// [useSizeInMeters] is enabled. That size is optional and when existent will be the + /// minimal height for the marker pixel region final double? minHeightUsingMetersPixels; /// Creates a container for a [child] widget located at a geographic coordinate From 35e1f11787777c259538a5659bcf4c6e2265194b Mon Sep 17 00:00:00 2001 From: Leon Date: Mon, 9 Jun 2025 13:33:44 -0300 Subject: [PATCH 06/29] refactor: marker parameters to control the min and max height and width in a box contraint to avoid so many parameters --- example/lib/pages/markers.dart | 6 ++-- lib/src/layer/marker_layer/marker.dart | 28 +++------------- lib/src/layer/marker_layer/marker_layer.dart | 35 +++++++++----------- 3 files changed, 24 insertions(+), 45 deletions(-) diff --git a/example/lib/pages/markers.dart b/example/lib/pages/markers.dart index d71a0d3d7..22dad83a1 100644 --- a/example/lib/pages/markers.dart +++ b/example/lib/pages/markers.dart @@ -181,8 +181,10 @@ class _MarkerPageState extends State { ), height: 20, width: 20, - maxHeightUsingMetersPixels: 200, - maxWidthUsingMetersPixels: 200, + boxConstraintsUsingMetersInPixels: const BoxConstraints( + maxHeight: 200, + maxWidth: 200, + ), child: LayoutBuilder( builder: (context, constraints) { final minDimension = min( diff --git a/lib/src/layer/marker_layer/marker.dart b/lib/src/layer/marker_layer/marker.dart index 9e724c987..f66a8c227 100644 --- a/lib/src/layer/marker_layer/marker.dart +++ b/lib/src/layer/marker_layer/marker.dart @@ -54,25 +54,10 @@ class Marker { /// maxHeight and minWidth values. final bool useSizeInMeters; - /// Parameter to control the max width in pixels of the marker region when the parameter - /// [useSizeInMeters] is enabled. That size is optional and when existent will limited - /// the marker size pixel width - final double? maxWidthUsingMetersPixels; - - /// Parameter to control the max height in pixels of the marker region when the parameter - /// [useSizeInMeters] is enabled. That size is optional and when existent will limited - /// the marker size pixel height - final double? maxHeightUsingMetersPixels; - - /// Parameter to control the min width in pixels of the marker region when the parameter - /// [useSizeInMeters] is enabled. That size is optional and when existent will be the - /// minimal width for the marker pixel region - final double? minWidthUsingMetersPixels; - - /// Parameter to control the min height in pixels of the marker region when the parameter - /// [useSizeInMeters] is enabled. That size is optional and when existent will be the - /// minimal height for the marker pixel region - final double? minHeightUsingMetersPixels; + /// Parameter to control the box size when the parameter [useSizeInMeters] is enabled. + /// That BoxConstraints is optional and when exists control the minimal and maximal size + /// in pixels of that region created by the map visible region in meters. + final BoxConstraints? boxConstraintsUsingMetersInPixels; /// Creates a container for a [child] widget located at a geographic coordinate /// [point] @@ -88,10 +73,7 @@ class Marker { this.alignment, this.rotate, this.useSizeInMeters = false, - this.maxWidthUsingMetersPixels, - this.maxHeightUsingMetersPixels, - this.minHeightUsingMetersPixels, - this.minWidthUsingMetersPixels, + this.boxConstraintsUsingMetersInPixels, }); /// Returns the alignment of a [width]x[height] rectangle by [left]x[top] pixels. diff --git a/lib/src/layer/marker_layer/marker_layer.dart b/lib/src/layer/marker_layer/marker_layer.dart index 91d230940..6489829e0 100644 --- a/lib/src/layer/marker_layer/marker_layer.dart +++ b/lib/src/layer/marker_layer/marker_layer.dart @@ -69,26 +69,21 @@ class MarkerLayer extends StatelessWidget { width = (baseOffset - map.getOffsetFromOrigin(rWidth)).distance * 2; - final maxHeightUsingMetersPixels = m.maxHeightUsingMetersPixels; - final maxWidthUsingMetersPixels = m.maxWidthUsingMetersPixels; - if (maxHeightUsingMetersPixels != null && - height > maxHeightUsingMetersPixels) { - height = maxHeightUsingMetersPixels; - } - if (maxWidthUsingMetersPixels != null && - width > maxWidthUsingMetersPixels) { - width = maxWidthUsingMetersPixels; - } - - final minHeightUsingMetersPixels = m.minHeightUsingMetersPixels; - final minWidthUsingMetersPixels = m.minWidthUsingMetersPixels; - if (minHeightUsingMetersPixels != null && - height < minHeightUsingMetersPixels) { - height = minHeightUsingMetersPixels; - } - if (minWidthUsingMetersPixels != null && - width < minWidthUsingMetersPixels) { - width = minWidthUsingMetersPixels; + final boxConstraintsUsingMetersInPixels = + m.boxConstraintsUsingMetersInPixels; + if (boxConstraintsUsingMetersInPixels != null) { + if (height > boxConstraintsUsingMetersInPixels.maxHeight) { + height = boxConstraintsUsingMetersInPixels.maxHeight; + } + if (width > boxConstraintsUsingMetersInPixels.maxWidth) { + width = boxConstraintsUsingMetersInPixels.maxWidth; + } + if (height < boxConstraintsUsingMetersInPixels.minHeight) { + height = boxConstraintsUsingMetersInPixels.minHeight; + } + if (width < boxConstraintsUsingMetersInPixels.minWidth) { + width = boxConstraintsUsingMetersInPixels.minWidth; + } } } From 481ea05d0df19adecaee25d94f9c63b8b858764f Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 30 Jun 2026 22:36:00 +0100 Subject: [PATCH 07/29] Collapse `useDimensionsInMeters` & `meterToPixelSizeConstraints` into one property --- example/lib/pages/markers.dart | 5 +-- lib/src/layer/marker_layer/marker.dart | 38 ++++++++++++-------- lib/src/layer/marker_layer/marker_layer.dart | 13 ++++--- 3 files changed, 33 insertions(+), 23 deletions(-) diff --git a/example/lib/pages/markers.dart b/example/lib/pages/markers.dart index 3f1ee018e..65b413ccb 100644 --- a/example/lib/pages/markers.dart +++ b/example/lib/pages/markers.dart @@ -1,5 +1,3 @@ -import 'dart:math'; - import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_example/misc/tile_providers.dart'; @@ -172,8 +170,7 @@ class _MarkerPageState extends State { ), height: 1000, width: 1000, - useDimensionsInMeters: true, - meterToPixelSizeConstraints: const BoxConstraints( + useDimensionsInMeters: const BoxConstraints( minHeight: 30, minWidth: 30, ), diff --git a/lib/src/layer/marker_layer/marker.dart b/lib/src/layer/marker_layer/marker.dart index f81866fb4..b3566c809 100644 --- a/lib/src/layer/marker_layer/marker.dart +++ b/lib/src/layer/marker_layer/marker.dart @@ -22,24 +22,32 @@ class Marker { /// The [Marker] itself is not a widget. final Widget child; - /// Width of child, in pixels (or meters if [useDimensionsInMeters] is set). + /// Width of child, in pixels (unless [useDimensionsInMeters] is set). final double width; - /// Width of child, in pixels (or meters if [useDimensionsInMeters] is set). + /// Width of child, in pixels (unless [useDimensionsInMeters] is set). final double height; - /// Whether to treat [width] and [height] as a number of meters. + /// Whether to treat [width] and [height] as meters, with optional pixel size + /// constraints. /// - /// If this is `true`, the child can use [SizedBox.expand] to expand itself to - /// the available size. It can also use a [LayoutBuilder] to obtain its - /// calculated true size, if necessary. + /// If `null` (as default), [width] and [height] are specified in pixels. /// - /// See also [meterToPixelSizeConstraints]. - final bool useDimensionsInMeters; - - /// Optional constraints on the child's size in pixels, when - /// [useDimensionsInMeters] is enabled. - final BoxConstraints? meterToPixelSizeConstraints; + /// If set to [BoxConstraints], [width] and [height] are specified in meters. + /// They will constrain size of the marker on the screen in pixels. The + /// constraints must have finite minimum dimensions. + /// + /// Set an empty [BoxConstraints] to display the marker as its true + /// geographical size (without constraints): + /// + /// ```dart + /// useDimensionsInMeters: const BoxConstraints(), + /// ``` + /// + /// When using geographical sizing, the child can use [SizedBox.expand] to + /// expand itself to the available size. [LayoutBuilder] can be used to obtain + /// its calculated screen size, if necessary. + final BoxConstraints? useDimensionsInMeters; /// Alignment of the marker relative to the normal center at [point]. /// @@ -72,13 +80,13 @@ class Marker { required this.child, this.width = 30, this.height = 30, - this.useDimensionsInMeters = false, - this.meterToPixelSizeConstraints, + this.useDimensionsInMeters, this.alignment, this.rotate, }); - /// Returns the alignment of a [width]x[height] rectangle by [left]x[top] pixels. + /// Returns the alignment of a [width]x[height] rectangle by [left]x[top] + /// pixels. static Alignment computePixelAlignment({ required final double width, required final double height, diff --git a/lib/src/layer/marker_layer/marker_layer.dart b/lib/src/layer/marker_layer/marker_layer.dart index 023f46fff..33be25fc2 100644 --- a/lib/src/layer/marker_layer/marker_layer.dart +++ b/lib/src/layer/marker_layer/marker_layer.dart @@ -109,7 +109,8 @@ class _MarkerLayerState extends State { } (double, double) _getDimensionsInPixels(Marker marker) { - if (!marker.useDimensionsInMeters) return (marker.width, marker.height); + final constraints = marker.useDimensionsInMeters; + if (constraints == null) return (marker.width, marker.height); final camera = MapCamera.of(context); @@ -141,10 +142,14 @@ class _MarkerLayerState extends State { width = _pixelsPerMeter! * marker.width; height = _pixelsPerMeter! * marker.height; } - if (marker.meterToPixelSizeConstraints case final c?) { - return (c.constrainWidth(width), c.constrainHeight(height)); + + if (!constraints.minWidth.isFinite || !constraints.minHeight.isFinite) { + throw RangeError('`Marker.useSizeInMeters` must have finite minimums'); } - return (width, height); + return ( + constraints.constrainWidth(width), + constraints.constrainHeight(height) + ); } @override From d86b9ba67af1739917ac9ab482edbcb1925a64f9 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 1 Jul 2026 15:28:37 +0100 Subject: [PATCH 08/29] Fix support for Flutter 3.27 --- example/lib/pages/markers.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/lib/pages/markers.dart b/example/lib/pages/markers.dart index 65b413ccb..6426550de 100644 --- a/example/lib/pages/markers.dart +++ b/example/lib/pages/markers.dart @@ -177,7 +177,7 @@ class _MarkerPageState extends State { child: SizedBox.expand( child: LayoutBuilder( builder: (context, constraints) => DecoratedBox( - decoration: BoxDecoration(border: BoxBorder.all()), + decoration: BoxDecoration(border: Border.all()), ), ), ), From f2fe8f0916692ca63f5182693f1b16ba959a457f Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 1 Jul 2026 15:28:54 +0100 Subject: [PATCH 09/29] Minor fixes to workflows --- .github/workflows/branch.yml | 16 ++++++++-------- .github/workflows/master.yml | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/branch.yml b/.github/workflows/branch.yml index f19de9c46..5bf1bdab6 100644 --- a/.github/workflows/branch.yml +++ b/.github/workflows/branch.yml @@ -1,7 +1,7 @@ name: "Branch & PR" on: push: - branches: [ "!master" ] + branches: ["!master"] pull_request: workflow_dispatch: @@ -49,7 +49,7 @@ jobs: strategy: fail-fast: false matrix: - sdk: [ "3.27.0", "" ] + sdk: ["3.27.0", ""] steps: - name: Checkout Repository uses: actions/checkout@v7 @@ -72,7 +72,7 @@ jobs: strategy: fail-fast: false matrix: - sdk: [ "3.27.0", "" ] + sdk: ["3.27.0", ""] defaults: run: working-directory: ./example @@ -91,7 +91,7 @@ jobs: flutter-version: ${{ matrix.sdk }} cache: true - name: Build Android Application - run: flutter build apk --dart-define=COMMIT_SHA=${{ github.sha }} --dart-define=flutter.flutter_map.unblockOSM="${{ secrets.UNBLOCK_OSM }}" + run: flutter build apk --dart-define=COMMIT_SHA=${{ github.sha }} - name: Archive Artifact if: ${{ matrix.sdk == '' }} uses: actions/upload-artifact@v7 @@ -106,7 +106,7 @@ jobs: strategy: fail-fast: false matrix: - sdk: [ "3.27.0", "" ] + sdk: ["3.27.0", ""] defaults: run: working-directory: ./example @@ -119,7 +119,7 @@ jobs: flutter-version: ${{ matrix.sdk }} cache: true - name: Build Windows Application - run: flutter build windows --dart-define=COMMIT_SHA=${{ github.sha }} --dart-define=flutter.flutter_map.unblockOSM="${{ secrets.UNBLOCK_OSM }}" + run: flutter build windows --dart-define=COMMIT_SHA=${{ github.sha }} - name: Install Inno Setup if: ${{ matrix.sdk == '' }} run: choco install innosetup --yes --no-progress @@ -141,7 +141,7 @@ jobs: strategy: fail-fast: false matrix: - sdk: [ "3.27.0", "" ] + sdk: ["3.27.0", ""] defaults: run: working-directory: ./example @@ -154,7 +154,7 @@ jobs: flutter-version: ${{ matrix.sdk }} cache: true - name: Build Web Application - run: flutter build web --wasm --dart-define=COMMIT_SHA=${{ github.sha }} --dart-define=flutter.flutter_map.unblockOSM="${{ secrets.UNBLOCK_OSM }}" + run: flutter build web --wasm --dart-define=COMMIT_SHA=${{ github.sha }} - name: Archive Artifact uses: actions/upload-artifact@v7 if: ${{ matrix.sdk == '' }} diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 107d588ae..51f31b949 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -31,7 +31,7 @@ jobs: build-android: name: "Build Android Example App" runs-on: ubuntu-latest - needs: [ run-tests ] + needs: [run-tests] if: github.repository == 'fleaflet/flutter_map' defaults: run: @@ -44,14 +44,14 @@ jobs: with: distribution: "temurin" java-version: "21" - cache: 'gradle' + cache: "gradle" - name: Setup Flutter Environment uses: subosito/flutter-action@v2 with: channel: "stable" cache: true - name: Build Android Application - run: flutter build apk --dart-define=COMMIT_SHA=${{ github.sha }} --dart-define=flutter.flutter_map.unblockOSM="${{ secrets.UNBLOCK_OSM }}" + run: flutter build apk --dart-define=COMMIT_SHA=${{ github.sha }} - name: Archive Artifact uses: actions/upload-artifact@v7 with: @@ -62,7 +62,7 @@ jobs: build-windows: name: "Build Windows Example App" runs-on: windows-latest - needs: [ run-tests ] + needs: [run-tests] if: github.repository == 'fleaflet/flutter_map' defaults: run: @@ -75,7 +75,7 @@ jobs: with: cache: true - name: Build Windows Application - run: flutter build windows --dart-define=COMMIT_SHA=${{ github.sha }} --dart-define=flutter.flutter_map.unblockOSM="${{ secrets.UNBLOCK_OSM }}" + run: flutter build windows --dart-define=COMMIT_SHA=${{ github.sha }} - name: Install Inno Setup run: choco install innosetup --yes --no-progress - name: Create Windows Application Installer @@ -91,7 +91,7 @@ jobs: build-web: name: "Build & Deploy Web Example App" runs-on: ubuntu-latest - needs: [ run-tests ] + needs: [run-tests] if: github.repository == 'fleaflet/flutter_map' defaults: run: @@ -105,7 +105,7 @@ jobs: channel: "stable" cache: true - name: Build Web Application - run: flutter build web --wasm --dart-define=COMMIT_SHA=${{ github.sha }} --dart-define=flutter.flutter_map.unblockOSM="${{ secrets.UNBLOCK_OSM }}" + run: flutter build web --wasm --dart-define=COMMIT_SHA=${{ github.sha }} - name: Archive Artifact uses: actions/upload-artifact@v7 with: @@ -118,4 +118,4 @@ jobs: repoToken: "${{ secrets.GITHUB_TOKEN }}" firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_FLEAFLET }}" channelId: live - projectId: fleaflet-firebase \ No newline at end of file + projectId: fleaflet-firebase From d26597880a81af0f4f5b5c4c3ca79747f5abae73 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 1 Jul 2026 15:34:31 +0100 Subject: [PATCH 10/29] Bump Windows workflow 3.29 matrix verison to 3.32 to attempt to resolve failures --- .github/workflows/branch.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/branch.yml b/.github/workflows/branch.yml index 5bf1bdab6..927fb6d0a 100644 --- a/.github/workflows/branch.yml +++ b/.github/workflows/branch.yml @@ -106,7 +106,7 @@ jobs: strategy: fail-fast: false matrix: - sdk: ["3.27.0", ""] + sdk: ["3.32.0", ""] defaults: run: working-directory: ./example From 4f0c90d8f3d92608a9fb37f6f148947e8f63a4a7 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 1 Jul 2026 15:35:41 +0100 Subject: [PATCH 11/29] Revert changes to initial position on markers demo page --- example/lib/pages/markers.dart | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/example/lib/pages/markers.dart b/example/lib/pages/markers.dart index 6426550de..54b6702a2 100644 --- a/example/lib/pages/markers.dart +++ b/example/lib/pages/markers.dart @@ -117,11 +117,8 @@ class _MarkerPageState extends State { Flexible( child: FlutterMap( options: MapOptions( - initialCenter: const LatLng( - 51.51868093513547, - -0.12835376940892318, - ), - initialZoom: 15, + initialCenter: const LatLng(51.5, -0.09), + initialZoom: 5, onTap: (_, p) => setState(() => customMarkers.add(buildPin(p))), interactionOptions: const InteractionOptions( flags: ~InteractiveFlag.doubleTapZoom, From c8ca512e5451ebce29c079f1a0bed6d42b996531 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 1 Jul 2026 15:36:19 +0100 Subject: [PATCH 12/29] Minor typo fix --- lib/src/layer/marker_layer/marker.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/layer/marker_layer/marker.dart b/lib/src/layer/marker_layer/marker.dart index b3566c809..2f27696c5 100644 --- a/lib/src/layer/marker_layer/marker.dart +++ b/lib/src/layer/marker_layer/marker.dart @@ -25,7 +25,7 @@ class Marker { /// Width of child, in pixels (unless [useDimensionsInMeters] is set). final double width; - /// Width of child, in pixels (unless [useDimensionsInMeters] is set). + /// Height of child, in pixels (unless [useDimensionsInMeters] is set). final double height; /// Whether to treat [width] and [height] as meters, with optional pixel size From aab420ed04d59942d2186f70974efff1232c5016 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 1 Jul 2026 15:48:32 +0100 Subject: [PATCH 13/29] Bump Windows workflow 3.32 matrix verison to 3.39 to attempt to resolve failures --- .github/workflows/branch.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/branch.yml b/.github/workflows/branch.yml index 927fb6d0a..7ab29d854 100644 --- a/.github/workflows/branch.yml +++ b/.github/workflows/branch.yml @@ -106,7 +106,7 @@ jobs: strategy: fail-fast: false matrix: - sdk: ["3.32.0", ""] + sdk: ["3.39.0", ""] defaults: run: working-directory: ./example From 5c060c09ad992069a2d20560c6fd6bf49a912248 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 1 Jul 2026 15:50:22 +0100 Subject: [PATCH 14/29] Bump Windows workflow 3.39 matrix verison to 3.41 to attempt to resolve failures --- .github/workflows/branch.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/branch.yml b/.github/workflows/branch.yml index 7ab29d854..7410f9e9c 100644 --- a/.github/workflows/branch.yml +++ b/.github/workflows/branch.yml @@ -106,7 +106,7 @@ jobs: strategy: fail-fast: false matrix: - sdk: ["3.39.0", ""] + sdk: ["3.41.0", ""] # Can't use 3.27 on github because windows-latest is incompatible now defaults: run: working-directory: ./example From 44922d8e7b4bea76d2b9337a8be7c01a8ea10652 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 2 Jul 2026 17:20:19 +0100 Subject: [PATCH 15/29] Minor documentation improvements --- lib/src/layer/marker_layer/marker.dart | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/lib/src/layer/marker_layer/marker.dart b/lib/src/layer/marker_layer/marker.dart index 2f27696c5..8d8d3f011 100644 --- a/lib/src/layer/marker_layer/marker.dart +++ b/lib/src/layer/marker_layer/marker.dart @@ -22,10 +22,12 @@ class Marker { /// The [Marker] itself is not a widget. final Widget child; - /// Width of child, in pixels (unless [useDimensionsInMeters] is set). + /// Width dimension of child, in pixels (unless [useDimensionsInMeters] is + /// set). final double width; - /// Height of child, in pixels (unless [useDimensionsInMeters] is set). + /// Height dimension of child, in pixels (unless [useDimensionsInMeters] is + /// set). final double height; /// Whether to treat [width] and [height] as meters, with optional pixel size @@ -34,19 +36,21 @@ class Marker { /// If `null` (as default), [width] and [height] are specified in pixels. /// /// If set to [BoxConstraints], [width] and [height] are specified in meters. - /// They will constrain size of the marker on the screen in pixels. The - /// constraints must have finite minimum dimensions. + /// These [BoxConstraints] are in pixels, and constrain the screen size of the + /// child. /// - /// Set an empty [BoxConstraints] to display the marker as its true - /// geographical size (without constraints): + /// Set an empty [BoxConstraints] to display the child in meters without + /// constraints (true size): /// /// ```dart /// useDimensionsInMeters: const BoxConstraints(), /// ``` /// - /// When using geographical sizing, the child can use [SizedBox.expand] to - /// expand itself to the available size. [LayoutBuilder] can be used to obtain - /// its calculated screen size, if necessary. + /// Any constraints set must have finite minimum dimensions. + /// + /// When using meters, the child can use [SizedBox.expand] to expand itself to + /// the available size. [LayoutBuilder] can be used to obtain the calculated + /// screen size, if necessary. final BoxConstraints? useDimensionsInMeters; /// Alignment of the marker relative to the normal center at [point]. From c5870011c67a6ddf32cff68daa9f272514d13e64 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 2 Jul 2026 18:02:50 +0100 Subject: [PATCH 16/29] Improve efficiency by using cached projected marker points during meters calculations Improve efficiency by only calculating meters dimension once when size is square Improve efficiency by using objects instead of records and minimizing constructions Minor other improvements --- lib/src/layer/marker_layer/marker_layer.dart | 86 ++++++++++---------- 1 file changed, 42 insertions(+), 44 deletions(-) diff --git a/lib/src/layer/marker_layer/marker_layer.dart b/lib/src/layer/marker_layer/marker_layer.dart index 33be25fc2..49f5b25c0 100644 --- a/lib/src/layer/marker_layer/marker_layer.dart +++ b/lib/src/layer/marker_layer/marker_layer.dart @@ -108,47 +108,49 @@ class _MarkerLayerState extends State { ); } - (double, double) _getDimensionsInPixels(Marker marker) { + /// Calculate marker dimensions + Size _getSizeInPixels(Marker marker, Offset markerPoint) { final constraints = marker.useDimensionsInMeters; - if (constraints == null) return (marker.width, marker.height); + if (constraints == null) return Size(marker.width, marker.height); + if (!constraints.minWidth.isFinite || !constraints.minHeight.isFinite) { + throw RangeError( + '`Marker.useDimensionsInMeters` must have finite minimums', + ); + } + + // Marker dimensions are now in meters and constraints are valid final camera = MapCamera.of(context); + Size metersToScreenPixels() { + final baseOffset = markerPoint - camera.pixelOrigin; - (double, double) metersToScreenPixels() { - final baseOffset = camera.getOffsetFromOrigin(marker.point); - return ( - (baseOffset - - camera.getOffsetFromOrigin( - _distance.offset(marker.point, marker.width / 2, 180))) - .distance * - 2, - (baseOffset - + final width = 2 * + (baseOffset - + camera.getOffsetFromOrigin( + _distance.offset(marker.point, marker.width / 2, 180))) + .distance; + + if (marker.width == marker.height) return Size(width, width); + + return Size( + width, + 2 * + (baseOffset - camera.getOffsetFromOrigin( _distance.offset(marker.point, marker.height / 2, 180))) - .distance * - 2 + .distance, ); } - double width; - double height; if (!widget.optimizeDimensionsInMeters) { - // If not optimizing, then we need to calculate this for every marker... - (width, height) = metersToScreenPixels(); - } else { - // ...otherwise we use the cached ratio if available, or calculate it - // (using the first marker in the layer, given how this method is called) - _pixelsPerMeter ??= metersToScreenPixels().$1 / marker.width; - width = _pixelsPerMeter! * marker.width; - height = _pixelsPerMeter! * marker.height; + return constraints.constrain(metersToScreenPixels()); } - - if (!constraints.minWidth.isFinite || !constraints.minHeight.isFinite) { - throw RangeError('`Marker.useSizeInMeters` must have finite minimums'); - } - return ( - constraints.constrainWidth(width), - constraints.constrainHeight(height) + // If optimizing, use the cached ratio if available, or calculate it + // (using the first marker in the layer, given how this method is called) + _pixelsPerMeter ??= metersToScreenPixels().width / marker.width; + return constraints.constrainDimensions( + _pixelsPerMeter! * marker.width, + _pixelsPerMeter! * marker.height, ); } @@ -178,18 +180,14 @@ class _MarkerLayerState extends State { crs.transform(projected.dx, projected.dy, zoomScale); final pxPoint = Offset(px, py); - // Get marker dimensions - final double width; - final double height; - (width, height) = _getDimensionsInPixels(m); - - // Resolve real alignment - final left = - 0.5 * width * ((m.alignment ?? widget.alignment).x + 1); - final top = - 0.5 * height * ((m.alignment ?? widget.alignment).y + 1); - final right = width - left; - final bottom = height - top; + // Resolve real size and alignment + final size = _getSizeInPixels(m, pxPoint); + final resolvedAlignmentOffset = + (m.alignment ?? widget.alignment).alongSize(size); + final left = resolvedAlignmentOffset.dx; + final top = resolvedAlignmentOffset.dy; + final right = size.width - left; + final bottom = size.height - top; Positioned? getPositioned(double worldShift) { final shiftedX = pxPoint.dx + worldShift; @@ -211,8 +209,8 @@ class _MarkerLayerState extends State { return Positioned( key: m.key, - width: width, - height: height, + width: size.width, + height: size.height, left: shiftedLocalPoint.dx - right, top: shiftedLocalPoint.dy - bottom, child: (m.rotate ?? widget.rotate) From ddcdff4c484f6522057841443f8e30b9768a85a5 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 2 Jul 2026 18:36:56 +0100 Subject: [PATCH 17/29] Simplified internal calculations --- example/lib/pages/markers.dart | 2 +- lib/src/layer/marker_layer/marker_layer.dart | 30 ++++++++------------ 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/example/lib/pages/markers.dart b/example/lib/pages/markers.dart index 54b6702a2..6bbc130e4 100644 --- a/example/lib/pages/markers.dart +++ b/example/lib/pages/markers.dart @@ -166,7 +166,7 @@ class _MarkerPageState extends State { -0.12835, ), height: 1000, - width: 1000, + width: 500, useDimensionsInMeters: const BoxConstraints( minHeight: 30, minWidth: 30, diff --git a/lib/src/layer/marker_layer/marker_layer.dart b/lib/src/layer/marker_layer/marker_layer.dart index 49f5b25c0..d1ebcd9e3 100644 --- a/lib/src/layer/marker_layer/marker_layer.dart +++ b/lib/src/layer/marker_layer/marker_layer.dart @@ -108,7 +108,6 @@ class _MarkerLayerState extends State { ); } - /// Calculate marker dimensions Size _getSizeInPixels(Marker marker, Offset markerPoint) { final constraints = marker.useDimensionsInMeters; if (constraints == null) return Size(marker.width, marker.height); @@ -122,23 +121,19 @@ class _MarkerLayerState extends State { final camera = MapCamera.of(context); Size metersToScreenPixels() { - final baseOffset = markerPoint - camera.pixelOrigin; - - final width = 2 * - (baseOffset - - camera.getOffsetFromOrigin( - _distance.offset(marker.point, marker.width / 2, 180))) - .distance; + final width = markerPoint.dy - + camera + .projectAtZoom(_distance.offset(marker.point, marker.width, 0)) + .dy; if (marker.width == marker.height) return Size(width, width); return Size( width, - 2 * - (baseOffset - - camera.getOffsetFromOrigin( - _distance.offset(marker.point, marker.height / 2, 180))) - .distance, + markerPoint.dy - + camera + .projectAtZoom(_distance.offset(marker.point, marker.height, 0)) + .dy, ); } @@ -182,10 +177,9 @@ class _MarkerLayerState extends State { // Resolve real size and alignment final size = _getSizeInPixels(m, pxPoint); - final resolvedAlignmentOffset = - (m.alignment ?? widget.alignment).alongSize(size); - final left = resolvedAlignmentOffset.dx; - final top = resolvedAlignmentOffset.dy; + final alignment = m.alignment ?? widget.alignment; + final left = 0.5 * size.width * (alignment.x + 1); + final top = 0.5 * size.height * (alignment.y + 1); final right = size.width - left; final bottom = size.height - top; @@ -216,7 +210,7 @@ class _MarkerLayerState extends State { child: (m.rotate ?? widget.rotate) ? Transform.rotate( angle: -map.rotationRad, - alignment: (m.alignment ?? widget.alignment) * -1, + alignment: Alignment(-alignment.x, -alignment.y), child: m.child, ) : m.child, From 42e95bfa539823c3f199029e2b85e98793c90d83 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 2 Jul 2026 18:55:19 +0100 Subject: [PATCH 18/29] Fixed bug Fixed incorrect documentation --- lib/src/layer/circle_layer/circle_layer.dart | 8 ++++---- lib/src/layer/marker_layer/marker_layer.dart | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/src/layer/circle_layer/circle_layer.dart b/lib/src/layer/circle_layer/circle_layer.dart index 4fc6b86e8..7c5afec7b 100644 --- a/lib/src/layer/circle_layer/circle_layer.dart +++ b/lib/src/layer/circle_layer/circle_layer.dart @@ -28,12 +28,12 @@ class CircleLayer extends StatelessWidget { /// > location of the circles, this may or may not be significant. /// /// Where all circles within this layer are geographically (particularly - /// latitudinally) close, the difference in the ratio between pixels and + /// longitudinally) close, the difference in the ratio between pixels and /// meters between circles is likely to be small. Calculating this /// conversion ratio is expensive, and is usually done for every circle to - /// ensure accuracy, as the ratio depends on the latitude. Setting this `true` - /// means the ratio is calculated based off the first circle only, then reused - /// for all other circles within this layer. + /// ensure accuracy, as the ratio depends on the longitude. Setting this + /// `true` means the ratio is calculated based off the first circle only, then + /// reused for all other circles within this layer. /// /// This should not be used where circles are geographically spread out - it /// is best suited, for example, for circles located within a single city. diff --git a/lib/src/layer/marker_layer/marker_layer.dart b/lib/src/layer/marker_layer/marker_layer.dart index d1ebcd9e3..b7932162f 100644 --- a/lib/src/layer/marker_layer/marker_layer.dart +++ b/lib/src/layer/marker_layer/marker_layer.dart @@ -38,10 +38,10 @@ class MarkerLayer extends StatefulWidget { /// > location of the markers, this may or may not be significant. /// /// Where all markers within this layer are geographically (particularly - /// latitudinally) close, the difference in the ratio between pixels and + /// longitudinally) close, the difference in the ratio between pixels and /// meters between markers is likely to be small. Calculating this conversion /// ratio is expensive, and is usually done for every marker to ensure - /// accuracy, as the ratio depends on the latitude. Setting this `true` means + /// accuracy, as the ratio depends on the longitude. Setting this `true` means /// the ratio is calculated based off the first marker only, then reused for /// all other markers within this layer. /// @@ -159,6 +159,7 @@ class _MarkerLayerState extends State { _projectedPoints = _projectPoints(crs); } final projectedPoints = _projectedPoints!; + _pixelsPerMeter = null; final worldWidth = map.getWorldWidthAtZoom(); final zoomScale = crs.scale(map.zoom); From 34fcd53f4e3f8904d36b7e6a1df7a1a9ce42bc97 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 2 Jul 2026 22:15:02 +0100 Subject: [PATCH 19/29] Revert inaccurate documentation fix --- lib/src/layer/circle_layer/circle_layer.dart | 4 ++-- lib/src/layer/marker_layer/marker_layer.dart | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/src/layer/circle_layer/circle_layer.dart b/lib/src/layer/circle_layer/circle_layer.dart index 7c5afec7b..5602c6ecc 100644 --- a/lib/src/layer/circle_layer/circle_layer.dart +++ b/lib/src/layer/circle_layer/circle_layer.dart @@ -28,10 +28,10 @@ class CircleLayer extends StatelessWidget { /// > location of the circles, this may or may not be significant. /// /// Where all circles within this layer are geographically (particularly - /// longitudinally) close, the difference in the ratio between pixels and + /// latitudinally) close, the difference in the ratio between pixels and /// meters between circles is likely to be small. Calculating this /// conversion ratio is expensive, and is usually done for every circle to - /// ensure accuracy, as the ratio depends on the longitude. Setting this + /// ensure accuracy, as the ratio depends on the latitude. Setting this /// `true` means the ratio is calculated based off the first circle only, then /// reused for all other circles within this layer. /// diff --git a/lib/src/layer/marker_layer/marker_layer.dart b/lib/src/layer/marker_layer/marker_layer.dart index b7932162f..25a7de734 100644 --- a/lib/src/layer/marker_layer/marker_layer.dart +++ b/lib/src/layer/marker_layer/marker_layer.dart @@ -38,10 +38,10 @@ class MarkerLayer extends StatefulWidget { /// > location of the markers, this may or may not be significant. /// /// Where all markers within this layer are geographically (particularly - /// longitudinally) close, the difference in the ratio between pixels and + /// latitudinally) close, the difference in the ratio between pixels and /// meters between markers is likely to be small. Calculating this conversion /// ratio is expensive, and is usually done for every marker to ensure - /// accuracy, as the ratio depends on the longitude. Setting this `true` means + /// accuracy, as the ratio depends on the latitude. Setting this `true` means /// the ratio is calculated based off the first marker only, then reused for /// all other markers within this layer. /// From 270896b7d65b2d9e6d8abfcda2efd1b8a5c159e7 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 2 Jul 2026 23:45:46 +0100 Subject: [PATCH 20/29] Cache projections used to calculate meter sizes Improved markers demo page Fixed bugs --- example/lib/pages/markers.dart | 239 +++++++++++++++---- lib/src/layer/marker_layer/marker_layer.dart | 80 ++++--- 2 files changed, 238 insertions(+), 81 deletions(-) diff --git a/example/lib/pages/markers.dart b/example/lib/pages/markers.dart index 6bbc130e4..18fb3c5a6 100644 --- a/example/lib/pages/markers.dart +++ b/example/lib/pages/markers.dart @@ -16,6 +16,7 @@ class MarkerPage extends StatefulWidget { class _MarkerPageState extends State { Alignment selectedAlignment = Alignment.topCenter; bool counterRotate = false; + bool constrainMeterMarkers = false; static const alignments = { 315: Alignment.topLeft, @@ -59,57 +60,117 @@ class _MarkerPageState extends State { children: [ Padding( padding: const EdgeInsets.all(8), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, + child: Wrap( + alignment: WrapAlignment.center, + runAlignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + runSpacing: 20, + spacing: 20, children: [ - SizedBox.square( - dimension: 130, - child: GridView.builder( - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - mainAxisSpacing: 5, - crossAxisSpacing: 5, + Row( + spacing: 8, + mainAxisSize: MainAxisSize.min, + children: [ + RotatedBox( + quarterTurns: 3, + child: FittedBox( + child: Text( + 'ALIGNMENT OF PINS', + textAlign: TextAlign.center, + style: Theme.of(context) + .textTheme + .labelMedium! + .copyWith( + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.w400, + ), + ), + ), ), - itemCount: 9, - itemBuilder: (_, index) { - final deg = alignments.keys.elementAt(index); - final align = alignments.values.elementAt(index); - - return IconButton.outlined( - onPressed: () => - setState(() => selectedAlignment = align), - icon: Transform.rotate( - angle: deg == null ? 0 : deg * pi / 180, - child: Icon( - deg == null ? Icons.circle : Icons.arrow_upward, - color: selectedAlignment == align - ? Colors.green - : null, - size: deg == null ? 16 : null, - ), + SizedBox.square( + dimension: 130, + child: GridView.builder( + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + mainAxisSpacing: 5, + crossAxisSpacing: 5, ), - ); - }, - ), + itemCount: 9, + itemBuilder: (_, index) { + final deg = alignments.keys.elementAt(index); + final align = alignments.values.elementAt(index); + + return IconButton.outlined( + onPressed: () => + setState(() => selectedAlignment = align), + icon: Transform.rotate( + angle: deg == null ? 0 : deg * pi / 180, + child: Icon( + deg == null ? Icons.circle : Icons.arrow_upward, + color: selectedAlignment == align + ? Colors.green + : null, + size: deg == null ? 16 : null, + ), + ), + ); + }, + ), + ), + ], ), - const SizedBox(width: 16), - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('Tap the map to add markers!'), - const SizedBox(height: 10), - Row( + DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.onInverseSurface, + borderRadius: BorderRadius.circular(16), + ), + child: const Padding( + padding: EdgeInsets.all(16), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 8, children: [ - const Text('Counter-rotation'), - const SizedBox(width: 10), - Switch.adaptive( - value: counterRotate, - onChanged: (v) => setState(() => counterRotate = v), + Text( + 'Tap/click map to\nadd more pins', + textAlign: TextAlign.center, ), + Icon(Icons.add_location, size: 32) ], ), - ], + ), + ), + IntrinsicWidth( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + spacing: 12, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Flexible(child: Text('Counter rotate to map')), + Switch.adaptive( + value: counterRotate, + onChanged: (v) => setState(() => counterRotate = v), + ), + ], + ), + Row( + spacing: 12, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Flexible( + child: Text('Constraints on meter marker')), + Switch.adaptive( + value: constrainMeterMarkers, + onChanged: (v) => + setState(() => constrainMeterMarkers = v), + ), + ], + ), + ], + ), ), ], ), @@ -160,24 +221,72 @@ class _MarkerPageState extends State { rotate: false, child: ColoredBox(color: Colors.black), ), + Marker( + point: const LatLng( + 0, + -0.12835, + ), + height: 500000, + width: 300000, + useDimensionsInMeters: constrainMeterMarkers + ? const BoxConstraints(maxHeight: 200, maxWidth: 200) + : const BoxConstraints(), + child: + const _MeterMarkerChild(label: '500x300km\n(200px²)'), + ), Marker( point: const LatLng( 51.51868, -0.12835, ), height: 1000, - width: 500, - useDimensionsInMeters: const BoxConstraints( - minHeight: 30, - minWidth: 30, + width: 1000, + useDimensionsInMeters: constrainMeterMarkers + ? const BoxConstraints( + minHeight: 30, + minWidth: 30, + maxHeight: 1000, + maxWidth: 1000, + ) + : const BoxConstraints(), + child: _MeterMarkerChild( + label: '\n\n1km²\n(' + '${constrainMeterMarkers ? '30px²-1000px²' : 'constraints off'})', ), - child: SizedBox.expand( - child: LayoutBuilder( - builder: (context, constraints) => DecoratedBox( - decoration: BoxDecoration(border: Border.all()), - ), - ), + ), + const Marker( + point: LatLng( + 71.51868, + -0.12835, + ), + height: 500000, + width: 300000, + useDimensionsInMeters: BoxConstraints(), + child: _MeterMarkerChild( + label: '500x300km\n(no constraints)', + ), + ), + ], + ), + CircleLayer( + circles: [ + CircleMarker( + point: const LatLng( + 0, + -0.12835, + ), + radius: 150000, + useRadiusInMeter: true, + color: Colors.black.withValues(alpha: 0.2), + ), + CircleMarker( + point: const LatLng( + 71.51868, + -0.12835, ), + radius: 150000, + useRadiusInMeter: true, + color: Colors.black.withValues(alpha: 0.2), ), ], ), @@ -194,3 +303,27 @@ class _MarkerPageState extends State { ); } } + +class _MeterMarkerChild extends StatelessWidget { + const _MeterMarkerChild({required this.label}); + + final String label; + + @override + Widget build(BuildContext context) { + return SizedBox.expand( + child: LayoutBuilder( + builder: (context, constraints) => DecoratedBox( + decoration: BoxDecoration(border: Border.all()), + child: Padding( + padding: const EdgeInsets.all(6), + child: FittedBox( + fit: BoxFit.scaleDown, + child: Center(child: Text(label, textAlign: TextAlign.center)), + ), + ), + ), + ), + ); + } +} diff --git a/lib/src/layer/marker_layer/marker_layer.dart b/lib/src/layer/marker_layer/marker_layer.dart index 25a7de734..28b26c9e0 100644 --- a/lib/src/layer/marker_layer/marker_layer.dart +++ b/lib/src/layer/marker_layer/marker_layer.dart @@ -76,6 +76,7 @@ class _MarkerLayerState extends State { /// projected -> screen transformation per marker, instead of a full /// re-projection. List? _projectedPoints; + Map? _projectedMeterSizes; Crs? _projectionCrs; // Cached number of pixels per meter. @@ -89,6 +90,7 @@ class _MarkerLayerState extends State { // new widget instance re-projects, so in-place mutations of the markers // list keep working as they did when projection was performed per-frame. _projectedPoints = null; + _projectedMeterSizes = null; } List _projectPoints(Crs crs) { @@ -108,9 +110,34 @@ class _MarkerLayerState extends State { ); } - Size _getSizeInPixels(Marker marker, Offset markerPoint) { - final constraints = marker.useDimensionsInMeters; - if (constraints == null) return Size(marker.width, marker.height); + Map _projectMeterSizes(Crs crs) { + final projection = crs.projection; + return Map.fromEntries( + () sync* { + for (int i = 0; i < widget.markers.length; i++) { + final m = widget.markers[i]; + if (m.useDimensionsInMeters == null) continue; + + final w = + projection.project(_distance.offset(m.point, m.width / 2, 180)); + if (m.width == m.height) yield MapEntry(i, (w, w)); + yield MapEntry( + i, + ( + w, + projection.project(_distance.offset(m.point, m.height / 2, 180)), + ), + ); + + if (widget.optimizeDimensionsInMeters) break; + } + }(), + ); + } + + Size _getSizeInPixels(Marker m, int i, Offset pxPoint, double zoomScale) { + final constraints = m.useDimensionsInMeters; + if (constraints == null) return Size(m.width, m.height); if (!constraints.minWidth.isFinite || !constraints.minHeight.isFinite) { throw RangeError( '`Marker.useDimensionsInMeters` must have finite minimums', @@ -120,64 +147,61 @@ class _MarkerLayerState extends State { // Marker dimensions are now in meters and constraints are valid final camera = MapCamera.of(context); - Size metersToScreenPixels() { - final width = markerPoint.dy - - camera - .projectAtZoom(_distance.offset(marker.point, marker.width, 0)) - .dy; - - if (marker.width == marker.height) return Size(width, width); + Size metersToScreenPixels(int i) { + final p = _projectedMeterSizes![i]!; + final (wpx, wpy) = camera.crs.transform(p.$1.dx, p.$1.dy, zoomScale); + final width = 2 * (pxPoint - Offset(wpx, wpy)).distance; + if (m.width == m.height) return Size(width, width); + final (hpx, hpy) = camera.crs.transform(p.$2.dx, p.$2.dy, zoomScale); return Size( width, - markerPoint.dy - - camera - .projectAtZoom(_distance.offset(marker.point, marker.height, 0)) - .dy, + 2 * (pxPoint - Offset(hpx, hpy)).distance, ); } if (!widget.optimizeDimensionsInMeters) { - return constraints.constrain(metersToScreenPixels()); + return constraints.constrain(metersToScreenPixels(i)); } // If optimizing, use the cached ratio if available, or calculate it // (using the first marker in the layer, given how this method is called) - _pixelsPerMeter ??= metersToScreenPixels().width / marker.width; + _pixelsPerMeter ??= metersToScreenPixels(i).width / m.width; return constraints.constrainDimensions( - _pixelsPerMeter! * marker.width, - _pixelsPerMeter! * marker.height, + _pixelsPerMeter! * m.width, + _pixelsPerMeter! * m.height, ); } @override Widget build(BuildContext context) { final map = MapCamera.of(context); - final crs = map.crs; - if (_projectedPoints == null || _projectionCrs != crs) { - _projectionCrs = crs; - _projectedPoints = _projectPoints(crs); + if (_projectedPoints == null || + _projectedMeterSizes == null || + _projectionCrs != map.crs) { + _projectionCrs = map.crs; + _projectedPoints = _projectPoints(map.crs); + _projectedMeterSizes = _projectMeterSizes(map.crs); } - final projectedPoints = _projectedPoints!; _pixelsPerMeter = null; final worldWidth = map.getWorldWidthAtZoom(); - final zoomScale = crs.scale(map.zoom); + final zoomScale = map.crs.scale(map.zoom); return MobileLayerTransformer( child: Stack( children: () sync* { - for (var i = 0; i < widget.markers.length; i++) { + for (int i = 0; i < widget.markers.length; i++) { final m = widget.markers[i]; // Scale the cached projection to the current zoom - final projected = projectedPoints[i]; + final projected = _projectedPoints![i]; final (px, py) = - crs.transform(projected.dx, projected.dy, zoomScale); + map.crs.transform(projected.dx, projected.dy, zoomScale); final pxPoint = Offset(px, py); // Resolve real size and alignment - final size = _getSizeInPixels(m, pxPoint); + final size = _getSizeInPixels(m, i, pxPoint, zoomScale); final alignment = m.alignment ?? widget.alignment; final left = 0.5 * size.width * (alignment.x + 1); final top = 0.5 * size.height * (alignment.y + 1); From 45ae43b36771b2cdd11df3b55baea63b0042e2ce Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Fri, 3 Jul 2026 00:12:55 +0100 Subject: [PATCH 21/29] Minor performance improvement --- lib/src/layer/marker_layer/marker_layer.dart | 30 +++++++++++--------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/lib/src/layer/marker_layer/marker_layer.dart b/lib/src/layer/marker_layer/marker_layer.dart index 28b26c9e0..07ef753f5 100644 --- a/lib/src/layer/marker_layer/marker_layer.dart +++ b/lib/src/layer/marker_layer/marker_layer.dart @@ -136,8 +136,7 @@ class _MarkerLayerState extends State { } Size _getSizeInPixels(Marker m, int i, Offset pxPoint, double zoomScale) { - final constraints = m.useDimensionsInMeters; - if (constraints == null) return Size(m.width, m.height); + final constraints = m.useDimensionsInMeters!; if (!constraints.minWidth.isFinite || !constraints.minHeight.isFinite) { throw RangeError( '`Marker.useDimensionsInMeters` must have finite minimums', @@ -154,10 +153,7 @@ class _MarkerLayerState extends State { final width = 2 * (pxPoint - Offset(wpx, wpy)).distance; if (m.width == m.height) return Size(width, width); final (hpx, hpy) = camera.crs.transform(p.$2.dx, p.$2.dy, zoomScale); - return Size( - width, - 2 * (pxPoint - Offset(hpx, hpy)).distance, - ); + return Size(width, 2 * (pxPoint - Offset(hpx, hpy)).distance); } if (!widget.optimizeDimensionsInMeters) { @@ -201,12 +197,20 @@ class _MarkerLayerState extends State { final pxPoint = Offset(px, py); // Resolve real size and alignment - final size = _getSizeInPixels(m, i, pxPoint, zoomScale); + final double width; + final double height; + if (m.useDimensionsInMeters == null) { + width = m.width; + height = m.height; + } else { + Size(:width, :height) = + _getSizeInPixels(m, i, pxPoint, zoomScale); + } final alignment = m.alignment ?? widget.alignment; - final left = 0.5 * size.width * (alignment.x + 1); - final top = 0.5 * size.height * (alignment.y + 1); - final right = size.width - left; - final bottom = size.height - top; + final left = 0.5 * width * (alignment.x + 1); + final top = 0.5 * height * (alignment.y + 1); + final right = width - left; + final bottom = height - top; Positioned? getPositioned(double worldShift) { final shiftedX = pxPoint.dx + worldShift; @@ -228,8 +232,8 @@ class _MarkerLayerState extends State { return Positioned( key: m.key, - width: size.width, - height: size.height, + width: width, + height: height, left: shiftedLocalPoint.dx - right, top: shiftedLocalPoint.dy - bottom, child: (m.rotate ?? widget.rotate) From 8f5b33e5502865c2648269227faaea04b79e064b Mon Sep 17 00:00:00 2001 From: Luka Stillingfleet Date: Fri, 3 Jul 2026 00:13:53 +0100 Subject: [PATCH 22/29] Discard changes to lib/src/layer/circle_layer/circle_layer.dart --- lib/src/layer/circle_layer/circle_layer.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/layer/circle_layer/circle_layer.dart b/lib/src/layer/circle_layer/circle_layer.dart index 5602c6ecc..4fc6b86e8 100644 --- a/lib/src/layer/circle_layer/circle_layer.dart +++ b/lib/src/layer/circle_layer/circle_layer.dart @@ -31,9 +31,9 @@ class CircleLayer extends StatelessWidget { /// latitudinally) close, the difference in the ratio between pixels and /// meters between circles is likely to be small. Calculating this /// conversion ratio is expensive, and is usually done for every circle to - /// ensure accuracy, as the ratio depends on the latitude. Setting this - /// `true` means the ratio is calculated based off the first circle only, then - /// reused for all other circles within this layer. + /// ensure accuracy, as the ratio depends on the latitude. Setting this `true` + /// means the ratio is calculated based off the first circle only, then reused + /// for all other circles within this layer. /// /// This should not be used where circles are geographically spread out - it /// is best suited, for example, for circles located within a single city. From 7fce640a4dd3ecf556502879d8a9813a3bb37ff2 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Fri, 3 Jul 2026 12:59:10 +0100 Subject: [PATCH 23/29] Minor performance improvements --- example/pubspec.lock | 2 +- lib/src/layer/marker_layer/marker_layer.dart | 96 ++++++++++---------- 2 files changed, 51 insertions(+), 47 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index 50adcec9a..4cd9a4455 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -124,7 +124,7 @@ packages: path: ".." relative: true source: path - version: "8.3.0" + version: "8.3.1" flutter_test: dependency: "direct dev" description: flutter diff --git a/lib/src/layer/marker_layer/marker_layer.dart b/lib/src/layer/marker_layer/marker_layer.dart index 07ef753f5..152a69433 100644 --- a/lib/src/layer/marker_layer/marker_layer.dart +++ b/lib/src/layer/marker_layer/marker_layer.dart @@ -135,54 +135,38 @@ class _MarkerLayerState extends State { ); } - Size _getSizeInPixels(Marker m, int i, Offset pxPoint, double zoomScale) { - final constraints = m.useDimensionsInMeters!; - if (!constraints.minWidth.isFinite || !constraints.minHeight.isFinite) { - throw RangeError( - '`Marker.useDimensionsInMeters` must have finite minimums', - ); - } - - // Marker dimensions are now in meters and constraints are valid - - final camera = MapCamera.of(context); - Size metersToScreenPixels(int i) { - final p = _projectedMeterSizes![i]!; - - final (wpx, wpy) = camera.crs.transform(p.$1.dx, p.$1.dy, zoomScale); - final width = 2 * (pxPoint - Offset(wpx, wpy)).distance; - if (m.width == m.height) return Size(width, width); - final (hpx, hpy) = camera.crs.transform(p.$2.dx, p.$2.dy, zoomScale); - return Size(width, 2 * (pxPoint - Offset(hpx, hpy)).distance); - } - - if (!widget.optimizeDimensionsInMeters) { - return constraints.constrain(metersToScreenPixels(i)); - } - // If optimizing, use the cached ratio if available, or calculate it - // (using the first marker in the layer, given how this method is called) - _pixelsPerMeter ??= metersToScreenPixels(i).width / m.width; - return constraints.constrainDimensions( - _pixelsPerMeter! * m.width, - _pixelsPerMeter! * m.height, - ); + /// Use generated [_projectedMeterSizes] to convert a marker's size in meters + /// to its screen size + Size _metersToScreenPixels( + MapCamera camera, + Marker m, + Offset pxPoint, + int i, + double zoomScale, + ) { + final p = _projectedMeterSizes![i]!; + final (wpx, wpy) = camera.crs.transform(p.$1.dx, p.$1.dy, zoomScale); + final width = 2 * (pxPoint - Offset(wpx, wpy)).distance; + if (m.width == m.height) return Size.square(width); + final (hpx, hpy) = camera.crs.transform(p.$2.dx, p.$2.dy, zoomScale); + return Size(width, 2 * (pxPoint - Offset(hpx, hpy)).distance); } @override Widget build(BuildContext context) { - final map = MapCamera.of(context); + final camera = MapCamera.of(context); if (_projectedPoints == null || _projectedMeterSizes == null || - _projectionCrs != map.crs) { - _projectionCrs = map.crs; - _projectedPoints = _projectPoints(map.crs); - _projectedMeterSizes = _projectMeterSizes(map.crs); + _projectionCrs != camera.crs) { + _projectionCrs = camera.crs; + _projectedPoints = _projectPoints(camera.crs); + _projectedMeterSizes = _projectMeterSizes(camera.crs); } _pixelsPerMeter = null; - final worldWidth = map.getWorldWidthAtZoom(); - final zoomScale = map.crs.scale(map.zoom); + final worldWidth = camera.getWorldWidthAtZoom(); + final zoomScale = camera.crs.scale(camera.zoom); return MobileLayerTransformer( child: Stack( @@ -193,18 +177,38 @@ class _MarkerLayerState extends State { // Scale the cached projection to the current zoom final projected = _projectedPoints![i]; final (px, py) = - map.crs.transform(projected.dx, projected.dy, zoomScale); + camera.crs.transform(projected.dx, projected.dy, zoomScale); final pxPoint = Offset(px, py); // Resolve real size and alignment final double width; final double height; - if (m.useDimensionsInMeters == null) { + if (m.useDimensionsInMeters case final constraints?) { + if (!constraints.minWidth.isFinite || + !constraints.minHeight.isFinite) { + throw RangeError( + '`Marker.useDimensionsInMeters` must have finite minimums', + ); + } + if (!widget.optimizeDimensionsInMeters) { + final size = + _metersToScreenPixels(camera, m, pxPoint, i, zoomScale); + width = constraints.constrainWidth(size.width); + height = constraints.constrainHeight(size.height); + } else { + // If optimizing, use the cached ratio if available, or + // calculate it (using the first marker in the layer) + _pixelsPerMeter ??= + _metersToScreenPixels(camera, m, pxPoint, i, zoomScale) + .width / + m.width; + width = constraints.constrainWidth(_pixelsPerMeter! * m.width); + height = + constraints.constrainHeight(_pixelsPerMeter! * m.height); + } + } else { width = m.width; height = m.height; - } else { - Size(:width, :height) = - _getSizeInPixels(m, i, pxPoint, zoomScale); } final alignment = m.alignment ?? widget.alignment; final left = 0.5 * width * (alignment.x + 1); @@ -216,7 +220,7 @@ class _MarkerLayerState extends State { final shiftedX = pxPoint.dx + worldShift; // Cull if out of bounds - if (!map.pixelBounds.overlaps( + if (!camera.pixelBounds.overlaps( Rect.fromPoints( Offset(shiftedX + left, pxPoint.dy - bottom), Offset(shiftedX - right, pxPoint.dy + top), @@ -228,7 +232,7 @@ class _MarkerLayerState extends State { // Shift original coordinate along worlds, then move into relative // to origin space final shiftedLocalPoint = - Offset(shiftedX, pxPoint.dy) - map.pixelOrigin; + Offset(shiftedX, pxPoint.dy) - camera.pixelOrigin; return Positioned( key: m.key, @@ -238,7 +242,7 @@ class _MarkerLayerState extends State { top: shiftedLocalPoint.dy - bottom, child: (m.rotate ?? widget.rotate) ? Transform.rotate( - angle: -map.rotationRad, + angle: -camera.rotationRad, alignment: Alignment(-alignment.x, -alignment.y), child: m.child, ) From 832a9f90aeec1748b17c69a1a918d516e1730293 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 8 Jul 2026 20:14:03 +0100 Subject: [PATCH 24/29] Fixed performance bug --- lib/src/layer/marker_layer/marker_layer.dart | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/lib/src/layer/marker_layer/marker_layer.dart b/lib/src/layer/marker_layer/marker_layer.dart index 152a69433..f83ca025a 100644 --- a/lib/src/layer/marker_layer/marker_layer.dart +++ b/lib/src/layer/marker_layer/marker_layer.dart @@ -120,15 +120,11 @@ class _MarkerLayerState extends State { final w = projection.project(_distance.offset(m.point, m.width / 2, 180)); - if (m.width == m.height) yield MapEntry(i, (w, w)); - yield MapEntry( - i, - ( - w, - projection.project(_distance.offset(m.point, m.height / 2, 180)), - ), - ); + late final h = + projection.project(_distance.offset(m.point, m.height / 2, 180)); + yield MapEntry(i, (w, m.width == m.height ? w : h)); + // If we're optimizing, we'll never use any more values we generate if (widget.optimizeDimensionsInMeters) break; } }(), From ef119602b9e1fb1783fed6386256813d612dd07e Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 8 Jul 2026 22:42:17 +0100 Subject: [PATCH 25/29] Improved example application --- example/lib/pages/many_circles.dart | 131 +++++++++++----------- example/lib/pages/many_markers.dart | 164 +++++++++++++++++++++++----- 2 files changed, 201 insertions(+), 94 deletions(-) diff --git a/example/lib/pages/many_circles.dart b/example/lib/pages/many_circles.dart index 6d03da5f3..11e42c53a 100644 --- a/example/lib/pages/many_circles.dart +++ b/example/lib/pages/many_circles.dart @@ -99,6 +99,7 @@ class _ManyCirclesPageState extends State { right: 16, child: RepaintBoundary( child: Column( + spacing: 12, children: [ NumberOfItemsSlider( number: displayedCirclesCount, @@ -106,7 +107,6 @@ class _ManyCirclesPageState extends State { maxNumber: _maxCirclesCount, itemDescription: 'Circle', ), - const SizedBox(height: 12), UnconstrainedBox( child: Container( decoration: BoxDecoration( @@ -117,71 +117,70 @@ class _ManyCirclesPageState extends State { vertical: 4, horizontal: 16, ), - child: Row( - children: [ - const Tooltip( - message: 'Use Borders', - child: Icon(Icons.circle_outlined), - ), - const SizedBox(width: 8), - Switch.adaptive( - value: useBorders, - onChanged: (v) { - allCircles = allCircles - .map( - (c) => CircleMarker( - point: c.point, - radius: c.radius, - color: c.color, - useRadiusInMeter: c.useRadiusInMeter, - borderColor: c.borderColor, - borderStrokeWidth: v ? 5 : 0, - ), - ) - .toList(growable: false); - useBorders = v; - setState(() {}); - }, - ), - const SizedBox(width: 16), - const Tooltip( - message: 'Use Radius In Meters', - child: Icon(Icons.straighten), - ), - const SizedBox(width: 8), - Switch.adaptive( - value: useRadiusInMeters, - onChanged: (v) { - allCircles = allCircles - .map( - (c) => CircleMarker( - point: c.point, - radius: v ? 25000 : 5, - color: c.color, - useRadiusInMeter: v, - borderColor: c.borderColor, - borderStrokeWidth: c.borderStrokeWidth, - ), - ) - .toList(growable: false); - useRadiusInMeters = v; - setState(() {}); - }, - ), - const SizedBox(width: 16), - const Tooltip( - message: 'Optimise Meters Radius', - child: Icon(Icons.speed_rounded), - ), - const SizedBox(width: 8), - Switch.adaptive( - value: optimizeRadiusInMeters, - onChanged: useRadiusInMeters - ? (v) => - setState(() => optimizeRadiusInMeters = v) - : null, - ), - ], + child: IntrinsicHeight( + child: Row( + spacing: 8, + children: [ + const Tooltip( + message: 'Use Borders', + child: Icon(Icons.circle_outlined), + ), + Switch.adaptive( + value: useBorders, + onChanged: (v) { + allCircles = allCircles + .map( + (c) => CircleMarker( + point: c.point, + radius: c.radius, + color: c.color, + useRadiusInMeter: c.useRadiusInMeter, + borderColor: c.borderColor, + borderStrokeWidth: v ? 5 : 0, + ), + ) + .toList(growable: false); + useBorders = v; + setState(() {}); + }, + ), + const VerticalDivider(), + const Tooltip( + message: 'Use Radius In Meters', + child: Icon(Icons.straighten), + ), + Switch.adaptive( + value: useRadiusInMeters, + onChanged: (v) { + allCircles = allCircles + .map( + (c) => CircleMarker( + point: c.point, + radius: v ? 25000 : 5, + color: c.color, + useRadiusInMeter: v, + borderColor: c.borderColor, + borderStrokeWidth: c.borderStrokeWidth, + ), + ) + .toList(growable: false); + useRadiusInMeters = v; + setState(() {}); + }, + ), + const Tooltip( + message: 'Optimise Meters Radius', + child: Icon(Icons.speed_rounded), + ), + Switch.adaptive( + value: optimizeRadiusInMeters, + onChanged: useRadiusInMeters + ? (v) => + setState(() => optimizeRadiusInMeters = v) + : null, + ), + ], + ), ), ), ), diff --git a/example/lib/pages/many_markers.dart b/example/lib/pages/many_markers.dart index f8060e785..ec839e23b 100644 --- a/example/lib/pages/many_markers.dart +++ b/example/lib/pages/many_markers.dart @@ -30,7 +30,7 @@ class ManyMarkersPage extends StatefulWidget { class ManyMarkersPageState extends State { final randomGenerator = Random(10); - late final allMarkers = List.generate( + late List allMarkers = List.generate( _maxMarkersCount, (_) { final angle = randomGenerator.nextDouble() * 2 * pi; @@ -46,28 +46,14 @@ class ManyMarkersPageState extends State { return Marker( point: position, - width: 30, - height: 30, - child: GestureDetector( - onTap: () => ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'Tapped existing marker (${position.latitude}, ' - '${position.longitude})', - ), - duration: const Duration(seconds: 1), - showCloseIcon: true, - ), - ), - child: Icon( - Icons.location_pin, - size: 30, - color: Color.fromARGB( - 255, - randomGenerator.nextInt(256), - randomGenerator.nextInt(256), - randomGenerator.nextInt(256), - ), + child: Icon( + Icons.location_pin, + size: 30, + color: Color.fromARGB( + 255, + randomGenerator.nextInt(256), + randomGenerator.nextInt(256), + randomGenerator.nextInt(256), ), ), ); @@ -75,6 +61,10 @@ class ManyMarkersPageState extends State { ); int displayedMarkersCount = _maxMarkersCount ~/ 10; + bool useIcons = true; + bool useSizeInMeters = false; + bool optimizeSizeInMeters = true; + @override void initState() { super.initState(); @@ -103,6 +93,7 @@ class ManyMarkersPageState extends State { markers: allMarkers .take(displayedMarkersCount) .toList(growable: false), + optimizeDimensionsInMeters: optimizeSizeInMeters, ), ], ), @@ -110,11 +101,128 @@ class ManyMarkersPageState extends State { left: 16, top: 16, right: 16, - child: NumberOfItemsSlider( - number: displayedMarkersCount, - onChanged: (v) => setState(() => displayedMarkersCount = v), - maxNumber: _maxMarkersCount, - itemDescription: 'Marker', + child: Column( + spacing: 12, + children: [ + NumberOfItemsSlider( + number: displayedMarkersCount, + onChanged: (v) => setState(() => displayedMarkersCount = v), + maxNumber: _maxMarkersCount, + itemDescription: 'Marker', + ), + UnconstrainedBox( + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(32), + ), + padding: const EdgeInsets.symmetric( + vertical: 4, + horizontal: 16, + ), + child: IntrinsicHeight( + child: Row( + spacing: 8, + children: [ + const Tooltip( + message: 'Use Icons', + child: Icon(Icons.location_on), + ), + Switch.adaptive( + value: useIcons, + onChanged: (v) { + if (v) { + allMarkers = allMarkers.map( + (c) { + return Marker( + point: c.point, + child: Icon( + Icons.location_pin, + size: 30, + color: Color.fromARGB( + 255, + randomGenerator.nextInt(256), + randomGenerator.nextInt(256), + randomGenerator.nextInt(256), + ), + ), + ); + }, + ).toList(growable: false); + } else { + allMarkers = allMarkers.map( + (c) { + return Marker( + point: c.point, + useDimensionsInMeters: useSizeInMeters + ? const BoxConstraints() + : null, + height: useSizeInMeters ? 1000 : 30, + width: useSizeInMeters ? 1000 : 30, + child: SizedBox.expand( + child: DecoratedBox( + decoration: BoxDecoration( + border: Border.all(), + ), + ), + ), + ); + }, + ).toList(growable: false); + } + useIcons = v; + setState(() {}); + }, + ), + const VerticalDivider(), + const Tooltip( + message: 'Use Radius In Meters', + child: Icon(Icons.straighten), + ), + Switch.adaptive( + value: useSizeInMeters, + onChanged: useIcons + ? null + : (v) { + allMarkers = allMarkers.map( + (c) { + return Marker( + point: c.point, + useDimensionsInMeters: + v ? const BoxConstraints() : null, + height: v ? 1000 : 30, + width: v ? 1000 : 30, + child: SizedBox.expand( + child: DecoratedBox( + decoration: BoxDecoration( + border: Border.all(), + ), + ), + ), + ); + }, + ).toList(growable: false); + useSizeInMeters = v; + setState(() {}); + }, + ), + const Tooltip( + message: 'Optimise Meters Radius', + child: Icon(Icons.speed_rounded), + ), + Switch.adaptive( + value: optimizeSizeInMeters, + onChanged: useSizeInMeters && !useIcons + ? (v) => + setState(() => optimizeSizeInMeters = v) + : null, + ), + ], + ), + ), + ), + ), + ], ), ), if (!kIsWeb) From 30b04ddc0aad9d1c4b68bdda2ae3412a66cf74b6 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 8 Jul 2026 23:06:20 +0100 Subject: [PATCH 26/29] Use `HashMap` over `LinkedHashMap` to store projected meter cache --- lib/src/layer/marker_layer/marker_layer.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/src/layer/marker_layer/marker_layer.dart b/lib/src/layer/marker_layer/marker_layer.dart index f83ca025a..76d640642 100644 --- a/lib/src/layer/marker_layer/marker_layer.dart +++ b/lib/src/layer/marker_layer/marker_layer.dart @@ -1,3 +1,5 @@ +import 'dart:collection'; + import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; @@ -112,7 +114,7 @@ class _MarkerLayerState extends State { Map _projectMeterSizes(Crs crs) { final projection = crs.projection; - return Map.fromEntries( + return HashMap.fromEntries( () sync* { for (int i = 0; i < widget.markers.length; i++) { final m = widget.markers[i]; From 2de7cb8ec3811bd6d9ec84d53973362ac8e365a6 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 9 Jul 2026 20:47:11 +0100 Subject: [PATCH 27/29] Fix bug where a defined key on `Marker` would break across multiple worlds --- lib/src/layer/marker_layer/marker_layer.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/layer/marker_layer/marker_layer.dart b/lib/src/layer/marker_layer/marker_layer.dart index 76d640642..8fb98d759 100644 --- a/lib/src/layer/marker_layer/marker_layer.dart +++ b/lib/src/layer/marker_layer/marker_layer.dart @@ -233,7 +233,7 @@ class _MarkerLayerState extends State { Offset(shiftedX, pxPoint.dy) - camera.pixelOrigin; return Positioned( - key: m.key, + key: m.key != null ? ValueKey((m.key, worldShift)) : null, width: width, height: height, left: shiftedLocalPoint.dx - right, From 145fa7d8938fc3b8a106d2d2e72d291bcd6d5244 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 9 Jul 2026 20:54:50 +0100 Subject: [PATCH 28/29] Revert previous commit --- lib/src/layer/marker_layer/marker_layer.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/layer/marker_layer/marker_layer.dart b/lib/src/layer/marker_layer/marker_layer.dart index 8fb98d759..76d640642 100644 --- a/lib/src/layer/marker_layer/marker_layer.dart +++ b/lib/src/layer/marker_layer/marker_layer.dart @@ -233,7 +233,7 @@ class _MarkerLayerState extends State { Offset(shiftedX, pxPoint.dy) - camera.pixelOrigin; return Positioned( - key: m.key != null ? ValueKey((m.key, worldShift)) : null, + key: m.key, width: width, height: height, left: shiftedLocalPoint.dx - right, From 3fdb4fbd1ab0cf48a6f879e1e056e131acd9f9a1 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 9 Jul 2026 21:09:46 +0100 Subject: [PATCH 29/29] Minor fix to example app --- example/lib/pages/markers.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/example/lib/pages/markers.dart b/example/lib/pages/markers.dart index 18fb3c5a6..67e295bff 100644 --- a/example/lib/pages/markers.dart +++ b/example/lib/pages/markers.dart @@ -231,8 +231,10 @@ class _MarkerPageState extends State { useDimensionsInMeters: constrainMeterMarkers ? const BoxConstraints(maxHeight: 200, maxWidth: 200) : const BoxConstraints(), - child: - const _MeterMarkerChild(label: '500x300km\n(200px²)'), + child: _MeterMarkerChild( + label: '500x300km\n(' + '${constrainMeterMarkers ? '0-200px²' : 'constraints off'})', + ), ), Marker( point: const LatLng(