From 24807334f0d37e8807acfdaf65f8e63dc0a1dc14 Mon Sep 17 00:00:00 2001
From: Simon Irmancnik
Date: Mon, 15 Dec 2025 22:57:15 +0100
Subject: [PATCH 01/38] Return all targets when supplying empty layers list on
iOS (#680)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This PR fixes a bug on iOS when calling queryRenderedFeatures.
If you supply an empy array list, none of the targets are returned,
which is contrary to what happens on Android.
Co-authored-by: Simon Irmančnik
---
.../maplibre_gl/Sources/maplibre_gl/MapLibreMapController.swift | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/MapLibreMapController.swift b/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/MapLibreMapController.swift
index dcdb0bdfd..4a4344abd 100644
--- a/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/MapLibreMapController.swift
+++ b/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/MapLibreMapController.swift
@@ -254,7 +254,7 @@ class MapLibreMapController: NSObject, FlutterPlatformView, MLNMapViewDelegate,
case "map#queryRenderedFeatures":
guard let arguments = methodCall.arguments as? [String: Any] else { return }
var styleLayerIdentifiers: Set?
- if let layerIds = arguments["layerIds"] as? [String] {
+ if let layerIds = arguments["layerIds"] as? [String], !layerIds.isEmpty {
styleLayerIdentifiers = Set(layerIds)
}
var filterExpression: NSPredicate?
From 382005eb3b013a89e7fee24acb4c0dde246a279e Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 30 Dec 2025 12:43:16 +0100
Subject: [PATCH 02/38] chore(deps): bump com.android.application from 8.12.0
to 8.13.2 in /maplibre_gl_example/android (#689)
Bumps com.android.application from 8.12.0 to 8.13.2.
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Gabriel <56477412+gabbopalma@users.noreply.github.com>
---
maplibre_gl_example/android/settings.gradle | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/maplibre_gl_example/android/settings.gradle b/maplibre_gl_example/android/settings.gradle
index 64a0deb89..b69d934bd 100644
--- a/maplibre_gl_example/android/settings.gradle
+++ b/maplibre_gl_example/android/settings.gradle
@@ -18,7 +18,7 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
- id "com.android.application" version "8.12.0" apply false
+ id "com.android.application" version "8.13.2" apply false
id "org.jetbrains.kotlin.android" version "2.1.0" apply false
}
From 62c3237641e09d4815fa7e0a7821b6bfe4cf3945 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 30 Dec 2025 12:56:02 +0100
Subject: [PATCH 03/38] chore(deps): bump org.maplibre.gl:android-sdk from
11.13.5 to 12.3.0 in /maplibre_gl/android (#690)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps
[org.maplibre.gl:android-sdk](https://github.com/maplibre/maplibre-native)
from 11.13.5 to 12.3.0.
Release notes
Sourced from org.maplibre.gl:android-sdk's
releases .
android-v12.3.0
✨ Features and improvements
Implemented synchronous update for GeoJSON source (#3968 ).
🐞 Bug fixes
Cancel pending style request when loading style JSON (#3989 ).
android-v12.3.0-pre0
Disable icon scaling with offsets #3928 .
Modify the transform implementation to allow for concurrent
animations #3487 .
android-v12.2.2
🐞 Bug fixes
Fix crash due to pure virtual function call v2 (#3996 ).
android-v12.2.1
🐞 Bug fixes
Fix crash due to pure virtual function call (#3979 ).
android-v12.2.0
✨ Features and improvements
Allow setting frustum offset to not render edges of the screen (#3676 ).
🐞 Bug fixes
Fix LineBucket::addGeometry() empty coordinates. (#2959 ).
Use deprecated readParcelable on Tiramisu to avoid crash (#3950 ).
Handle BufferResource::version overflow (#3962 ).
android-v12.1.3
Disable UnsatisfiedLinkError during local tests (#3942 )
android-v12.1.2
Update to latest MLT submodule (#3945 ).
android-v12.1.1
Update to latest MLT submodule (#3945 ).
android-v12.1.1-pre1
No release notes provided.
android-v12.1.0
✨ Features and improvements
Add support for parsing MLT-format vector tile sources (#3246 ).
... (truncated)
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
---------
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Gabriel <56477412+gabbopalma@users.noreply.github.com>
Co-authored-by: Gabriel Palmisano
---
maplibre_gl/android/build.gradle | 2 +-
maplibre_gl_example/lib/animate_camera.dart | 17 +-
maplibre_gl_example/lib/layer.dart | 238 +++++++++++---------
maplibre_gl_example/lib/line.dart | 106 ++++-----
maplibre_gl_example/lib/map_ui.dart | 54 +++--
maplibre_gl_example/lib/move_camera.dart | 17 +-
maplibre_gl_example/lib/place_batch.dart | 29 +--
maplibre_gl_example/lib/place_circle.dart | 152 ++++++-------
maplibre_gl_example/lib/place_fill.dart | 104 ++++-----
maplibre_gl_example/lib/place_source.dart | 121 +++++-----
maplibre_gl_example/lib/place_symbol.dart | 215 ++++++++----------
11 files changed, 517 insertions(+), 538 deletions(-)
diff --git a/maplibre_gl/android/build.gradle b/maplibre_gl/android/build.gradle
index 0f4f6d3f2..71b123704 100644
--- a/maplibre_gl/android/build.gradle
+++ b/maplibre_gl/android/build.gradle
@@ -50,7 +50,7 @@ android {
jvmTarget = JavaVersion.VERSION_21.toString()
}
dependencies {
- implementation 'org.maplibre.gl:android-sdk:11.13.5'
+ implementation 'org.maplibre.gl:android-sdk:12.3.0'
implementation 'org.maplibre.gl:android-plugin-annotation-v9:3.0.2'
implementation 'org.maplibre.gl:android-plugin-offline-v9:3.0.2'
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
diff --git a/maplibre_gl_example/lib/animate_camera.dart b/maplibre_gl_example/lib/animate_camera.dart
index 4ba60672d..3d6addc01 100644
--- a/maplibre_gl_example/lib/animate_camera.dart
+++ b/maplibre_gl_example/lib/animate_camera.dart
@@ -49,10 +49,13 @@ class AnimateCameraState extends State {
const CameraPosition(target: LatLng(0.0, 0.0)),
),
),
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceEvenly,
- children: [
- Column(
+ Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: SingleChildScrollView(
+ child: Wrap(
+ spacing: 4.0,
+ runSpacing: 4.0,
+ alignment: WrapAlignment.center,
children: [
TextButton(
onPressed: () async {
@@ -162,10 +165,6 @@ class AnimateCameraState extends State {
},
child: const Text('setMaximumFps'),
),
- ],
- ),
- Column(
- children: [
TextButton(
onPressed: () async {
await mapController.animateCamera(
@@ -236,7 +235,7 @@ class AnimateCameraState extends State {
),
],
),
- ],
+ ),
)
],
);
diff --git a/maplibre_gl_example/lib/layer.dart b/maplibre_gl_example/lib/layer.dart
index 266d3265b..d7eb8e4ab 100644
--- a/maplibre_gl_example/lib/layer.dart
+++ b/maplibre_gl_example/lib/layer.dart
@@ -62,118 +62,132 @@ class LayerState extends State {
)),
Expanded(
child: SingleChildScrollView(
- child: Column(
- children: [
- TextButton(
- onPressed: () async {
- await controller
- .setLayerProperties(
- "lines",
- LineLayerProperties.fromJson(
- {"visibility": linesVisible ? "none" : "visible"},
- ),
- )
- .then(
- (value) =>
- setState(() => linesVisible = !linesVisible),
- );
- },
- child: const Text('toggle line visibility'),
- ),
- TextButton(
- onPressed: () async {
- await controller
- .setLayerProperties(
- "lines",
- LineLayerProperties.fromJson(
- {"line-color": linesRed ? "#0000ff" : "#ff0000"},
- ),
- )
- .then((value) => setState(() => linesRed = !linesRed));
- },
- child: const Text('toggle line color'),
- ),
- TextButton(
- onPressed: () async {
- await controller
- .setLayerProperties(
- "fills",
- FillLayerProperties.fromJson(
- {"visibility": fillsVisible ? "none" : "visible"},
- ),
- )
- .then(
- (value) =>
- setState(() => fillsVisible = !fillsVisible),
- );
- },
- child: const Text('toggle fill visibility'),
- ),
- TextButton(
- onPressed: () async {
- await controller
- .setLayerProperties(
- "fills",
- FillLayerProperties.fromJson(
- {"fill-color": fillsRed ? "#0000ff" : "#ff0000"},
- ),
- )
- .then(
- (value) => setState(() => fillsRed = !fillsRed),
- );
- },
- child: const Text('toggle fill color'),
- ),
- TextButton(
- onPressed: () async {
- await controller
- .setLayerProperties(
- "circles",
- CircleLayerProperties.fromJson(
- {"visibility": circlesVisible ? "none" : "visible"},
- ),
- )
- .then(
- (value) =>
- setState(() => circlesVisible = !circlesVisible),
- );
- },
- child: const Text('toggle circle visibility'),
- ),
- TextButton(
- onPressed: () async {
- await controller
- .setLayerProperties(
- "circles",
- CircleLayerProperties.fromJson(
- {
- "circle-color": circlesRed ? "#0000ff" : "#ff0000"
- },
- ),
- )
- .then(
- (value) => setState(() => circlesRed = !circlesRed),
- );
- },
- child: const Text('toggle circle color'),
- ),
- TextButton(
- onPressed: () async {
- await controller
- .setLayerProperties(
- "symbols",
- SymbolLayerProperties.fromJson(
- {"visibility": symbolsVisible ? "none" : "visible"},
- ),
- )
- .then(
- (value) =>
- setState(() => symbolsVisible = !symbolsVisible),
- );
- },
- child: const Text('toggle (non-moving) symbols visibility'),
- ),
- ],
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Wrap(
+ spacing: 4.0,
+ runSpacing: 4.0,
+ alignment: WrapAlignment.center,
+ children: [
+ TextButton(
+ onPressed: () async {
+ await controller
+ .setLayerProperties(
+ "lines",
+ LineLayerProperties.fromJson(
+ {"visibility": linesVisible ? "none" : "visible"},
+ ),
+ )
+ .then(
+ (value) =>
+ setState(() => linesVisible = !linesVisible),
+ );
+ },
+ child: const Text('toggle line visibility'),
+ ),
+ TextButton(
+ onPressed: () async {
+ await controller
+ .setLayerProperties(
+ "lines",
+ LineLayerProperties.fromJson(
+ {"line-color": linesRed ? "#0000ff" : "#ff0000"},
+ ),
+ )
+ .then(
+ (value) => setState(() => linesRed = !linesRed));
+ },
+ child: const Text('toggle line color'),
+ ),
+ TextButton(
+ onPressed: () async {
+ await controller
+ .setLayerProperties(
+ "fills",
+ FillLayerProperties.fromJson(
+ {"visibility": fillsVisible ? "none" : "visible"},
+ ),
+ )
+ .then(
+ (value) =>
+ setState(() => fillsVisible = !fillsVisible),
+ );
+ },
+ child: const Text('toggle fill visibility'),
+ ),
+ TextButton(
+ onPressed: () async {
+ await controller
+ .setLayerProperties(
+ "fills",
+ FillLayerProperties.fromJson(
+ {"fill-color": fillsRed ? "#0000ff" : "#ff0000"},
+ ),
+ )
+ .then(
+ (value) => setState(() => fillsRed = !fillsRed),
+ );
+ },
+ child: const Text('toggle fill color'),
+ ),
+ TextButton(
+ onPressed: () async {
+ await controller
+ .setLayerProperties(
+ "circles",
+ CircleLayerProperties.fromJson(
+ {
+ "visibility":
+ circlesVisible ? "none" : "visible"
+ },
+ ),
+ )
+ .then(
+ (value) => setState(
+ () => circlesVisible = !circlesVisible),
+ );
+ },
+ child: const Text('toggle circle visibility'),
+ ),
+ TextButton(
+ onPressed: () async {
+ await controller
+ .setLayerProperties(
+ "circles",
+ CircleLayerProperties.fromJson(
+ {
+ "circle-color":
+ circlesRed ? "#0000ff" : "#ff0000"
+ },
+ ),
+ )
+ .then(
+ (value) => setState(() => circlesRed = !circlesRed),
+ );
+ },
+ child: const Text('toggle circle color'),
+ ),
+ TextButton(
+ onPressed: () async {
+ await controller
+ .setLayerProperties(
+ "symbols",
+ SymbolLayerProperties.fromJson(
+ {
+ "visibility":
+ symbolsVisible ? "none" : "visible"
+ },
+ ),
+ )
+ .then(
+ (value) => setState(
+ () => symbolsVisible = !symbolsVisible),
+ );
+ },
+ child: const Text('toggle (non-moving) symbols visibility'),
+ ),
+ ],
+ ),
),
),
),
diff --git a/maplibre_gl_example/lib/line.dart b/maplibre_gl_example/lib/line.dart
index 1e061f788..3ff8b1872 100644
--- a/maplibre_gl_example/lib/line.dart
+++ b/maplibre_gl_example/lib/line.dart
@@ -194,64 +194,54 @@ class LineBodyState extends State {
),
Expanded(
child: SingleChildScrollView(
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceEvenly,
- children: [
- Column(
- children: [
- Row(
- children: [
- TextButton(
- onPressed: (_lineCount == 12) ? null : _add,
- child: const Text('add'),
- ),
- TextButton(
- onPressed: (_selectedLine == null) ? null : _remove,
- child: const Text('remove'),
- ),
- TextButton(
- onPressed: (_selectedLine == null)
- ? null
- : () async {
- await _move();
- },
- child: const Text('move'),
- ),
- TextButton(
- onPressed: (_selectedLine == null)
- ? null
- : _changeLinePattern,
- child: const Text('change line-pattern'),
- ),
- ],
- ),
- Row(
- children: [
- TextButton(
- onPressed:
- (_selectedLine == null) ? null : _changeAlpha,
- child: const Text('change alpha'),
- ),
- TextButton(
- onPressed:
- (_selectedLine == null) ? null : _toggleVisible,
- child: const Text('toggle visible'),
- ),
- TextButton(
- onPressed: (_selectedLine == null)
- ? null
- : () {
- final latLngs = controller!
- .getLineLatLngs(_selectedLine!);
- debugPrint('Current geometry: $latLngs');
- },
- child: const Text('print current LatLng'),
- ),
- ],
- ),
- ],
- ),
- ],
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Wrap(
+ spacing: 4.0,
+ runSpacing: 4.0,
+ alignment: WrapAlignment.center,
+ children: [
+ TextButton(
+ onPressed: (_lineCount == 12) ? null : _add,
+ child: const Text('add'),
+ ),
+ TextButton(
+ onPressed: (_selectedLine == null) ? null : _remove,
+ child: const Text('remove'),
+ ),
+ TextButton(
+ onPressed: (_selectedLine == null)
+ ? null
+ : () async {
+ await _move();
+ },
+ child: const Text('move'),
+ ),
+ TextButton(
+ onPressed:
+ (_selectedLine == null) ? null : _changeLinePattern,
+ child: const Text('change line-pattern'),
+ ),
+ TextButton(
+ onPressed: (_selectedLine == null) ? null : _changeAlpha,
+ child: const Text('change alpha'),
+ ),
+ TextButton(
+ onPressed: (_selectedLine == null) ? null : _toggleVisible,
+ child: const Text('toggle visible'),
+ ),
+ TextButton(
+ onPressed: (_selectedLine == null)
+ ? null
+ : () {
+ final latLngs =
+ controller!.getLineLatLngs(_selectedLine!);
+ debugPrint('Current geometry: $latLngs');
+ },
+ child: const Text('print current LatLng'),
+ ),
+ ],
+ ),
),
),
),
diff --git a/maplibre_gl_example/lib/map_ui.dart b/maplibre_gl_example/lib/map_ui.dart
index b4014636b..8256db9cd 100644
--- a/maplibre_gl_example/lib/map_ui.dart
+++ b/maplibre_gl_example/lib/map_ui.dart
@@ -427,17 +427,11 @@ class MapUiBodyState extends State {
},
);
- final listViewChildren = [];
+ final controlWidgets = [];
if (mapController != null) {
- listViewChildren.addAll(
+ controlWidgets.addAll(
[
- Text('camera bearing: ${_position.bearing}'),
- Text('camera target: ${_position.target.latitude.toStringAsFixed(4)},'
- '${_position.target.longitude.toStringAsFixed(4)}'),
- Text('camera zoom: ${_position.zoom}'),
- Text('camera tilt: ${_position.tilt}'),
- Text(_isMoving ? '(Camera moving)' : '(Camera idle)'),
_mapSizeToggler(),
_queryFilterToggler(),
_compassToggler(),
@@ -462,16 +456,44 @@ class MapUiBodyState extends State {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
- Center(
- child: SizedBox(
- width: width,
- height: height,
- child: maplibreMap,
- ),
+ Stack(
+ children: [
+ Center(
+ child: SizedBox(
+ width: width,
+ height: height,
+ child: maplibreMap,
+ ),
+ ),
+ Align(
+ alignment: Alignment.topLeft,
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Text(
+ 'Lat: ${_position.target.latitude.toStringAsFixed(4)}, '
+ 'Lng: ${_position.target.longitude.toStringAsFixed(4)}, '
+ 'Zoom: ${_position.zoom.toStringAsFixed(2)}'
+ '${_isMoving ? " (moving)" : ""}',
+ style: const TextStyle(
+ backgroundColor: Colors.white,
+ fontSize: 16.0,
+ ),
+ ),
+ ),
+ ),
+ ],
),
Expanded(
- child: ListView(
- children: listViewChildren,
+ child: SingleChildScrollView(
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Wrap(
+ spacing: 4.0,
+ runSpacing: 4.0,
+ alignment: WrapAlignment.center,
+ children: controlWidgets,
+ ),
+ ),
),
)
],
diff --git a/maplibre_gl_example/lib/move_camera.dart b/maplibre_gl_example/lib/move_camera.dart
index 72e4371a5..052788937 100644
--- a/maplibre_gl_example/lib/move_camera.dart
+++ b/maplibre_gl_example/lib/move_camera.dart
@@ -48,10 +48,13 @@ class MoveCameraState extends State {
const CameraPosition(target: LatLng(0.0, 0.0)),
),
),
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceEvenly,
- children: [
- Column(
+ Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: SingleChildScrollView(
+ child: Wrap(
+ spacing: 4.0,
+ runSpacing: 4.0,
+ alignment: WrapAlignment.center,
children: [
TextButton(
onPressed: () async {
@@ -113,10 +116,6 @@ class MoveCameraState extends State {
},
child: const Text('scrollBy'),
),
- ],
- ),
- Column(
- children: [
TextButton(
onPressed: () async {
await mapController.moveCamera(
@@ -178,7 +177,7 @@ class MoveCameraState extends State {
),
],
),
- ],
+ ),
)
],
);
diff --git a/maplibre_gl_example/lib/place_batch.dart b/maplibre_gl_example/lib/place_batch.dart
index 4ab2bd71e..0a70a405e 100644
--- a/maplibre_gl_example/lib/place_batch.dart
+++ b/maplibre_gl_example/lib/place_batch.dart
@@ -167,23 +167,18 @@ class BatchAddBodyState extends State {
),
Expanded(
child: SingleChildScrollView(
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceEvenly,
- children: [
- Row(
- children: [
- Column(
- children: [
- TextButton(
- onPressed: _add, child: const Text('batch add')),
- TextButton(
- onPressed: _remove,
- child: const Text('batch remove')),
- ],
- ),
- ],
- )
- ],
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Wrap(
+ spacing: 4.0,
+ runSpacing: 4.0,
+ alignment: WrapAlignment.center,
+ children: [
+ TextButton(onPressed: _add, child: const Text('batch add')),
+ TextButton(
+ onPressed: _remove, child: const Text('batch remove')),
+ ],
+ ),
),
),
),
diff --git a/maplibre_gl_example/lib/place_circle.dart b/maplibre_gl_example/lib/place_circle.dart
index f07f9c165..b82c1dc8d 100644
--- a/maplibre_gl_example/lib/place_circle.dart
+++ b/maplibre_gl_example/lib/place_circle.dart
@@ -206,89 +206,75 @@ class PlaceCircleBodyState extends State {
),
Expanded(
child: SingleChildScrollView(
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceEvenly,
- children: [
- Row(
- children: [
- Column(
- children: [
- TextButton(
- onPressed: (_circleCount == 12) ? null : _add,
- child: const Text('add'),
- ),
- TextButton(
- onPressed: (_selectedCircle == null) ? null : _remove,
- child: const Text('remove'),
- ),
- ],
- ),
- Column(
- children: [
- TextButton(
- onPressed: (_selectedCircle == null)
- ? null
- : _changeCircleOpacity,
- child: const Text('change circle-opacity'),
- ),
- TextButton(
- onPressed: (_selectedCircle == null)
- ? null
- : _changeCircleRadius,
- child: const Text('change circle-radius'),
- ),
- TextButton(
- onPressed: (_selectedCircle == null)
- ? null
- : _changeCircleColor,
- child: const Text('change circle-color'),
- ),
- TextButton(
- onPressed: (_selectedCircle == null)
- ? null
- : _changeCircleBlur,
- child: const Text('change circle-blur'),
- ),
- TextButton(
- onPressed: (_selectedCircle == null)
- ? null
- : _changeCircleStrokeWidth,
- child: const Text('change circle-stroke-width'),
- ),
- TextButton(
- onPressed: (_selectedCircle == null)
- ? null
- : _changeCircleStrokeColor,
- child: const Text('change circle-stroke-color'),
- ),
- TextButton(
- onPressed: (_selectedCircle == null)
- ? null
- : _changeCircleStrokeOpacity,
- child: const Text('change circle-stroke-opacity'),
- ),
- TextButton(
- onPressed: (_selectedCircle == null)
- ? null
- : _changePosition,
- child: const Text('change position'),
- ),
- TextButton(
- onPressed: (_selectedCircle == null)
- ? null
- : _changeDraggable,
- child: const Text('toggle draggable'),
- ),
- TextButton(
- onPressed:
- (_selectedCircle == null) ? null : _getLatLng,
- child: const Text('get current LatLng'),
- ),
- ],
- ),
- ],
- )
- ],
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Wrap(
+ spacing: 4.0,
+ runSpacing: 4.0,
+ alignment: WrapAlignment.center,
+ children: [
+ TextButton(
+ onPressed: (_circleCount == 12) ? null : _add,
+ child: const Text('add'),
+ ),
+ TextButton(
+ onPressed: (_selectedCircle == null) ? null : _remove,
+ child: const Text('remove'),
+ ),
+ TextButton(
+ onPressed:
+ (_selectedCircle == null) ? null : _changeCircleOpacity,
+ child: const Text('change circle-opacity'),
+ ),
+ TextButton(
+ onPressed:
+ (_selectedCircle == null) ? null : _changeCircleRadius,
+ child: const Text('change circle-radius'),
+ ),
+ TextButton(
+ onPressed:
+ (_selectedCircle == null) ? null : _changeCircleColor,
+ child: const Text('change circle-color'),
+ ),
+ TextButton(
+ onPressed:
+ (_selectedCircle == null) ? null : _changeCircleBlur,
+ child: const Text('change circle-blur'),
+ ),
+ TextButton(
+ onPressed: (_selectedCircle == null)
+ ? null
+ : _changeCircleStrokeWidth,
+ child: const Text('change circle-stroke-width'),
+ ),
+ TextButton(
+ onPressed: (_selectedCircle == null)
+ ? null
+ : _changeCircleStrokeColor,
+ child: const Text('change circle-stroke-color'),
+ ),
+ TextButton(
+ onPressed: (_selectedCircle == null)
+ ? null
+ : _changeCircleStrokeOpacity,
+ child: const Text('change circle-stroke-opacity'),
+ ),
+ TextButton(
+ onPressed:
+ (_selectedCircle == null) ? null : _changePosition,
+ child: const Text('change position'),
+ ),
+ TextButton(
+ onPressed:
+ (_selectedCircle == null) ? null : _changeDraggable,
+ child: const Text('toggle draggable'),
+ ),
+ TextButton(
+ onPressed: (_selectedCircle == null) ? null : _getLatLng,
+ child: const Text('get current LatLng'),
+ ),
+ ],
+ ),
),
),
),
diff --git a/maplibre_gl_example/lib/place_fill.dart b/maplibre_gl_example/lib/place_fill.dart
index ea719a0ce..8712f59b1 100644
--- a/maplibre_gl_example/lib/place_fill.dart
+++ b/maplibre_gl_example/lib/place_fill.dart
@@ -184,63 +184,53 @@ class PlaceFillBodyState extends State {
),
Expanded(
child: SingleChildScrollView(
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceEvenly,
- children: [
- Row(
- children: [
- Column(
- children: [
- TextButton(
- onPressed: (_fillCount == 12) ? null : _add,
- child: const Text('add'),
- ),
- TextButton(
- onPressed: (_selectedFill == null) ? null : _remove,
- child: const Text('remove'),
- ),
- ],
- ),
- Column(
- children: [
- TextButton(
- onPressed: (_selectedFill == null)
- ? null
- : _changeFillOpacity,
- child: const Text('change fill-opacity'),
- ),
- TextButton(
- onPressed:
- (_selectedFill == null) ? null : _changeFillColor,
- child: const Text('change fill-color'),
- ),
- TextButton(
- onPressed: (_selectedFill == null)
- ? null
- : _changeFillOutlineColor,
- child: const Text('change fill-outline-color'),
- ),
- TextButton(
- onPressed: (_selectedFill == null)
- ? null
- : _changeFillPattern,
- child: const Text('change fill-pattern'),
- ),
- TextButton(
- onPressed:
- (_selectedFill == null) ? null : _changePosition,
- child: const Text('change position'),
- ),
- TextButton(
- onPressed:
- (_selectedFill == null) ? null : _changeDraggable,
- child: const Text('toggle draggable'),
- ),
- ],
- ),
- ],
- )
- ],
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Wrap(
+ spacing: 4.0,
+ runSpacing: 4.0,
+ alignment: WrapAlignment.center,
+ children: [
+ TextButton(
+ onPressed: (_fillCount == 12) ? null : _add,
+ child: const Text('add'),
+ ),
+ TextButton(
+ onPressed: (_selectedFill == null) ? null : _remove,
+ child: const Text('remove'),
+ ),
+ TextButton(
+ onPressed:
+ (_selectedFill == null) ? null : _changeFillOpacity,
+ child: const Text('change fill-opacity'),
+ ),
+ TextButton(
+ onPressed:
+ (_selectedFill == null) ? null : _changeFillColor,
+ child: const Text('change fill-color'),
+ ),
+ TextButton(
+ onPressed: (_selectedFill == null)
+ ? null
+ : _changeFillOutlineColor,
+ child: const Text('change fill-outline-color'),
+ ),
+ TextButton(
+ onPressed:
+ (_selectedFill == null) ? null : _changeFillPattern,
+ child: const Text('change fill-pattern'),
+ ),
+ TextButton(
+ onPressed: (_selectedFill == null) ? null : _changePosition,
+ child: const Text('change position'),
+ ),
+ TextButton(
+ onPressed:
+ (_selectedFill == null) ? null : _changeDraggable,
+ child: const Text('toggle draggable'),
+ ),
+ ],
+ ),
),
),
),
diff --git a/maplibre_gl_example/lib/place_source.dart b/maplibre_gl_example/lib/place_source.dart
index 9cfe6fcdf..7dc75a057 100644
--- a/maplibre_gl_example/lib/place_source.dart
+++ b/maplibre_gl_example/lib/place_source.dart
@@ -104,14 +104,17 @@ class PlaceSymbolBodyState extends State {
@override
Widget build(BuildContext context) {
+ final width = MediaQuery.of(context).size.width;
+ final height = MediaQuery.of(context).size.height;
+
return Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: SizedBox(
- width: 300.0,
- height: 200.0,
+ width: width,
+ height: height * 0.5,
child: MapLibreMap(
onMapCreated: _onMapCreated,
initialCameraPosition: const CameraPosition(
@@ -123,64 +126,62 @@ class PlaceSymbolBodyState extends State {
),
Expanded(
child: SingleChildScrollView(
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceEvenly,
- children: [
- Column(
- children: [
- TextButton(
- onPressed: sourceAdded
- ? null
- : () async {
- await addImageSourceFromAsset(
- sourceId, pickImage())
- .then((value) {
- setState(() => sourceAdded = true);
- });
- },
- child: const Text('Add source (asset image)'),
- ),
- TextButton(
- onPressed: sourceAdded
- ? () async {
- await removeLayer(layerId);
- await removeImageSource(sourceId).then((value) {
- setState(() => sourceAdded = false);
- });
- }
- : null,
- child: const Text('Remove source (asset image)'),
- ),
- TextButton(
- onPressed: sourceAdded
- ? () => addLayer(layerId, sourceId)
- : null,
- child: const Text('Show layer'),
- ),
- TextButton(
- onPressed: sourceAdded
- ? () => addLayerBelow(layerId, sourceId, 'water')
- : null,
- child: const Text('Show layer below water'),
- ),
- TextButton(
- onPressed:
- sourceAdded ? () => removeLayer(layerId) : null,
- child: const Text('Hide layer'),
- ),
- TextButton(
- onPressed: sourceAdded
- ? () async {
- setState(() => imageFlag = !imageFlag);
- await updateImageSourceFromAsset(
- sourceId, pickImage());
- }
- : null,
- child: const Text('Change image'),
- ),
- ],
- ),
- ],
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Wrap(
+ spacing: 4.0,
+ runSpacing: 4.0,
+ alignment: WrapAlignment.center,
+ children: [
+ TextButton(
+ onPressed: sourceAdded
+ ? null
+ : () async {
+ await addImageSourceFromAsset(sourceId, pickImage())
+ .then((value) {
+ setState(() => sourceAdded = true);
+ });
+ },
+ child: const Text('Add source (asset image)'),
+ ),
+ TextButton(
+ onPressed: sourceAdded
+ ? () async {
+ await removeLayer(layerId);
+ await removeImageSource(sourceId).then((value) {
+ setState(() => sourceAdded = false);
+ });
+ }
+ : null,
+ child: const Text('Remove source (asset image)'),
+ ),
+ TextButton(
+ onPressed:
+ sourceAdded ? () => addLayer(layerId, sourceId) : null,
+ child: const Text('Show layer'),
+ ),
+ TextButton(
+ onPressed: sourceAdded
+ ? () => addLayerBelow(layerId, sourceId, 'water')
+ : null,
+ child: const Text('Show layer below water'),
+ ),
+ TextButton(
+ onPressed: sourceAdded ? () => removeLayer(layerId) : null,
+ child: const Text('Hide layer'),
+ ),
+ TextButton(
+ onPressed: sourceAdded
+ ? () async {
+ setState(() => imageFlag = !imageFlag);
+ await updateImageSourceFromAsset(
+ sourceId, pickImage());
+ }
+ : null,
+ child: const Text('Change image'),
+ ),
+ ],
+ ),
),
),
),
diff --git a/maplibre_gl_example/lib/place_symbol.dart b/maplibre_gl_example/lib/place_symbol.dart
index 38fb96efe..f44e2c24e 100644
--- a/maplibre_gl_example/lib/place_symbol.dart
+++ b/maplibre_gl_example/lib/place_symbol.dart
@@ -310,122 +310,105 @@ class PlaceSymbolBodyState extends State {
),
Expanded(
child: SingleChildScrollView(
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceEvenly,
- children: [
- Row(
- children: [
- Column(
- children: [
- TextButton(
- child: const Text('add'),
- onPressed: () => (_symbolCount == 12)
- ? null
- : _add("custom-marker"),
- ),
- TextButton(
- child: const Text('add all'),
- onPressed: () => (_symbolCount == 12)
- ? null
- : _addAll("custom-marker"),
- ),
- TextButton(
- child: const Text('add (custom icon)'),
- onPressed: () => (_symbolCount == 12)
- ? null
- : _add("assets/symbols/custom-icon.png"),
- ),
- TextButton(
- onPressed: (_selectedSymbol == null) ? null : _remove,
- child: const Text('remove'),
- ),
- TextButton(
- onPressed: _changeIconOverlap,
- child: Text(
- '${_iconAllowOverlap ? 'disable' : 'enable'} icon overlap'),
- ),
- TextButton(
- onPressed: (_symbolCount == 0) ? null : _removeAll,
- child: const Text('remove all'),
- ),
- TextButton(
- child: const Text('add (asset image)'),
- onPressed: () => (_symbolCount == 12)
- ? null
- : _add(
- "assetImage"), //assetImage added to the style in _onStyleLoaded
- ),
- TextButton(
- child: const Text('add (network image)'),
- onPressed: () => (_symbolCount == 12)
- ? null
- : _add(
- "networkImage"), //networkImage added to the style in _onStyleLoaded
- ),
- TextButton(
- child: const Text('add (custom font)'),
- onPressed: () =>
- (_symbolCount == 12) ? null : _add("customFont"),
- )
- ],
- ),
- Column(
- children: [
- TextButton(
- onPressed:
- (_selectedSymbol == null) ? null : _changeAlpha,
- child: const Text('change alpha'),
- ),
- TextButton(
- onPressed: (_selectedSymbol == null)
- ? null
- : _changeIconOffset,
- child: const Text('change icon offset'),
- ),
- TextButton(
- onPressed: (_selectedSymbol == null)
- ? null
- : _changeIconAnchor,
- child: const Text('change icon anchor'),
- ),
- TextButton(
- onPressed: (_selectedSymbol == null)
- ? null
- : _toggleDraggable,
- child: const Text('toggle draggable'),
- ),
- TextButton(
- onPressed: (_selectedSymbol == null)
- ? null
- : _changePosition,
- child: const Text('change position'),
- ),
- TextButton(
- onPressed: (_selectedSymbol == null)
- ? null
- : _changeRotation,
- child: const Text('change rotation'),
- ),
- TextButton(
- onPressed:
- (_selectedSymbol == null) ? null : _toggleVisible,
- child: const Text('toggle visible'),
- ),
- TextButton(
- onPressed:
- (_selectedSymbol == null) ? null : _changeZIndex,
- child: const Text('change zIndex'),
- ),
- TextButton(
- onPressed:
- (_selectedSymbol == null) ? null : _getLatLng,
- child: const Text('get current LatLng'),
- ),
- ],
- ),
- ],
- )
- ],
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Wrap(
+ spacing: 4.0,
+ runSpacing: 4.0,
+ alignment: WrapAlignment.center,
+ children: [
+ TextButton(
+ child: const Text('add'),
+ onPressed: () =>
+ (_symbolCount == 12) ? null : _add("custom-marker"),
+ ),
+ TextButton(
+ child: const Text('add all'),
+ onPressed: () =>
+ (_symbolCount == 12) ? null : _addAll("custom-marker"),
+ ),
+ TextButton(
+ child: const Text('add (custom icon)'),
+ onPressed: () => (_symbolCount == 12)
+ ? null
+ : _add("assets/symbols/custom-icon.png"),
+ ),
+ TextButton(
+ onPressed: (_selectedSymbol == null) ? null : _remove,
+ child: const Text('remove'),
+ ),
+ TextButton(
+ onPressed: _changeIconOverlap,
+ child: Text(
+ '${_iconAllowOverlap ? 'disable' : 'enable'} icon overlap'),
+ ),
+ TextButton(
+ onPressed: (_symbolCount == 0) ? null : _removeAll,
+ child: const Text('remove all'),
+ ),
+ TextButton(
+ child: const Text('add (asset image)'),
+ onPressed: () => (_symbolCount == 12)
+ ? null
+ : _add(
+ "assetImage"), //assetImage added to the style in _onStyleLoaded
+ ),
+ TextButton(
+ child: const Text('add (network image)'),
+ onPressed: () => (_symbolCount == 12)
+ ? null
+ : _add(
+ "networkImage"), //networkImage added to the style in _onStyleLoaded
+ ),
+ TextButton(
+ child: const Text('add (custom font)'),
+ onPressed: () =>
+ (_symbolCount == 12) ? null : _add("customFont"),
+ ),
+ TextButton(
+ onPressed: (_selectedSymbol == null) ? null : _changeAlpha,
+ child: const Text('change alpha'),
+ ),
+ TextButton(
+ onPressed:
+ (_selectedSymbol == null) ? null : _changeIconOffset,
+ child: const Text('change icon offset'),
+ ),
+ TextButton(
+ onPressed:
+ (_selectedSymbol == null) ? null : _changeIconAnchor,
+ child: const Text('change icon anchor'),
+ ),
+ TextButton(
+ onPressed:
+ (_selectedSymbol == null) ? null : _toggleDraggable,
+ child: const Text('toggle draggable'),
+ ),
+ TextButton(
+ onPressed:
+ (_selectedSymbol == null) ? null : _changePosition,
+ child: const Text('change position'),
+ ),
+ TextButton(
+ onPressed:
+ (_selectedSymbol == null) ? null : _changeRotation,
+ child: const Text('change rotation'),
+ ),
+ TextButton(
+ onPressed:
+ (_selectedSymbol == null) ? null : _toggleVisible,
+ child: const Text('toggle visible'),
+ ),
+ TextButton(
+ onPressed: (_selectedSymbol == null) ? null : _changeZIndex,
+ child: const Text('change zIndex'),
+ ),
+ TextButton(
+ onPressed: (_selectedSymbol == null) ? null : _getLatLng,
+ child: const Text('get current LatLng'),
+ ),
+ ],
+ ),
),
),
),
From ea2af4ca80b7f5fc5b3bde7c937b289f9a4a8bed Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 30 Dec 2025 12:58:10 +0100
Subject: [PATCH 04/38] chore(deps): bump actions/upload-artifact from 4 to 6
(#688)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps
[actions/upload-artifact](https://github.com/actions/upload-artifact)
from 4 to 6.
Release notes
Sourced from actions/upload-artifact's
releases .
v6.0.0
v6 - What's new
[!IMPORTANT]
actions/upload-artifact@v6 now runs on Node.js 24 (runs.using:
node24) and requires a minimum Actions Runner version of 2.327.1.
If you are using self-hosted runners, ensure they are updated before
upgrading.
Node.js 24
This release updates the runtime to Node.js 24. v5 had preliminary
support for Node.js 24, however this action was by default still running
on Node.js 20. Now this action by default will run on Node.js 24.
What's Changed
Full Changelog : https://github.com/actions/upload-artifact/compare/v5.0.0...v6.0.0
v5.0.0
What's Changed
BREAKING CHANGE: this update supports Node
v24.x. This is not a breaking change per-se but we're
treating it as such.
New Contributors
Full Changelog : https://github.com/actions/upload-artifact/compare/v4...v5.0.0
v4.6.2
What's Changed
New Contributors
Full Changelog : https://github.com/actions/upload-artifact/compare/v4...v4.6.2
v4.6.1
What's Changed
... (truncated)
Commits
b7c566a
Merge pull request #745
from actions/upload-artifact-v6-release
e516bc8
docs: correct description of Node.js 24 support in README
ddc45ed
docs: update README to correct action name for Node.js 24 support
615b319
chore: release v6.0.0 for Node.js 24 support
017748b
Merge pull request #744
from actions/fix-storage-blob
38d4c79
chore: rebuild dist
7d27270
chore: add missing license cache files for @actions/core,
@actions/io, and mi...
5f643d3
chore: update license files for @actions/artifact@5 .0.1 dependencies
1df1684
chore: update package-lock.json with @actions/artifact@5 .0.1
b5b1a91
fix: update @actions/artifact to ^5.0.0 for Node.js 24
punycode fix
Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Gabriel <56477412+gabbopalma@users.noreply.github.com>
---
.github/workflows/flutter_beta.yml | 4 ++--
.github/workflows/flutter_ci.yml | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/flutter_beta.yml b/.github/workflows/flutter_beta.yml
index da40e3db7..e636817b6 100644
--- a/.github/workflows/flutter_beta.yml
+++ b/.github/workflows/flutter_beta.yml
@@ -29,7 +29,7 @@ jobs:
- name: Build example APK
run: flutter build apk
- name: Upload apk as artifact
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v6
with:
name: maplibre-flutter-demo.apk
path: maplibre_gl_example/build/app/outputs/flutter-apk/app-release.apk
@@ -53,7 +53,7 @@ jobs:
- name: Build iOS package
run: flutter build ios --simulator
- name: Upload Runner.app as artifact
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v6
with:
name: maplibre-flutter-demo.app
path: maplibre_gl_example/build/ios/iphonesimulator
diff --git a/.github/workflows/flutter_ci.yml b/.github/workflows/flutter_ci.yml
index 6167b7130..3e0603f8c 100644
--- a/.github/workflows/flutter_ci.yml
+++ b/.github/workflows/flutter_ci.yml
@@ -106,7 +106,7 @@ jobs:
- name: Build example APK
run: flutter build apk
- name: Upload apk as artifact
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v6
with:
name: maplibre-flutter-demo.apk
path: maplibre_gl_example/build/app/outputs/flutter-apk/app-release.apk
@@ -132,7 +132,7 @@ jobs:
- name: Build iOS package
run: flutter build ios --simulator
- name: Upload Runner.app as artifact
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v6
with:
name: maplibre-flutter-demo.app
path: maplibre_gl_example/build/ios/iphonesimulator
From fcdd8be602f02192c53d0eeb1f8e10c02f1fd919 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 30 Dec 2025 13:08:38 +0100
Subject: [PATCH 05/38] chore(deps): bump com.android.tools.build:gradle from
8.13.0 to 8.13.1 in /maplibre_gl/android in the gradle-plugin group (#674)
Bumps the gradle-plugin group in /maplibre_gl/android with 1 update:
com.android.tools.build:gradle.
Updates `com.android.tools.build:gradle` from 8.13.0 to 8.13.1
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore ` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore ` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore ` will
remove the ignore condition of the specified dependency and ignore
conditions
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
maplibre_gl/android/build.gradle | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/maplibre_gl/android/build.gradle b/maplibre_gl/android/build.gradle
index 71b123704..a1cd03ebd 100644
--- a/maplibre_gl/android/build.gradle
+++ b/maplibre_gl/android/build.gradle
@@ -9,7 +9,7 @@ buildscript {
}
dependencies {
- classpath 'com.android.tools.build:gradle:8.13.0'
+ classpath 'com.android.tools.build:gradle:8.13.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
From a347dfee0fd46c27d37f1a9ba75bf57b14d03f8a Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 30 Dec 2025 13:11:05 +0100
Subject: [PATCH 06/38] chore(deps): bump actions/checkout from 5 to 6 (#672)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to
6.
Release notes
Sourced from actions/checkout's
releases .
v6.0.0
What's Changed
Full Changelog : https://github.com/actions/checkout/compare/v5.0.0...v6.0.0
v6-beta
What's Changed
Updated persist-credentials to store the credentials under
$RUNNER_TEMP instead of directly in the local git
config.
This requires a minimum Actions Runner version of v2.329.0
to access the persisted credentials for Docker
container action scenarios.
v5.0.1
What's Changed
Full Changelog : https://github.com/actions/checkout/compare/v5...v5.0.1
Changelog
Sourced from actions/checkout's
changelog .
Changelog
V6.0.0
V5.0.1
V5.0.0
V4.3.1
V4.3.0
v4.2.2
v4.2.1
v4.2.0
v4.1.7
v4.1.6
v4.1.5
... (truncated)
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Gabriel <56477412+gabbopalma@users.noreply.github.com>
---
.github/workflows/dependency-review.yml | 2 +-
.github/workflows/flutter_beta.yml | 6 +++---
.github/workflows/flutter_ci.yml | 16 ++++++++--------
.github/workflows/publish-single.yml | 2 +-
4 files changed, 13 insertions(+), 13 deletions(-)
diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml
index 046e9c88c..3ea91b44e 100644
--- a/.github/workflows/dependency-review.yml
+++ b/.github/workflows/dependency-review.yml
@@ -15,6 +15,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
- name: 'Dependency Review'
uses: actions/dependency-review-action@v4
diff --git a/.github/workflows/flutter_beta.yml b/.github/workflows/flutter_beta.yml
index e636817b6..a9901ffff 100644
--- a/.github/workflows/flutter_beta.yml
+++ b/.github/workflows/flutter_beta.yml
@@ -15,7 +15,7 @@ jobs:
run:
working-directory: maplibre_gl_example
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- uses: actions/setup-java@v5
with:
java-version: '17'
@@ -41,7 +41,7 @@ jobs:
run:
working-directory: maplibre_gl_example
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- uses: subosito/flutter-action@v2
with:
channel: ${{ env.FLUTTER_CHANNEL }}
@@ -65,7 +65,7 @@ jobs:
run:
working-directory: maplibre_gl_example
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- uses: subosito/flutter-action@v2
with:
channel: ${{ env.FLUTTER_CHANNEL }}
diff --git a/.github/workflows/flutter_ci.yml b/.github/workflows/flutter_ci.yml
index 3e0603f8c..c052acd17 100644
--- a/.github/workflows/flutter_ci.yml
+++ b/.github/workflows/flutter_ci.yml
@@ -12,7 +12,7 @@ jobs:
name: "Check formatting"
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- uses: subosito/flutter-action@v2
with:
channel: stable
@@ -27,7 +27,7 @@ jobs:
name: "Static code analysis"
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- uses: subosito/flutter-action@v2
with:
channel: stable
@@ -42,7 +42,7 @@ jobs:
name: "Run tests"
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- uses: subosito/flutter-action@v2
with:
channel: stable
@@ -56,7 +56,7 @@ jobs:
name: "Run web tests"
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- uses: subosito/flutter-action@v2
with:
channel: stable
@@ -70,7 +70,7 @@ jobs:
name: "Generate code from templates"
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- uses: subosito/flutter-action@v2
with:
channel: stable
@@ -90,7 +90,7 @@ jobs:
run:
working-directory: maplibre_gl_example
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- uses: subosito/flutter-action@v2
with:
channel: stable
@@ -118,7 +118,7 @@ jobs:
run:
working-directory: maplibre_gl_example
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- uses: subosito/flutter-action@v2
with:
channel: stable
@@ -144,7 +144,7 @@ jobs:
run:
working-directory: maplibre_gl_example
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- uses: subosito/flutter-action@v2
with:
channel: stable
diff --git a/.github/workflows/publish-single.yml b/.github/workflows/publish-single.yml
index e274cedce..137c08552 100644
--- a/.github/workflows/publish-single.yml
+++ b/.github/workflows/publish-single.yml
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
if: github.repository_owner == 'maplibre'
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- uses: dart-lang/setup-dart@v1
# --force skips the y/N confirmation
# --skip-validation because of "Because maplibre_gl requires the Flutter SDK, version solving failed. Flutter users should use `flutter pub` instead of `dart pub`."
From ce098b662c1680e88d8ddbfecc68874d85a4f3e5 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 30 Dec 2025 13:15:03 +0100
Subject: [PATCH 07/38] chore(deps): bump com.squareup.okhttp3:okhttp from
4.12.0 to 5.3.2 in /maplibre_gl/android (#676)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [com.squareup.okhttp3:okhttp](https://github.com/square/okhttp)
from 4.12.0 to 5.3.2.
Changelog
Sourced from com.squareup.okhttp3:okhttp's
changelog .
Version 5.3.2
2025-11-18
Version 5.3.1
2025-11-16
This release is the same as 5.3.0. Okio 3.16.3 didn't have a
necessary fix!
Upgrade: [Okio 3.16.3][okio_3_16_3].
Version 5.3.0
2025-10-30
New: Add tags to Call, including computable tags. Use
this to attach application-specific
metadata to a Call in an EventListener or
Interceptor. The tag can be read in any other
EventListener or Interceptor.
override fun intercept(chain:
Interceptor.Chain): Response {
chain.call().tag(MyAnalyticsTag::class) {
MyAnalyticsTag(...)
}
return chain.proceed(chain.request())
}
New: Support request bodies on HTTP/1.1 connection upgrades.
New: EventListener.plus() makes it easier to observe
events in multiple listeners.
Fix: Don't spam logs with ‘Method isLoggable in android.util.Log
not mocked.’ when using
OkHttp in Robolectric and Paparazzi tests.
Upgrade: [Kotlin 2.2.21][kotlin_2_2_21].
Upgrade: [Okio 3.16.2][okio_3_16_2].
Upgrade: [ZSTD-KMP 0.4.0][zstd_kmp_0_4_0]. This update fixes a bug
that caused APKs to fail
[16 KB ELF alignment checks][elf_alignment].
Version 5.2.3
2025-11-18
... (truncated)
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Gabriel <56477412+gabbopalma@users.noreply.github.com>
---
maplibre_gl/android/build.gradle | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/maplibre_gl/android/build.gradle b/maplibre_gl/android/build.gradle
index a1cd03ebd..3c3e5e057 100644
--- a/maplibre_gl/android/build.gradle
+++ b/maplibre_gl/android/build.gradle
@@ -53,7 +53,7 @@ android {
implementation 'org.maplibre.gl:android-sdk:12.3.0'
implementation 'org.maplibre.gl:android-plugin-annotation-v9:3.0.2'
implementation 'org.maplibre.gl:android-plugin-offline-v9:3.0.2'
- implementation 'com.squareup.okhttp3:okhttp:4.12.0'
+ implementation 'com.squareup.okhttp3:okhttp:5.3.2'
}
}
From 9cf1b63b378614dcb377aab5277207a82bc87ca3 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 30 Dec 2025 13:53:22 +0100
Subject: [PATCH 08/38] chore(deps): bump
org.jetbrains.kotlin:kotlin-gradle-plugin from 2.1.0 to 2.2.20 in
/maplibre_gl/android in the kotlin group (#634)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps the kotlin group in /maplibre_gl/android with 1 update:
[org.jetbrains.kotlin:kotlin-gradle-plugin](https://github.com/JetBrains/kotlin).
Updates `org.jetbrains.kotlin:kotlin-gradle-plugin` from 2.1.0 to 2.2.20
Release notes
Sourced from org.jetbrains.kotlin:kotlin-gradle-plugin's
releases .
Kotlin 2.2.20
Changelog
Analysis API
KT-78187
Synthetic properties not to be shown as callables
KT-72525
K2. red code and KIWA on new-lines in guarded when conditions (with
parentheses)
KT-74246
KaVisibilityChecker.isVisible is inefficient with multiple calls on the
same use-site
Analysis API. Code Compilation
KT-78382
K2 IR lowering error when interface extends interface
KT-73201
K2 IDE: Error while evaluating expressions with local classes
KT-78164
Evaluator: '@JvmName' annotations are not recognized in
other modules
KT-76457
K2 IDE / KMP Debugger: KISEWA “Cannot compile a common source without a
JVM counterpart” on evaluating inline fun from common module inside
jvm
KT-73084
K2 evaluator cannot resolve local variables standing at the closing
brace
Analysis API. FIR
Performance Improvements
KT-76490
Do not load ast during the contracts phase if no contracts present
KT-78132
Do not check FirElementBuilder#tryGetFirWithoutBodyResolve optimization
for already resolved declarations
Fixes
KT-72227
SOE from recursive value class
KT-68977
K2 IDE: Reference to companion object through typealias in a function
call does not work
KT-72357
Implement partial body resolution
KT-76932
Support context parameters on dangling modifier list
KT-72407
FirImplementationByDelegationWithDifferentGenericSignatureChecker:
FirLazyExpression should be calculated before accessing
KT-77602
K2 / Analysis API: KAEWA “No fir element was found for KtParameter” on
incorrect context()-call
KT-77629
K2: NPE:
"org.jetbrains.kotlin.fir.java.declarations.FirJavaTypeParameter.performFirstRoundOfBoundsResolution"
KT-76855
Analysis API: KaType.asPsiType returns null
for a local inner class in dependent analysis tests
KT-72718
ImplicitReceiverValue.createSnapshot creates invalid FIR if receiver is
smart-casted
KT-76811
Analysis API: resolveToFirSymbol finds a
FirPropertySymbol for a KtScript in dependent
analysis
KT-73586
[Analysis API] Add lazyResolveToPhase(STATUS) before
accessing modifiers of members
KT-71135
AA: exception from sealed inheritors checker when
analyzeCopy
KT-75534
K2 AA: "Containing declaration should present for nested
declaration class KtNamedFunction" with dangling annotation on
top-level anonymous function
KT-75687
K2: local variable doesn't get to the do-while scope
KT-56543
LL FIR: rework lazy transformers so transformers modify only
declarations they suppose to
Analysis API. Infrastructure
KT-76809
Analysis API: Dependent analysis tests frequently work with the original
element instead of the copied element
Analysis API. Light Classes
KT-78835
Find usages of a light constructor from a class with an empty body finds
usages of class as well
KT-78878
K2. Method shown as unavailable in Java when
@JvmExposeBoxed is applied (redundantly) at both class and
method level in Kotlin
... (truncated)
Changelog
Sourced from org.jetbrains.kotlin:kotlin-gradle-plugin's
changelog .
2.2.20
Analysis API
KT-78187
Synthetic properties not to be shown as callables
KT-72525
K2. red code and KIWA on new-lines in guarded when conditions (with
parentheses)
KT-74246
KaVisibilityChecker.isVisible is inefficient with multiple calls on the
same use-site
Analysis API. Code Compilation
KT-78382
K2 IR lowering error when interface extends interface
KT-73201
K2 IDE: Error while evaluating expressions with local classes
KT-78164
Evaluator: '@JvmName' annotations are not recognized in
other modules
KT-76457
K2 IDE / KMP Debugger: KISEWA “Cannot compile a common source without a
JVM counterpart” on evaluating inline fun from common module inside
jvm
KT-73084
K2 evaluator cannot resolve local variables standing at the closing
brace
Analysis API. FIR
Performance Improvements
KT-76490
Do not load ast during the contracts phase if no contracts present
KT-78132
Do not check FirElementBuilder#tryGetFirWithoutBodyResolve optimization
for already resolved declarations
Fixes
KT-72227
SOE from recursive value class
KT-68977
K2 IDE: Reference to companion object through typealias in a function
call does not work
KT-72357
Implement partial body resolution
KT-76932
Support context parameters on dangling modifier list
KT-72407
FirImplementationByDelegationWithDifferentGenericSignatureChecker:
FirLazyExpression should be calculated before accessing
KT-77602
K2 / Analysis API: KAEWA “No fir element was found for KtParameter” on
incorrect context()-call
KT-77629
K2: NPE:
"org.jetbrains.kotlin.fir.java.declarations.FirJavaTypeParameter.performFirstRoundOfBoundsResolution"
KT-76855
Analysis API: KaType.asPsiType returns null
for a local inner class in dependent analysis tests
KT-72718
ImplicitReceiverValue.createSnapshot creates invalid FIR if receiver is
smart-casted
KT-76811
Analysis API: resolveToFirSymbol finds a
FirPropertySymbol for a KtScript in dependent
analysis
KT-73586
[Analysis API] Add lazyResolveToPhase(STATUS) before
accessing modifiers of members
KT-71135
AA: exception from sealed inheritors checker when
analyzeCopy
KT-75534
K2 AA: "Containing declaration should present for nested
declaration class KtNamedFunction" with dangling annotation on
top-level anonymous function
KT-75687
K2: local variable doesn't get to the do-while scope
KT-56543
LL FIR: rework lazy transformers so transformers modify only
declarations they suppose to
Analysis API. Infrastructure
KT-76809
Analysis API: Dependent analysis tests frequently work with the original
element instead of the copied element
Analysis API. Light Classes
KT-78835
Find usages of a light constructor from a class with an empty body finds
usages of class as well
KT-78878
K2. Method shown as unavailable in Java when
@JvmExposeBoxed is applied (redundantly) at both class and
method level in Kotlin
KT-78065
Support "Expose boxed inline value classes" in Light
Classes
... (truncated)
Commits
693c44e
Add ChangeLog for 2.2.20-RC2
5b7c7af
[Gradle] Fail the build if AGP has already configured Kotlin in the
project
1756c32
Add permissions for GRADLE_RO_DEP_CACHE to security policy
05dcf52
[Native Macos] update llvm with fixes for xcode26 ^KT-79571 fixed
0b2dd95
[Wasm] Do not backport devServer, because it is mutable collection
6b0a1e4
[IR] Use sanitized names when calculating scopes for lambdas
64daa7e
[FIR2IR] Properly handle generics with nullable types in delegate body
genera...
9237f28
[Test] Reproduce KT-79816
e86b28e
[Gradle] Add @ExperimentalKotlinGradlePluginApi
to exportKdoc
0f5c8a7
Add ChangeLog for 2.2.20-RC
Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore ` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore ` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore ` will
remove the ignore condition of the specified dependency and ignore
conditions
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Gabriel <56477412+gabbopalma@users.noreply.github.com>
---
maplibre_gl/android/build.gradle | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/maplibre_gl/android/build.gradle b/maplibre_gl/android/build.gradle
index 3c3e5e057..25cba6e5a 100644
--- a/maplibre_gl/android/build.gradle
+++ b/maplibre_gl/android/build.gradle
@@ -2,7 +2,7 @@ group 'org.maplibre.maplibregl'
version '1.0-SNAPSHOT'
buildscript {
- ext.kotlin_version = '2.1.0'
+ ext.kotlin_version = '2.3.0'
repositories {
google()
mavenCentral()
From 556eb9e83cf6f48b9f5c52df753f84b24b6a6e94 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 30 Dec 2025 14:15:49 +0100
Subject: [PATCH 09/38] chore(deps): bump org.jetbrains.kotlin.android from
2.1.0 to 2.2.21 in /maplibre_gl_example/android (#665)
Bumps
[org.jetbrains.kotlin.android](https://github.com/JetBrains/kotlin) from
2.1.0 to 2.2.21.
Release notes
Sourced from org.jetbrains.kotlin.android's
releases .
Kotlin 2.2.21
Changelog
Backend. Wasm
KT-81372
K/Wasm: JsException: Exception was thrown while running JavaScript code
on Safari 18.2/18.3
KT-80018
K/Wasm: exceptions don't work properly in JavaScriptCore (vm inside
Safari, WebKit)
Compiler
KT-81191
K2: "null cannot be cast to non-null type
ConeTypeParameterLookupTag" with invalid code
KT-80936
NON_PUBLIC_CALL_FROM_PUBLIC_INLINE : @PublishedApi doesn't
work for fun interfaces
JavaScript
KT-79926
Wrong export of interfaces with companions with ES Modules
KT-81424
Kotlin/JS: Cannot Get / in a simple running application
KT-80873
KJS: Stdlib requires ES2020-compatible JS engine due to BigInt type
literal
Native
KT-79384
K/N: Application Not Responding: Thread Deadlock
Tools. Gradle
KT-79047
Gradle compileKotlin fails with configuration cache
KT-81148
Publishing helpers in KGP are incompatible with Isolated Projects
KT-80950
KGP breaks configuration cache when signing plugin with GnuPG is
applied
Tools. Gradle. Multiplatform
KT-61127
Remove scoped resolvable and intransitive DependenciesMetadata
configurations used in the pre-IdeMultiplatformImport IDE import
KT-81249
Kotlin 2.2.20 broke KMP implementation of Parcelize
Tools. Gradle. Native
KT-81510
commonizeCInterop exception with
'kotlinNativeBundleConfiguration' not found
KT-81134
Native: Gradle configuration failure likely related to Klibs
cross-compilation
KT-77732
commonizeCInterop failed with "Unresolved classifier:
platform/posix/size_t"
KT-80675
Commonized cinterops between "test" compilations produce an
import failure
Tools. Maven
KT-81218
Kotlin Maven Plugin 2.2.20: Java classes not resolved with enabled
incremental compilation without daemon
Tools. Wasm
KT-80582
Multiple reloads when using webpack dev server after 2.2.20-Beta2
Kotlin 2.2.21-RC2
... (truncated)
Changelog
Sourced from org.jetbrains.kotlin.android's
changelog .
2.2.21
Backend. Wasm
KT-81372
K/Wasm: JsException: Exception was thrown while running JavaScript code
on Safari 18.2/18.3
KT-80018
K/Wasm: exceptions don't work properly in JavaScriptCore (vm inside
Safari, WebKit)
Compiler
KT-81191
K2: "null cannot be cast to non-null type
ConeTypeParameterLookupTag" with invalid code
KT-80936
NON_PUBLIC_CALL_FROM_PUBLIC_INLINE : @PublishedApi doesn't
work for fun interfaces
JavaScript
KT-79926
Wrong export of interfaces with companions with ES Modules
KT-81424
Kotlin/JS: Cannot Get / in a simple running application
KT-80873
KJS: Stdlib requires ES2020-compatible JS engine due to BigInt type
literal
Native
KT-79384
K/N: Application Not Responding: Thread Deadlock
Tools. Gradle
KT-79047
Gradle compileKotlin fails with configuration cache
KT-81148
Publishing helpers in KGP are incompatible with Isolated Projects
KT-80950
KGP breaks configuration cache when signing plugin with GnuPG is
applied
Tools. Gradle. Multiplatform
KT-61127
Remove scoped resolvable and intransitive DependenciesMetadata
configurations used in the pre-IdeMultiplatformImport IDE import
KT-81249
Kotlin 2.2.20 broke KMP implementation of Parcelize
Tools. Gradle. Native
KT-81510
commonizeCInterop exception with
'kotlinNativeBundleConfiguration' not found
KT-81134
Native: Gradle configuration failure likely related to Klibs
cross-compilation
KT-77732
commonizeCInterop failed with "Unresolved classifier:
platform/posix/size_t"
KT-80675
Commonized cinterops between "test" compilations produce an
import failure
Tools. Maven
KT-81218
Kotlin Maven Plugin 2.2.20: Java classes not resolved with enabled
incremental compilation without daemon
Tools. Wasm
KT-80582
Multiple reloads when using webpack dev server after 2.2.20-Beta2
2.2.20
... (truncated)
Commits
2146684
Add ChangeLog for 2.2.21-RC2
d8cf44a
[KGP][IT] Require Xcode 26 for
shouldDownloadLightNativeBundleWithMaven
bd2b426
[Gradle] Only register commonizeCInterop if there are native
targets
f66516e
[Gradle] Added tests for accessing target's publishable property
7aad8e5
[Gradle] Workaround for not completable Future with cross
compilation
d061774
[Wasm, JS] Add statics field to DevServer data constructor for data
class
0609896
Add ChangeLog for 2.2.21-RC
4f2bc0c
[Gradle] Dont add parcelize plugin to JVM compilations
948802f
[K/N][tests] Fixed lldb tests to work with Xcode 26
a32c8f3
[stdlib] Add os.arch as an input property to prevent build
cache reuse acro...
Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Gabriel <56477412+gabbopalma@users.noreply.github.com>
---
maplibre_gl_example/android/settings.gradle | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/maplibre_gl_example/android/settings.gradle b/maplibre_gl_example/android/settings.gradle
index b69d934bd..23b1cc95b 100644
--- a/maplibre_gl_example/android/settings.gradle
+++ b/maplibre_gl_example/android/settings.gradle
@@ -19,7 +19,7 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.13.2" apply false
- id "org.jetbrains.kotlin.android" version "2.1.0" apply false
+ id "org.jetbrains.kotlin.android" version "2.3.0" apply false
}
include ":app"
\ No newline at end of file
From e19216d5c9276984addc27bc7a7ec97577888eb8 Mon Sep 17 00:00:00 2001
From: Albert Moravec
Date: Tue, 30 Dec 2025 20:18:01 +0100
Subject: [PATCH 10/38] Implemented explicit annotation manager initialization
(#668)
This PR separates `AnnotationManager` initialization logic into a
separate `initialize()` method which can be awaited. This way annotation
managers can be predictably initialized.
Previous initialization logic possibly led to situations where the
manager was used before its initialization finished. It is practically
impossible to avoid it because you cannot know if and when does the
initialization finish. This was especially the case when creating custom
`AnnotationManager` instances on the fly.
This changes how annotation managers are used and users are now required
to call `initialize()` before using the manager, so this is a breaking
change.
Also map style loaded callback is now only called after all the managers
finished loading, which adds some possibly unnecessary load time, but
prevents some nasty errors.
### Alternatives?
I'm open to different solutions - normally I'm opposed to initialization
methods which have to be called manually, but here it seemed like to
most straightforward solution. It is a bit clunky with the
`_initilizing` and `_initialized` flags, I'll admit that.
---------
Co-authored-by: Gabriel <56477412+gabbopalma@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Gabriel Palmisano
---
maplibre_gl/lib/src/annotation_manager.dart | 56 +++-
maplibre_gl/lib/src/controller.dart | 10 +-
maplibre_gl_example/lib/animate_camera.dart | 330 ++++++++++----------
3 files changed, 217 insertions(+), 179 deletions(-)
diff --git a/maplibre_gl/lib/src/annotation_manager.dart b/maplibre_gl/lib/src/annotation_manager.dart
index c15bdb78b..bf1f3b4b0 100644
--- a/maplibre_gl/lib/src/annotation_manager.dart
+++ b/maplibre_gl/lib/src/annotation_manager.dart
@@ -4,6 +4,10 @@ part of '../maplibre_gl.dart';
/// owning their backing style source(s)/layer(s) and performing efficient
/// batched updates.
///
+/// The [initialize] method must be called before [AnnotationManager] instance
+/// can be used. Once [AnnotationManager] is initialized, the [isInitialized]
+/// getter will return true.
+///
/// An [AnnotationManager] keeps an internal mapping from annotation id to its
/// model object and mirrors the collection into one or more GeoJSON sources;
/// each source is bound to a style layer whose visual properties come from
@@ -13,12 +17,18 @@ part of '../maplibre_gl.dart';
/// underlying annotation is translated & re-set.
abstract class AnnotationManager {
final MapLibreMapController controller;
+
+ bool _isInitializing = false;
+ bool _isInitialized = false;
final _idToAnnotation = {};
final _idToLayerIndex = {};
/// Base identifier of the manager. Use [layerIds] for concrete layer ids.
final String id;
+ /// Tracks whether the manager and its layers were initialized.
+ bool get isInitialized => _isInitialized;
+
List get layerIds =>
[for (int i = 0; i < allLayerProperties.length; i++) _makeLayerId(i)];
@@ -45,20 +55,42 @@ abstract class AnnotationManager {
this.controller, {
this.selectLayer,
required this.enableInteraction,
- }) : id = getRandomString() {
- for (var i = 0; i < allLayerProperties.length; i++) {
- final layerId = _makeLayerId(i);
- unawaited(controller.addGeoJsonSource(layerId, buildFeatureCollection([]),
- promoteId: "id"));
- unawaited(controller.addLayer(
- layerId,
- layerId,
- allLayerProperties[i],
- enableInteraction: enableInteraction,
- ));
+ }) : id = getRandomString();
+
+ @mustCallSuper
+ Future initialize() async {
+ if (_isInitializing || _isInitialized) {
+ return;
}
- controller.onFeatureDrag.add(_onDrag);
+ // Mark initialization process start, so that it cannot be entered again
+ _isInitializing = true;
+
+ try {
+ for (var i = 0; i < allLayerProperties.length; i++) {
+ final layerId = _makeLayerId(i);
+
+ await controller.addGeoJsonSource(
+ layerId,
+ buildFeatureCollection([]),
+ promoteId: "id",
+ );
+ await controller.addLayer(
+ layerId,
+ layerId,
+ allLayerProperties[i],
+ enableInteraction: enableInteraction,
+ );
+ }
+
+ controller.onFeatureDrag.add(_onDrag);
+
+ // Mark as initialized
+ _isInitialized = true;
+ } finally {
+ // Mark initialization process end
+ _isInitializing = false;
+ }
}
/// Rebuilds all backing style layers (e.g. after overlap settings changed).
diff --git a/maplibre_gl/lib/src/controller.dart b/maplibre_gl/lib/src/controller.dart
index 0ebf3552b..bd8248fe0 100644
--- a/maplibre_gl/lib/src/controller.dart
+++ b/maplibre_gl/lib/src/controller.dart
@@ -194,7 +194,7 @@ class MapLibreMapController extends ChangeNotifier {
notifyListeners();
});
- _maplibrePlatform.onMapStyleLoadedPlatform.add((_) {
+ _maplibrePlatform.onMapStyleLoadedPlatform.add((_) async {
final interactionEnabled = annotationConsumeTapEvents.toSet();
for (final type in annotationOrder.toSet()) {
final enableInteraction = interactionEnabled.contains(type);
@@ -204,21 +204,25 @@ class MapLibreMapController extends ChangeNotifier {
this,
enableInteraction: enableInteraction,
);
+ await fillManager!.initialize();
case AnnotationType.line:
lineManager = LineManager(
this,
enableInteraction: enableInteraction,
);
+ await lineManager!.initialize();
case AnnotationType.circle:
circleManager = CircleManager(
this,
enableInteraction: enableInteraction,
);
+ await circleManager!.initialize();
case AnnotationType.symbol:
symbolManager = SymbolManager(
this,
enableInteraction: enableInteraction,
);
+ await symbolManager!.initialize();
}
}
onStyleLoadedCallback?.call();
@@ -1700,8 +1704,8 @@ class MapLibreMapController extends ChangeNotifier {
/// Ensures that the given manager is initialized.
/// If not, throws an [Exception].
- void _ensureManagerInitialized(Object? manager) {
- if (manager == null) {
+ void _ensureManagerInitialized(AnnotationManager? manager) {
+ if (manager == null || !manager.isInitialized) {
throw Exception(
"This Annotation Manager has not been initialized. Make sure that the map style has been loaded.",
);
diff --git a/maplibre_gl_example/lib/animate_camera.dart b/maplibre_gl_example/lib/animate_camera.dart
index 3d6addc01..d95210a67 100644
--- a/maplibre_gl_example/lib/animate_camera.dart
+++ b/maplibre_gl_example/lib/animate_camera.dart
@@ -49,194 +49,196 @@ class AnimateCameraState extends State {
const CameraPosition(target: LatLng(0.0, 0.0)),
),
),
- Padding(
- padding: const EdgeInsets.all(8.0),
- child: SingleChildScrollView(
- child: Wrap(
- spacing: 4.0,
- runSpacing: 4.0,
- alignment: WrapAlignment.center,
- children: [
- TextButton(
- onPressed: () async {
- await mapController
- .animateCamera(
- CameraUpdate.newCameraPosition(
- const CameraPosition(
- bearing: 270.0,
- target: LatLng(51.5160895, -0.1294527),
- tilt: 30.0,
- zoom: 17.0,
- ),
- ),
- )
- .then(
- (result) => debugPrint(
- "mapController.animateCamera() returned $result"),
- );
- },
- child: const Text('newCameraPosition'),
- ),
- if (!kIsWeb)
+ Expanded(
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: SingleChildScrollView(
+ child: Wrap(
+ spacing: 4.0,
+ runSpacing: 4.0,
+ alignment: WrapAlignment.center,
+ children: [
TextButton(
onPressed: () async {
await mapController
- .easeCamera(
+ .animateCamera(
CameraUpdate.newCameraPosition(
const CameraPosition(
bearing: 270.0,
- target: LatLng(46.233487, 14.363610),
+ target: LatLng(51.5160895, -0.1294527),
tilt: 30.0,
zoom: 17.0,
),
),
- duration: const Duration(seconds: 2),
)
.then(
(result) => debugPrint(
- "mapController.easeCamera() returned $result"),
+ "mapController.animateCamera() returned $result"),
);
},
- child: const Text('easeCamera'),
+ child: const Text('newCameraPosition'),
),
- TextButton(
- onPressed: () async {
- await mapController
- .animateCamera(
- CameraUpdate.newLatLng(
- const LatLng(56.1725505, 10.1850512),
+ if (!kIsWeb)
+ TextButton(
+ onPressed: () async {
+ await mapController
+ .easeCamera(
+ CameraUpdate.newCameraPosition(
+ const CameraPosition(
+ bearing: 270.0,
+ target: LatLng(46.233487, 14.363610),
+ tilt: 30.0,
+ zoom: 17.0,
+ ),
+ ),
+ duration: const Duration(seconds: 2),
+ )
+ .then(
+ (result) => debugPrint(
+ "mapController.easeCamera() returned $result"),
+ );
+ },
+ child: const Text('easeCamera'),
+ ),
+ TextButton(
+ onPressed: () async {
+ await mapController
+ .animateCamera(
+ CameraUpdate.newLatLng(
+ const LatLng(56.1725505, 10.1850512),
+ ),
+ duration: const Duration(seconds: 5),
+ )
+ .then((result) => debugPrint(
+ "mapController.animateCamera() returned $result"));
+ },
+ child: const Text('newLatLng'),
+ ),
+ TextButton(
+ onPressed: () async {
+ await mapController.animateCamera(
+ CameraUpdate.newLatLngBounds(
+ LatLngBounds(
+ southwest: const LatLng(-38.483935, 113.248673),
+ northeast: const LatLng(-8.982446, 153.823821),
),
- duration: const Duration(seconds: 5),
- )
- .then((result) => debugPrint(
- "mapController.animateCamera() returned $result"));
- },
- child: const Text('newLatLng'),
- ),
- TextButton(
- onPressed: () async {
- await mapController.animateCamera(
- CameraUpdate.newLatLngBounds(
- LatLngBounds(
- southwest: const LatLng(-38.483935, 113.248673),
- northeast: const LatLng(-8.982446, 153.823821),
+ left: 10,
+ top: 5,
+ bottom: 25,
),
- left: 10,
- top: 5,
- bottom: 25,
- ),
- );
- },
- child: const Text('newLatLngBounds'),
- ),
- TextButton(
- onPressed: () async {
- await mapController.animateCamera(
- CameraUpdate.newLatLngZoom(
- const LatLng(37.4231613, -122.087159),
- 11.0,
- ),
- );
- },
- child: const Text('newLatLngZoom'),
- ),
- TextButton(
- onPressed: () async {
- await mapController.animateCamera(
- CameraUpdate.scrollBy(150.0, -225.0),
- );
- },
- child: const Text('scrollBy'),
- ),
- if (!kIsWeb)
+ );
+ },
+ child: const Text('newLatLngBounds'),
+ ),
TextButton(
onPressed: () async {
- await mapController.queryCameraPosition().then(
- (result) => debugPrint(
- "queryCameraPosition() returned $result"),
- );
+ await mapController.animateCamera(
+ CameraUpdate.newLatLngZoom(
+ const LatLng(37.4231613, -122.087159),
+ 11.0,
+ ),
+ );
+ },
+ child: const Text('newLatLngZoom'),
+ ),
+ TextButton(
+ onPressed: () async {
+ await mapController.animateCamera(
+ CameraUpdate.scrollBy(150.0, -225.0),
+ );
+ },
+ child: const Text('scrollBy'),
+ ),
+ if (!kIsWeb)
+ TextButton(
+ onPressed: () async {
+ await mapController.queryCameraPosition().then(
+ (result) => debugPrint(
+ "queryCameraPosition() returned $result"),
+ );
+ },
+ child: const Text('queryCameraPosition'),
+ ),
+ TextButton(
+ onPressed: () async {
+ _fps = _fps == 30 ? 3 : 30;
+ await mapController.setMaximumFps(_fps);
+ },
+ child: const Text('setMaximumFps'),
+ ),
+ TextButton(
+ onPressed: () async {
+ await mapController.animateCamera(
+ CameraUpdate.zoomBy(
+ -0.5,
+ const Offset(30.0, 20.0),
+ ),
+ );
},
- child: const Text('queryCameraPosition'),
+ child: const Text('zoomBy with focus'),
),
- TextButton(
- onPressed: () async {
- _fps = _fps == 30 ? 3 : 30;
- await mapController.setMaximumFps(_fps);
- },
- child: const Text('setMaximumFps'),
- ),
- TextButton(
- onPressed: () async {
- await mapController.animateCamera(
- CameraUpdate.zoomBy(
- -0.5,
- const Offset(30.0, 20.0),
- ),
- );
- },
- child: const Text('zoomBy with focus'),
- ),
- TextButton(
- onPressed: () async {
- await mapController.animateCamera(
- CameraUpdate.newLatLngZoom(const LatLng(48, 11), 5),
- duration: const Duration(milliseconds: 300),
- );
- },
- child: const Text('latlngZoom'),
- ),
- TextButton(
- onPressed: () async {
- await mapController.animateCamera(
- CameraUpdate.zoomBy(-0.5),
- );
- },
- child: const Text('zoomBy'),
- ),
- TextButton(
- onPressed: () async {
- await mapController.animateCamera(
- CameraUpdate.zoomIn(),
- );
- },
- child: const Text('zoomIn'),
- ),
- TextButton(
- onPressed: () async {
- await mapController.animateCamera(
- CameraUpdate.zoomOut(),
- );
- },
- child: const Text('zoomOut'),
- ),
- TextButton(
- onPressed: () async {
- await mapController.animateCamera(
- CameraUpdate.zoomTo(16.0),
- );
- },
- child: const Text('zoomTo'),
- ),
- TextButton(
- onPressed: () async {
- await mapController.animateCamera(
- CameraUpdate.bearingTo(45.0),
- );
- },
- child: const Text('bearingTo'),
- ),
- TextButton(
- onPressed: () async {
- await mapController.animateCamera(
- CameraUpdate.tiltTo(30.0),
- );
- },
- child: const Text('tiltTo'),
- ),
- ],
+ TextButton(
+ onPressed: () async {
+ await mapController.animateCamera(
+ CameraUpdate.newLatLngZoom(const LatLng(48, 11), 5),
+ duration: const Duration(milliseconds: 300),
+ );
+ },
+ child: const Text('latlngZoom'),
+ ),
+ TextButton(
+ onPressed: () async {
+ await mapController.animateCamera(
+ CameraUpdate.zoomBy(-0.5),
+ );
+ },
+ child: const Text('zoomBy'),
+ ),
+ TextButton(
+ onPressed: () async {
+ await mapController.animateCamera(
+ CameraUpdate.zoomIn(),
+ );
+ },
+ child: const Text('zoomIn'),
+ ),
+ TextButton(
+ onPressed: () async {
+ await mapController.animateCamera(
+ CameraUpdate.zoomOut(),
+ );
+ },
+ child: const Text('zoomOut'),
+ ),
+ TextButton(
+ onPressed: () async {
+ await mapController.animateCamera(
+ CameraUpdate.zoomTo(16.0),
+ );
+ },
+ child: const Text('zoomTo'),
+ ),
+ TextButton(
+ onPressed: () async {
+ await mapController.animateCamera(
+ CameraUpdate.bearingTo(45.0),
+ );
+ },
+ child: const Text('bearingTo'),
+ ),
+ TextButton(
+ onPressed: () async {
+ await mapController.animateCamera(
+ CameraUpdate.tiltTo(30.0),
+ );
+ },
+ child: const Text('tiltTo'),
+ ),
+ ],
+ ),
),
),
- )
+ ),
],
);
}
From 761102cbcbdef6f9cfc5971b34bc2ba2a77b790c Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 5 Jan 2026 21:03:58 +0100
Subject: [PATCH 11/38] chore(deps): bump actions/checkout from 5 to 6 (#693)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to
6.
Release notes
Sourced from actions/checkout's
releases .
v6.0.0
What's Changed
Full Changelog : https://github.com/actions/checkout/compare/v5.0.0...v6.0.0
v6-beta
What's Changed
Updated persist-credentials to store the credentials under
$RUNNER_TEMP instead of directly in the local git
config.
This requires a minimum Actions Runner version of v2.329.0
to access the persisted credentials for Docker
container action scenarios.
v5.0.1
What's Changed
Full Changelog : https://github.com/actions/checkout/compare/v5...v5.0.1
Changelog
Sourced from actions/checkout's
changelog .
Changelog
v6.0.0
v5.0.1
v5.0.0
v4.3.1
v4.3.0
v4.2.2
v4.2.1
v4.2.0
v4.1.7
v4.1.6
v4.1.5
... (truncated)
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Gabriel <56477412+gabbopalma@users.noreply.github.com>
From 3ffc35be8ce0cc9b8904f1be7136909b4a05b762 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 5 Jan 2026 21:04:10 +0100
Subject: [PATCH 12/38] chore(deps): bump actions/upload-artifact from 4 to 6
(#694)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps
[actions/upload-artifact](https://github.com/actions/upload-artifact)
from 4 to 6.
Release notes
Sourced from actions/upload-artifact's
releases .
v6.0.0
v6 - What's new
[!IMPORTANT]
actions/upload-artifact@v6 now runs on Node.js 24 (runs.using:
node24) and requires a minimum Actions Runner version of 2.327.1.
If you are using self-hosted runners, ensure they are updated before
upgrading.
Node.js 24
This release updates the runtime to Node.js 24. v5 had preliminary
support for Node.js 24, however this action was by default still running
on Node.js 20. Now this action by default will run on Node.js 24.
What's Changed
Full Changelog : https://github.com/actions/upload-artifact/compare/v5.0.0...v6.0.0
v5.0.0
What's Changed
BREAKING CHANGE: this update supports Node
v24.x. This is not a breaking change per-se but we're
treating it as such.
New Contributors
Full Changelog : https://github.com/actions/upload-artifact/compare/v4...v5.0.0
v4.6.2
What's Changed
New Contributors
Full Changelog : https://github.com/actions/upload-artifact/compare/v4...v4.6.2
v4.6.1
What's Changed
... (truncated)
Commits
b7c566a
Merge pull request #745
from actions/upload-artifact-v6-release
e516bc8
docs: correct description of Node.js 24 support in README
ddc45ed
docs: update README to correct action name for Node.js 24 support
615b319
chore: release v6.0.0 for Node.js 24 support
017748b
Merge pull request #744
from actions/fix-storage-blob
38d4c79
chore: rebuild dist
7d27270
chore: add missing license cache files for @actions/core,
@actions/io, and mi...
5f643d3
chore: update license files for @actions/artifact@5 .0.1 dependencies
1df1684
chore: update package-lock.json with @actions/artifact@5 .0.1
b5b1a91
fix: update @actions/artifact to ^5.0.0 for Node.js 24
punycode fix
Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Gabriel <56477412+gabbopalma@users.noreply.github.com>
From 5230fab5bb73e45066b90086b5209bc3f776e9df Mon Sep 17 00:00:00 2001
From: Gabriel Palmisano
Date: Mon, 5 Jan 2026 21:26:47 +0100
Subject: [PATCH 13/38] fix: mix max zoom preference on iOS
---
.../ios/maplibre_gl/Sources/maplibre_gl/Convert.swift | 10 +++++++---
.../Sources/maplibre_gl/MapLibreMapController.swift | 10 +++++++---
.../Sources/maplibre_gl/MapLibreMapOptionsSink.swift | 2 +-
3 files changed, 15 insertions(+), 7 deletions(-)
diff --git a/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/Convert.swift b/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/Convert.swift
index 4f1a71c87..38aedb664 100644
--- a/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/Convert.swift
+++ b/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/Convert.swift
@@ -10,10 +10,14 @@ class Convert {
if let compassEnabled = options["compassEnabled"] as? Bool {
delegate.setCompassEnabled(compassEnabled: compassEnabled)
}
- if let minMaxZoomPreference = options["minMaxZoomPreference"] as? [Double] {
+ if let minMaxZoomPreference = options["minMaxZoomPreference"] as? [Any] {
+ // Handle both [Double] and [NSNull] (for unbounded zoom)
+ let minZoom: Double? = (minMaxZoomPreference[0] is NSNull) ? nil : minMaxZoomPreference[0] as? Double
+ let maxZoom: Double? = (minMaxZoomPreference[1] is NSNull) ? nil : minMaxZoomPreference[1] as? Double
+
delegate.setMinMaxZoomPreference(
- min: minMaxZoomPreference[0],
- max: minMaxZoomPreference[1]
+ min: minZoom,
+ max: maxZoom
)
}
if let styleString = options["styleString"] as? String {
diff --git a/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/MapLibreMapController.swift b/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/MapLibreMapController.swift
index 4a4344abd..8ff97a46d 100644
--- a/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/MapLibreMapController.swift
+++ b/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/MapLibreMapController.swift
@@ -1962,9 +1962,13 @@ class MapLibreMapController: NSObject, FlutterPlatformView, MLNMapViewDelegate,
mapView.compassView.isHidden = !compassEnabled
}
- func setMinMaxZoomPreference(min: Double, max: Double) {
- mapView.minimumZoomLevel = min
- mapView.maximumZoomLevel = max
+ func setMinMaxZoomPreference(min: Double?, max: Double?) {
+ // Use MapLibre defaults (0 for min, 22 for max) when unbounded (nil)
+ let minZoom = min ?? 0.0
+ let maxZoom = max ?? 22.0
+
+ mapView.minimumZoomLevel = minZoom
+ mapView.maximumZoomLevel = maxZoom
}
private static func styleStringIsJSON(_ styleString: String) -> Bool {
diff --git a/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/MapLibreMapOptionsSink.swift b/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/MapLibreMapOptionsSink.swift
index c0468c06d..feeafabbf 100644
--- a/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/MapLibreMapOptionsSink.swift
+++ b/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/MapLibreMapOptionsSink.swift
@@ -4,7 +4,7 @@ protocol MapLibreMapOptionsSink {
func setCameraTargetBounds(bounds: MLNCoordinateBounds?)
func setCompassEnabled(compassEnabled: Bool)
func setStyleString(styleString: String)
- func setMinMaxZoomPreference(min: Double, max: Double)
+ func setMinMaxZoomPreference(min: Double?, max: Double?)
func setRotateGesturesEnabled(rotateGesturesEnabled: Bool)
func setScrollGesturesEnabled(scrollGesturesEnabled: Bool)
func setTiltGesturesEnabled(tiltGesturesEnabled: Bool)
From 8bcd74a2691004edaccf4c63acc2f4d613f39fcc Mon Sep 17 00:00:00 2001
From: Gabriel Palmisano
Date: Mon, 5 Jan 2026 21:28:45 +0100
Subject: [PATCH 14/38] refactor: cameraTargetBounds on android and iOS
---
.../maplibregl/MapLibreMapController.java | 10 +++-
.../Sources/maplibre_gl/Convert.swift | 12 +++--
.../maplibre_gl/MapLibreMapController.swift | 48 ++++++++++++++++++-
3 files changed, 65 insertions(+), 5 deletions(-)
diff --git a/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/MapLibreMapController.java b/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/MapLibreMapController.java
index 372b0e7a3..02a94fffa 100644
--- a/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/MapLibreMapController.java
+++ b/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/MapLibreMapController.java
@@ -165,7 +165,7 @@ public void onStyleLoaded(@NonNull Style style) {
updateMyLocationEnabled();
if (null != bounds) {
- mapLibreMap.setLatLngBoundsForCameraTarget(bounds);
+ setCameraTargetBounds(bounds);
}
mapLibreMap.addOnMapClickListener(MapLibreMapController.this);
@@ -236,6 +236,11 @@ public void onMapReady(MapLibreMap mapLibreMap) {
mapLibreMap.addOnCameraMoveListener(this);
mapLibreMap.addOnCameraIdleListener(this);
+ // Apply camera target bounds if set during initialization
+ if (bounds != null) {
+ mapLibreMap.setLatLngBoundsForCameraTarget(bounds);
+ }
+
if (androidGesturesManager != null) {
androidGesturesManager.setMoveGestureListener(new MoveGestureListener());
mapView.setOnTouchListener(
@@ -2087,6 +2092,9 @@ public void onDestroy(@NonNull LifecycleOwner owner) {
@Override
public void setCameraTargetBounds(LatLngBounds bounds) {
this.bounds = bounds;
+ if (mapLibreMap != null) {
+ mapLibreMap.setLatLngBoundsForCameraTarget(bounds);
+ }
}
@Override
diff --git a/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/Convert.swift b/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/Convert.swift
index 38aedb664..073b2433c 100644
--- a/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/Convert.swift
+++ b/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/Convert.swift
@@ -3,9 +3,15 @@ import MapLibre
class Convert {
class func interpretMapLibreMapOptions(options: Any?, delegate: MapLibreMapOptionsSink) {
guard let options = options as? [String: Any] else { return }
- if let cameraTargetBounds = options["cameraTargetBounds"] as? [[[Double]]] {
- delegate
- .setCameraTargetBounds(bounds: MLNCoordinateBounds.fromArray(cameraTargetBounds[0]))
+ if let cameraTargetBounds = options["cameraTargetBounds"] as? [Any?] {
+ // Handle both [[[Double]]] and [nil] (for unbounded)
+ if let boundsArray = cameraTargetBounds[0] as? [[Double]] {
+ let bounds = MLNCoordinateBounds.fromArray(boundsArray)
+ delegate.setCameraTargetBounds(bounds: bounds)
+ } else {
+ // Unbounded - clear the bounds restriction
+ delegate.setCameraTargetBounds(bounds: nil)
+ }
}
if let compassEnabled = options["compassEnabled"] as? Bool {
delegate.setCompassEnabled(compassEnabled: compassEnabled)
diff --git a/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/MapLibreMapController.swift b/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/MapLibreMapController.swift
index 8ff97a46d..470890cc1 100644
--- a/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/MapLibreMapController.swift
+++ b/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/MapLibreMapController.swift
@@ -22,6 +22,7 @@ class MapLibreMapController: NSObject, FlutterPlatformView, MLNMapViewDelegate,
private var trackCameraPosition = false
private var myLocationEnabled = false
private var scrollingEnabled = true
+ private var isAdjustingCameraProgrammatically = false
private var interactiveFeatureLayerIds = Set()
private var addedShapesByLayer = [String: MLNShape]()
@@ -1853,7 +1854,47 @@ class MapLibreMapController: NSObject, FlutterPlatformView, MLNMapViewDelegate,
}
}
- func mapView(_ mapView: MLNMapView, regionDidChangeAnimated _: Bool) {
+ func mapView(_ mapView: MLNMapView, regionDidChangeAnimated animated: Bool) {
+ // Skip bounds enforcement if we're programmatically adjusting the camera to prevent recursion
+ if !isAdjustingCameraProgrammatically {
+ // Enforce camera target bounds if set
+ if let bounds = cameraTargetBounds {
+ let center = mapView.centerCoordinate
+ var needsAdjustment = false
+ var adjustedLat = center.latitude
+ var adjustedLng = center.longitude
+
+ // Check if center is outside bounds and clamp to bounds
+ if center.latitude < bounds.sw.latitude {
+ adjustedLat = bounds.sw.latitude
+ needsAdjustment = true
+ } else if center.latitude > bounds.ne.latitude {
+ adjustedLat = bounds.ne.latitude
+ needsAdjustment = true
+ }
+
+ if center.longitude < bounds.sw.longitude {
+ adjustedLng = bounds.sw.longitude
+ needsAdjustment = true
+ } else if center.longitude > bounds.ne.longitude {
+ adjustedLng = bounds.ne.longitude
+ needsAdjustment = true
+ }
+
+ // If the center coordinate is outside bounds, constrain it
+ if needsAdjustment {
+ let adjustedCenter = CLLocationCoordinate2D(latitude: adjustedLat, longitude: adjustedLng)
+ // Set flag to prevent recursion
+ isAdjustingCameraProgrammatically = true
+ // Use setCenter without animation to snap back to valid bounds
+ mapView.setCenter(adjustedCenter, animated: false)
+ isAdjustingCameraProgrammatically = false
+ // Early return to avoid invoking camera#onIdle during constraint adjustment
+ return
+ }
+ }
+ }
+
let arguments = trackCameraPosition ? [
"position": getCamera()?.toDict(mapView: mapView)
] : [:]
@@ -1954,7 +1995,12 @@ class MapLibreMapController: NSObject, FlutterPlatformView, MLNMapViewDelegate,
* MapLibreMapOptionsSink
*/
func setCameraTargetBounds(bounds: MLNCoordinateBounds?) {
+ guard let bounds = bounds else {
+ cameraTargetBounds = nil
+ return
+ }
cameraTargetBounds = bounds
+ mapView.setVisibleCoordinateBounds(bounds, animated: false)
}
func setCompassEnabled(compassEnabled: Bool) {
From b4e3d650970492cf36095bf7048aec4ecb3a46ed Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 5 Jan 2026 21:29:37 +0100
Subject: [PATCH 15/38] chore(deps): bump com.android.tools.build:gradle from
8.13.0 to 8.13.2 in /maplibre_gl/android in the gradle-plugin group (#695)
Bumps the gradle-plugin group in /maplibre_gl/android with 1 update:
com.android.tools.build:gradle.
Updates `com.android.tools.build:gradle` from 8.13.0 to 8.13.2
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore ` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore ` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore ` will
remove the ignore condition of the specified dependency and ignore
conditions
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Gabriel <56477412+gabbopalma@users.noreply.github.com>
From ee60566fce53dbc2184bf34e1dc2986279c73243 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 5 Jan 2026 21:43:34 +0100
Subject: [PATCH 16/38] chore(deps): bump com.android.application from 8.12.0
to 8.13.2 in /maplibre_gl_example/android (#696)
Bumps com.android.application from 8.12.0 to 8.13.2.
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Gabriel <56477412+gabbopalma@users.noreply.github.com>
From 33bbb976d7c39dd30ec010c167d9b61c675aeb70 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 5 Jan 2026 21:46:58 +0100
Subject: [PATCH 17/38] chore(deps): bump
org.jetbrains.kotlin:kotlin-gradle-plugin from 2.1.0 to 2.3.0 in
/maplibre_gl/android in the kotlin group (#697)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps the kotlin group in /maplibre_gl/android with 1 update:
[org.jetbrains.kotlin:kotlin-gradle-plugin](https://github.com/JetBrains/kotlin).
Updates `org.jetbrains.kotlin:kotlin-gradle-plugin` from 2.1.0 to 2.3.0
Release notes
Sourced from org.jetbrains.kotlin:kotlin-gradle-plugin's
releases .
Kotlin 2.3.0
Changelog
Analysis API
KT-80082
K2. False positive "Cannot resolve method" for self-bounded
generic with wildcard return type in Java interop
KT-80303
Move :native:analysis-api-klib-reader to
:libraries:tools
Analysis API. Code Compilation
KT-70860
K2 IDE / Kotlin Debugger: CCE “java.lang.String cannot be cast to
java.lang.Void” on evaluating not-null variable on the line with
assigning null to that var
KT-78554
K2 IDE / Kotlin Debugger: ISE “No override for FUN
IR_EXTERNAL_DECLARATION_STUB” on calling toString() for local class
instance during evaluation
KT-73201
K2 IDE: Error while evaluating expressions with local classes
Analysis API. FIR
KT-81378
Expected expression 'FirFunctionCallImpl' to be resolved caused by
suspend {}
KT-80473
Add events for tracking LL activities
KT-46375
Analysis API: Support cross-file class redeclaration checks using
indices
KT-80471
Analysis API: Deduplicate equivalent call candidates in
resolveToCallCandidates
KT-79653
[Analysis API] ContextCollector: BODY context of enum classes doesn't
contain enum entries
KT-75858
K2 AA: False positive 'property must be initialized' on incremental
analysis with 'field' usage and semicolon in setter
KT-80231
AnnotationArgumentsStateKeepers doesn't restore the initial annotation
in some cases
KT-80233
Pull mutation out of AnnotationArgumentsStateKeepers
KT-71466
LLFirBuiltinsSessionFactory uses
createCompositeSymbolProvider
KT-76432
JavaClassUseSiteMemberScope: Expected FirResolvedTypeRef with
ConeKotlinType but was FirUserTypeRefImpl
Analysis API. Infrastructure
KT-80717
Support IntelliJ Bazel build in the Kotlin Coop development mode
Analysis API. Light Classes
KT-80656
Duplicate no-args constructor in PSI
KT-60490
Symbol Light Classes: Property accessors from a delegated interface
don't present in the delegating class
KT-79689
SymbolLightClassForClassLike.toString() causes PSI tree loading
KT-80690
Private interface functions are not present in light classes
KT-80256
K2: Certain actions in JPA code causes infinite PIEAE: "Element
class CompositeElement of type REFERENCE_EXPRESSION (class
KtNameReferenceExpressionElementType)"
KT-79012
Add a high-level overview of light classes
Analysis API. Providers and Caches
Fixes
KT-81476
Analysis API: AlreadyDisposedException from low-memory
cache cleanup
KT-80911
Analysis API: Execute session invalidation in a non-cancelable
section
KT-81242
Analysis API: Add UUID/lifetime properties to LL FIR session structure
logging
KT-80622
Analysis API: Visualise LL FIR session structure & weight
KT-80904
Analysis API: "Invalid dangling file module" exception during
session invalidation
KT-78882
K2 AA: Calling containingSymbol on getProgressionLastElement causes
exception
... (truncated)
Changelog
Sourced from org.jetbrains.kotlin:kotlin-gradle-plugin's
changelog .
2.3.0
Analysis API
KT-80082
K2. False positive "Cannot resolve method" for self-bounded
generic with wildcard return type in Java interop
KT-80303
Move :native:analysis-api-klib-reader to
:libraries:tools
Analysis API. Code Compilation
KT-70860
K2 IDE / Kotlin Debugger: CCE “java.lang.String cannot be cast to
java.lang.Void” on evaluating not-null variable on the line with
assigning null to that var
KT-78554
K2 IDE / Kotlin Debugger: ISE “No override for FUN
IR_EXTERNAL_DECLARATION_STUB” on calling toString() for local class
instance during evaluation
KT-73201
K2 IDE: Error while evaluating expressions with local classes
Analysis API. FIR
KT-81378
Expected expression 'FirFunctionCallImpl' to be resolved caused by
suspend {}
KT-80473
Add events for tracking LL activities
KT-46375
Analysis API: Support cross-file class redeclaration checks using
indices
KT-80471
Analysis API: Deduplicate equivalent call candidates in
resolveToCallCandidates
KT-79653
[Analysis API] ContextCollector: BODY context of enum classes doesn't
contain enum entries
KT-75858
K2 AA: False positive 'property must be initialized' on incremental
analysis with 'field' usage and semicolon in setter
KT-80231
AnnotationArgumentsStateKeepers doesn't restore the initial annotation
in some cases
KT-80233
Pull mutation out of AnnotationArgumentsStateKeepers
KT-71466
LLFirBuiltinsSessionFactory uses
createCompositeSymbolProvider
KT-76432
JavaClassUseSiteMemberScope: Expected FirResolvedTypeRef with
ConeKotlinType but was FirUserTypeRefImpl
Analysis API. Infrastructure
KT-80717
Support IntelliJ Bazel build in the Kotlin Coop development mode
Analysis API. Light Classes
KT-80656
Duplicate no-args constructor in PSI
KT-60490
Symbol Light Classes: Property accessors from a delegated interface
don't present in the delegating class
KT-79689
SymbolLightClassForClassLike.toString() causes PSI tree loading
KT-80690
Private interface functions are not present in light classes
KT-80256
K2: Certain actions in JPA code causes infinite PIEAE: "Element
class CompositeElement of type REFERENCE_EXPRESSION (class
KtNameReferenceExpressionElementType)"
KT-79012
Add a high-level overview of light classes
Analysis API. Providers and Caches
Fixes
KT-81476
Analysis API: AlreadyDisposedException from low-memory
cache cleanup
KT-80911
Analysis API: Execute session invalidation in a non-cancelable
section
KT-81242
Analysis API: Add UUID/lifetime properties to LL FIR session structure
logging
KT-80622
Analysis API: Visualise LL FIR session structure & weight
KT-80904
Analysis API: "Invalid dangling file module" exception during
session invalidation
KT-78882
K2 AA: Calling containingSymbol on getProgressionLastElement causes
exception
KT-58325
Analysis API: Combine
LLKotlinStubBasedLibrarySymbolProviders in session
dependencies (optimization)
... (truncated)
Commits
f95cb2f
Add ChangeLog for 2.3.0-RC3
9d65a2e
KT-82901: Fix issue with converting Long.MIN_VALUE to Duration
35a9a82
FE: Postpone DiscriminateSuspendInOverloadResolution
e0b7eea
FE: Add tests for KT-82869
e66298c
Add ChangeLog for 2.3.0-RC2
e490802
[K/JS] Introduce a compiler argument to enable export of suspend
functions
585094b
FIR2IR: Avoid generation of incorrect suspend adapter for custom
implementation
c69adc7
FIR2IR: Rename and clarify contracts for suspicious utility
function
b4bb8bf
FIR2IR: Pass original expected type to
applySuspendConversionIfNeeded
4718830
FIR2IR: Add tests for KT-82590
Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore ` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore ` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore ` will
remove the ignore condition of the specified dependency and ignore
conditions
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
From 5461e29236b5ff969e242422383a3b72d52c7dd5 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 5 Jan 2026 22:12:25 +0100
Subject: [PATCH 18/38] chore(deps): bump org.jetbrains.kotlin.android from
2.1.0 to 2.3.0 in /maplibre_gl_example/android (#698)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps
[org.jetbrains.kotlin.android](https://github.com/JetBrains/kotlin) from
2.1.0 to 2.3.0.
Release notes
Sourced from org.jetbrains.kotlin.android's
releases .
Kotlin 2.3.0
Changelog
Analysis API
KT-80082
K2. False positive "Cannot resolve method" for self-bounded
generic with wildcard return type in Java interop
KT-80303
Move :native:analysis-api-klib-reader to
:libraries:tools
Analysis API. Code Compilation
KT-70860
K2 IDE / Kotlin Debugger: CCE “java.lang.String cannot be cast to
java.lang.Void” on evaluating not-null variable on the line with
assigning null to that var
KT-78554
K2 IDE / Kotlin Debugger: ISE “No override for FUN
IR_EXTERNAL_DECLARATION_STUB” on calling toString() for local class
instance during evaluation
KT-73201
K2 IDE: Error while evaluating expressions with local classes
Analysis API. FIR
KT-81378
Expected expression 'FirFunctionCallImpl' to be resolved caused by
suspend {}
KT-80473
Add events for tracking LL activities
KT-46375
Analysis API: Support cross-file class redeclaration checks using
indices
KT-80471
Analysis API: Deduplicate equivalent call candidates in
resolveToCallCandidates
KT-79653
[Analysis API] ContextCollector: BODY context of enum classes doesn't
contain enum entries
KT-75858
K2 AA: False positive 'property must be initialized' on incremental
analysis with 'field' usage and semicolon in setter
KT-80231
AnnotationArgumentsStateKeepers doesn't restore the initial annotation
in some cases
KT-80233
Pull mutation out of AnnotationArgumentsStateKeepers
KT-71466
LLFirBuiltinsSessionFactory uses
createCompositeSymbolProvider
KT-76432
JavaClassUseSiteMemberScope: Expected FirResolvedTypeRef with
ConeKotlinType but was FirUserTypeRefImpl
Analysis API. Infrastructure
KT-80717
Support IntelliJ Bazel build in the Kotlin Coop development mode
Analysis API. Light Classes
KT-80656
Duplicate no-args constructor in PSI
KT-60490
Symbol Light Classes: Property accessors from a delegated interface
don't present in the delegating class
KT-79689
SymbolLightClassForClassLike.toString() causes PSI tree loading
KT-80690
Private interface functions are not present in light classes
KT-80256
K2: Certain actions in JPA code causes infinite PIEAE: "Element
class CompositeElement of type REFERENCE_EXPRESSION (class
KtNameReferenceExpressionElementType)"
KT-79012
Add a high-level overview of light classes
Analysis API. Providers and Caches
Fixes
KT-81476
Analysis API: AlreadyDisposedException from low-memory
cache cleanup
KT-80911
Analysis API: Execute session invalidation in a non-cancelable
section
KT-81242
Analysis API: Add UUID/lifetime properties to LL FIR session structure
logging
KT-80622
Analysis API: Visualise LL FIR session structure & weight
KT-80904
Analysis API: "Invalid dangling file module" exception during
session invalidation
KT-78882
K2 AA: Calling containingSymbol on getProgressionLastElement causes
exception
... (truncated)
Changelog
Sourced from org.jetbrains.kotlin.android's
changelog .
2.3.0
Analysis API
KT-80082
K2. False positive "Cannot resolve method" for self-bounded
generic with wildcard return type in Java interop
KT-80303
Move :native:analysis-api-klib-reader to
:libraries:tools
Analysis API. Code Compilation
KT-70860
K2 IDE / Kotlin Debugger: CCE “java.lang.String cannot be cast to
java.lang.Void” on evaluating not-null variable on the line with
assigning null to that var
KT-78554
K2 IDE / Kotlin Debugger: ISE “No override for FUN
IR_EXTERNAL_DECLARATION_STUB” on calling toString() for local class
instance during evaluation
KT-73201
K2 IDE: Error while evaluating expressions with local classes
Analysis API. FIR
KT-81378
Expected expression 'FirFunctionCallImpl' to be resolved caused by
suspend {}
KT-80473
Add events for tracking LL activities
KT-46375
Analysis API: Support cross-file class redeclaration checks using
indices
KT-80471
Analysis API: Deduplicate equivalent call candidates in
resolveToCallCandidates
KT-79653
[Analysis API] ContextCollector: BODY context of enum classes doesn't
contain enum entries
KT-75858
K2 AA: False positive 'property must be initialized' on incremental
analysis with 'field' usage and semicolon in setter
KT-80231
AnnotationArgumentsStateKeepers doesn't restore the initial annotation
in some cases
KT-80233
Pull mutation out of AnnotationArgumentsStateKeepers
KT-71466
LLFirBuiltinsSessionFactory uses
createCompositeSymbolProvider
KT-76432
JavaClassUseSiteMemberScope: Expected FirResolvedTypeRef with
ConeKotlinType but was FirUserTypeRefImpl
Analysis API. Infrastructure
KT-80717
Support IntelliJ Bazel build in the Kotlin Coop development mode
Analysis API. Light Classes
KT-80656
Duplicate no-args constructor in PSI
KT-60490
Symbol Light Classes: Property accessors from a delegated interface
don't present in the delegating class
KT-79689
SymbolLightClassForClassLike.toString() causes PSI tree loading
KT-80690
Private interface functions are not present in light classes
KT-80256
K2: Certain actions in JPA code causes infinite PIEAE: "Element
class CompositeElement of type REFERENCE_EXPRESSION (class
KtNameReferenceExpressionElementType)"
KT-79012
Add a high-level overview of light classes
Analysis API. Providers and Caches
Fixes
KT-81476
Analysis API: AlreadyDisposedException from low-memory
cache cleanup
KT-80911
Analysis API: Execute session invalidation in a non-cancelable
section
KT-81242
Analysis API: Add UUID/lifetime properties to LL FIR session structure
logging
KT-80622
Analysis API: Visualise LL FIR session structure & weight
KT-80904
Analysis API: "Invalid dangling file module" exception during
session invalidation
KT-78882
K2 AA: Calling containingSymbol on getProgressionLastElement causes
exception
KT-58325
Analysis API: Combine
LLKotlinStubBasedLibrarySymbolProviders in session
dependencies (optimization)
... (truncated)
Commits
f95cb2f
Add ChangeLog for 2.3.0-RC3
9d65a2e
KT-82901: Fix issue with converting Long.MIN_VALUE to Duration
35a9a82
FE: Postpone DiscriminateSuspendInOverloadResolution
e0b7eea
FE: Add tests for KT-82869
e66298c
Add ChangeLog for 2.3.0-RC2
e490802
[K/JS] Introduce a compiler argument to enable export of suspend
functions
585094b
FIR2IR: Avoid generation of incorrect suspend adapter for custom
implementation
c69adc7
FIR2IR: Rename and clarify contracts for suspicious utility
function
b4bb8bf
FIR2IR: Pass original expected type to
applySuspendConversionIfNeeded
4718830
FIR2IR: Add tests for KT-82590
Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Gabriel <56477412+gabbopalma@users.noreply.github.com>
From 86907a348a3acac57cb46572cd1d36936259e1d1 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 5 Jan 2026 22:38:05 +0100
Subject: [PATCH 19/38] chore(deps): bump com.squareup.okhttp3:okhttp from
4.12.0 to 5.3.2 in /maplibre_gl/android (#700)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [com.squareup.okhttp3:okhttp](https://github.com/square/okhttp)
from 4.12.0 to 5.3.2.
Changelog
Sourced from com.squareup.okhttp3:okhttp's
changelog .
Version 5.3.2
2025-11-18
Version 5.3.1
2025-11-16
This release is the same as 5.3.0. Okio 3.16.3 didn't have a
necessary fix!
Upgrade: [Okio 3.16.3][okio_3_16_3].
Version 5.3.0
2025-10-30
New: Add tags to Call, including computable tags. Use
this to attach application-specific
metadata to a Call in an EventListener or
Interceptor. The tag can be read in any other
EventListener or Interceptor.
override fun intercept(chain:
Interceptor.Chain): Response {
chain.call().tag(MyAnalyticsTag::class) {
MyAnalyticsTag(...)
}
return chain.proceed(chain.request())
}
New: Support request bodies on HTTP/1.1 connection upgrades.
New: EventListener.plus() makes it easier to observe
events in multiple listeners.
Fix: Don't spam logs with ‘Method isLoggable in android.util.Log
not mocked.’ when using
OkHttp in Robolectric and Paparazzi tests.
Upgrade: [Kotlin 2.2.21][kotlin_2_2_21].
Upgrade: [Okio 3.16.2][okio_3_16_2].
Upgrade: [ZSTD-KMP 0.4.0][zstd_kmp_0_4_0]. This update fixes a bug
that caused APKs to fail
[16 KB ELF alignment checks][elf_alignment].
Version 5.2.3
2025-11-18
... (truncated)
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
---------
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Gabriel <56477412+gabbopalma@users.noreply.github.com>
---
maplibre_gl/android/build.gradle | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/maplibre_gl/android/build.gradle b/maplibre_gl/android/build.gradle
index 25cba6e5a..114f4ec22 100644
--- a/maplibre_gl/android/build.gradle
+++ b/maplibre_gl/android/build.gradle
@@ -50,7 +50,7 @@ android {
jvmTarget = JavaVersion.VERSION_21.toString()
}
dependencies {
- implementation 'org.maplibre.gl:android-sdk:12.3.0'
+ implementation 'org.maplibre.gl:android-sdk:12.3.1'
implementation 'org.maplibre.gl:android-plugin-annotation-v9:3.0.2'
implementation 'org.maplibre.gl:android-plugin-offline-v9:3.0.2'
implementation 'com.squareup.okhttp3:okhttp:5.3.2'
From b4fb1741da7512c3decf0901178e5ac4eab0b609 Mon Sep 17 00:00:00 2001
From: Gabriel Palmisano
Date: Tue, 6 Jan 2026 18:36:48 +0100
Subject: [PATCH 20/38] feat: add logo customization options including
visibility and position settings
---
.../java/org/maplibre/maplibregl/Convert.java | 8 +++++++
.../maplibregl/MapLibreMapBuilder.java | 23 ++++++++++++++++++
.../maplibregl/MapLibreMapController.java | 24 +++++++++++++++++++
.../maplibregl/MapLibreMapOptionsSink.kt | 4 ++++
.../Sources/maplibre_gl/Convert.swift | 8 +++++++
.../maplibre_gl/MapLibreMapController.swift | 9 ++++++-
.../maplibre_gl/MapLibreMapOptionsSink.swift | 2 ++
7 files changed, 77 insertions(+), 1 deletion(-)
diff --git a/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/Convert.java b/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/Convert.java
index e41df5ddd..7e070eccf 100644
--- a/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/Convert.java
+++ b/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/Convert.java
@@ -279,6 +279,14 @@ static void interpretMapLibreMapOptions(Object o, MapLibreMapOptionsSink sink, C
if (myLocationRenderMode != null) {
sink.setMyLocationRenderMode(toInt(myLocationRenderMode));
}
+ final Object logoEnabled = data.get("logoEnabled");
+ if (logoEnabled != null) {
+ sink.setLogoEnabled(toBoolean(logoEnabled));
+ }
+ final Object logoViewGravity = data.get("logoViewPosition");
+ if (logoViewGravity != null) {
+ sink.setLogoViewGravity(toInt(logoViewGravity));
+ }
final Object logoViewMargins = data.get("logoViewMargins");
if (logoViewMargins != null) {
final List logoViewMarginsData = toList(logoViewMargins);
diff --git a/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/MapLibreMapBuilder.java b/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/MapLibreMapBuilder.java
index 26f97f74d..98cc2dad8 100644
--- a/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/MapLibreMapBuilder.java
+++ b/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/MapLibreMapBuilder.java
@@ -121,6 +121,29 @@ public void setMyLocationRenderMode(int myLocationRenderMode) {
this.myLocationRenderMode = myLocationRenderMode;
}
+ @Override
+ public void setLogoEnabled(boolean logoEnabled) {
+ options.logoEnabled(logoEnabled);
+ }
+
+ @Override
+ public void setLogoViewGravity(int gravity) {
+ switch (gravity) {
+ case 0:
+ options.logoGravity(Gravity.TOP | Gravity.START);
+ break;
+ case 1:
+ options.logoGravity(Gravity.TOP | Gravity.END);
+ break;
+ case 2:
+ options.logoGravity(Gravity.BOTTOM | Gravity.START);
+ break;
+ case 3:
+ options.logoGravity(Gravity.BOTTOM | Gravity.END);
+ break;
+ }
+ }
+
public void setLogoViewMargins(int x, int y) {
options.logoMargins(
new int[] {
diff --git a/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/MapLibreMapController.java b/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/MapLibreMapController.java
index 02a94fffa..05e4b4fcd 100644
--- a/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/MapLibreMapController.java
+++ b/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/MapLibreMapController.java
@@ -2175,6 +2175,30 @@ public void setMyLocationRenderMode(int myLocationRenderMode) {
}
}
+ @Override
+ public void setLogoEnabled(boolean logoEnabled) {
+ mapLibreMap.getUiSettings().setLogoEnabled(logoEnabled);
+ }
+
+ @Override
+ public void setLogoViewGravity(int gravity) {
+ switch (gravity) {
+ case 0:
+ mapLibreMap.getUiSettings().setLogoGravity(Gravity.TOP | Gravity.START);
+ break;
+ case 1:
+ mapLibreMap.getUiSettings().setLogoGravity(Gravity.TOP | Gravity.END);
+ break;
+ default:
+ case 2:
+ mapLibreMap.getUiSettings().setLogoGravity(Gravity.BOTTOM | Gravity.START);
+ break;
+ case 3:
+ mapLibreMap.getUiSettings().setLogoGravity(Gravity.BOTTOM | Gravity.END);
+ break;
+ }
+ }
+
public void setLogoViewMargins(int x, int y) {
mapLibreMap.getUiSettings().setLogoMargins(x, 0, 0, y);
}
diff --git a/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/MapLibreMapOptionsSink.kt b/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/MapLibreMapOptionsSink.kt
index e0432f612..bc1575c8a 100644
--- a/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/MapLibreMapOptionsSink.kt
+++ b/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/MapLibreMapOptionsSink.kt
@@ -34,6 +34,10 @@ internal interface MapLibreMapOptionsSink {
fun setMyLocationRenderMode(myLocationRenderMode: Int)
+ fun setLogoEnabled(logoEnabled: Boolean)
+
+ fun setLogoViewGravity(gravity: Int)
+
fun setLogoViewMargins(x: Int, y: Int)
fun setCompassGravity(gravity: Int)
diff --git a/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/Convert.swift b/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/Convert.swift
index 073b2433c..b7fb0c046 100644
--- a/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/Convert.swift
+++ b/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/Convert.swift
@@ -57,6 +57,14 @@ class Convert {
{
delegate.setMyLocationRenderMode(myLocationRenderMode: renderMode)
}
+ if let logoEnabled = options["logoEnabled"] as? Bool {
+ delegate.setLogoEnabled(logoEnabled: logoEnabled)
+ }
+ if let logoViewPosition = options["logoViewPosition"] as? UInt,
+ let position = MLNOrnamentPosition(rawValue: logoViewPosition)
+ {
+ delegate.setLogoViewPosition(position: position)
+ }
if let logoViewMargins = options["logoViewMargins"] as? [Double] {
delegate.setLogoViewMargins(x: logoViewMargins[0], y: logoViewMargins[1])
}
diff --git a/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/MapLibreMapController.swift b/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/MapLibreMapController.swift
index 470890cc1..148546086 100644
--- a/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/MapLibreMapController.swift
+++ b/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/MapLibreMapController.swift
@@ -79,7 +79,6 @@ class MapLibreMapController: NSObject, FlutterPlatformView, MLNMapViewDelegate,
)
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
- mapView.logoView.isHidden = true
self.registrar = registrar
super.init()
@@ -2115,6 +2114,14 @@ class MapLibreMapController: NSObject, FlutterPlatformView, MLNMapViewDelegate,
}
}
+ func setLogoEnabled(logoEnabled: Bool) {
+ mapView.logoView.isHidden = !logoEnabled
+ }
+
+ func setLogoViewPosition(position: MLNOrnamentPosition) {
+ mapView.logoViewPosition = position
+ }
+
func setLogoViewMargins(x: Double, y: Double) {
mapView.logoViewMargins = CGPoint(x: x, y: y)
}
diff --git a/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/MapLibreMapOptionsSink.swift b/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/MapLibreMapOptionsSink.swift
index feeafabbf..4625b1276 100644
--- a/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/MapLibreMapOptionsSink.swift
+++ b/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/MapLibreMapOptionsSink.swift
@@ -13,6 +13,8 @@ protocol MapLibreMapOptionsSink {
func setMyLocationEnabled(myLocationEnabled: Bool)
func setMyLocationTrackingMode(myLocationTrackingMode: MLNUserTrackingMode)
func setMyLocationRenderMode(myLocationRenderMode: MyLocationRenderMode)
+ func setLogoEnabled(logoEnabled: Bool)
+ func setLogoViewPosition(position: MLNOrnamentPosition)
func setLogoViewMargins(x: Double, y: Double)
func setCompassViewPosition(position: MLNOrnamentPosition)
func setCompassViewMargins(x: Double, y: Double)
From ac877a4d0e9d1f9c66bc1ee540e866bc2b984124 Mon Sep 17 00:00:00 2001
From: Gabriel Palmisano
Date: Tue, 6 Jan 2026 18:38:46 +0100
Subject: [PATCH 21/38] refactor: Refactor all example app and pages.
Implemented new UI and fixed minor bugs. Added MapLibre logo options.
---
maplibre_gl/lib/maplibre_gl.dart | 1 +
maplibre_gl/lib/src/annotation_manager.dart | 8 +
maplibre_gl/lib/src/maplibre_map.dart | 24 +-
maplibre_gl_example/lib/animate_camera.dart | 245 -------
.../lib/annotation_order_maps.dart | 152 ----
maplibre_gl_example/lib/attribution.dart | 101 ---
.../lib/click_annotations.dart | 157 ----
.../advanced}/offline_region_map.dart | 0
.../advanced}/offline_regions.dart | 168 +++--
.../lib/examples/advanced/pmtiles.dart | 87 +++
.../advanced/translucent_full_map.dart | 108 +++
.../annotations/annotation_order_example.dart | 256 +++++++
.../annotation_properties_example.dart | 690 ++++++++++++++++++
.../annotations/annotations_example.dart | 545 ++++++++++++++
.../annotations}/custom_marker.dart | 5 +-
.../lib/examples/basics/full_map_example.dart | 86 +++
.../lib/examples/basics/get_map_state.dart | 164 +++++
.../examples/basics/gps_location_page.dart | 255 +++++++
.../basics}/multi_style_switch.dart | 8 +-
.../camera/camera_bounds_example.dart | 194 +++++
.../camera/camera_controls_example.dart | 240 ++++++
.../interaction/map_controls_example.dart | 229 ++++++
.../interaction/map_gestures_example.dart | 205 ++++++
.../examples/layers/circle_layer_example.dart | 377 ++++++++++
.../examples/layers/fill_layer_example.dart | 298 ++++++++
.../examples/layers/line_layer_example.dart | 455 ++++++++++++
.../examples/layers/symbol_layer_example.dart | 651 +++++++++++++++++
.../layers/various_sources.dart} | 8 +-
maplibre_gl_example/lib/full_map.dart | 53 --
.../lib/get_map_informations.dart | 120 ---
maplibre_gl_example/lib/given_bounds.dart | 77 --
maplibre_gl_example/lib/layer.dart | 483 ------------
.../lib/layer_manipulation.dart | 330 ---------
maplibre_gl_example/lib/line.dart | 251 -------
maplibre_gl_example/lib/localized_map.dart | 78 --
maplibre_gl_example/lib/main.dart | 295 ++++++--
maplibre_gl_example/lib/map_ui.dart | 512 -------------
maplibre_gl_example/lib/move_camera.dart | 185 -----
.../lib/no_location_permission_page.dart | 39 -
maplibre_gl_example/lib/page.dart | 21 +-
maplibre_gl_example/lib/place_batch.dart | 188 -----
maplibre_gl_example/lib/place_circle.dart | 284 -------
maplibre_gl_example/lib/place_fill.dart | 240 ------
maplibre_gl_example/lib/place_source.dart | 191 -----
maplibre_gl_example/lib/place_symbol.dart | 418 -----------
maplibre_gl_example/lib/pmtiles.dart | 54 --
.../gps_location/gps_location_page.dart | 124 ----
maplibre_gl_example/lib/scrolling_map.dart | 143 ----
maplibre_gl_example/lib/shared/constants.dart | 146 ++++
.../lib/shared/extensions.dart | 8 +
maplibre_gl_example/lib/shared/shared.dart | 10 +
.../lib/shared/widgets/color_picker.dart | 177 +++++
.../lib/shared/widgets/example_button.dart | 328 +++++++++
.../shared/widgets/map_example_scaffold.dart | 179 +++++
.../lib/translucent_full_map.dart | 99 ++-
.../lib/src/ui.dart | 8 +
maplibre_gl_web/lib/src/convert.dart | 4 +
.../lib/src/maplibre_web_gl_platform.dart | 5 +
maplibre_gl_web/lib/src/options_sink.dart | 2 +
59 files changed, 6166 insertions(+), 4603 deletions(-)
delete mode 100644 maplibre_gl_example/lib/animate_camera.dart
delete mode 100644 maplibre_gl_example/lib/annotation_order_maps.dart
delete mode 100644 maplibre_gl_example/lib/attribution.dart
delete mode 100644 maplibre_gl_example/lib/click_annotations.dart
rename maplibre_gl_example/lib/{ => examples/advanced}/offline_region_map.dart (100%)
rename maplibre_gl_example/lib/{ => examples/advanced}/offline_regions.dart (51%)
create mode 100644 maplibre_gl_example/lib/examples/advanced/pmtiles.dart
create mode 100644 maplibre_gl_example/lib/examples/advanced/translucent_full_map.dart
create mode 100644 maplibre_gl_example/lib/examples/annotations/annotation_order_example.dart
create mode 100644 maplibre_gl_example/lib/examples/annotations/annotation_properties_example.dart
create mode 100644 maplibre_gl_example/lib/examples/annotations/annotations_example.dart
rename maplibre_gl_example/lib/{ => examples/annotations}/custom_marker.dart (98%)
create mode 100644 maplibre_gl_example/lib/examples/basics/full_map_example.dart
create mode 100644 maplibre_gl_example/lib/examples/basics/get_map_state.dart
create mode 100644 maplibre_gl_example/lib/examples/basics/gps_location_page.dart
rename maplibre_gl_example/lib/{ => examples/basics}/multi_style_switch.dart (96%)
create mode 100644 maplibre_gl_example/lib/examples/camera/camera_bounds_example.dart
create mode 100644 maplibre_gl_example/lib/examples/camera/camera_controls_example.dart
create mode 100644 maplibre_gl_example/lib/examples/interaction/map_controls_example.dart
create mode 100644 maplibre_gl_example/lib/examples/interaction/map_gestures_example.dart
create mode 100644 maplibre_gl_example/lib/examples/layers/circle_layer_example.dart
create mode 100644 maplibre_gl_example/lib/examples/layers/fill_layer_example.dart
create mode 100644 maplibre_gl_example/lib/examples/layers/line_layer_example.dart
create mode 100644 maplibre_gl_example/lib/examples/layers/symbol_layer_example.dart
rename maplibre_gl_example/lib/{sources.dart => examples/layers/various_sources.dart} (98%)
delete mode 100644 maplibre_gl_example/lib/full_map.dart
delete mode 100644 maplibre_gl_example/lib/get_map_informations.dart
delete mode 100644 maplibre_gl_example/lib/given_bounds.dart
delete mode 100644 maplibre_gl_example/lib/layer.dart
delete mode 100644 maplibre_gl_example/lib/layer_manipulation.dart
delete mode 100644 maplibre_gl_example/lib/line.dart
delete mode 100644 maplibre_gl_example/lib/localized_map.dart
delete mode 100644 maplibre_gl_example/lib/map_ui.dart
delete mode 100644 maplibre_gl_example/lib/move_camera.dart
delete mode 100644 maplibre_gl_example/lib/no_location_permission_page.dart
delete mode 100644 maplibre_gl_example/lib/place_batch.dart
delete mode 100644 maplibre_gl_example/lib/place_circle.dart
delete mode 100644 maplibre_gl_example/lib/place_fill.dart
delete mode 100644 maplibre_gl_example/lib/place_source.dart
delete mode 100644 maplibre_gl_example/lib/place_symbol.dart
delete mode 100644 maplibre_gl_example/lib/pmtiles.dart
delete mode 100644 maplibre_gl_example/lib/presentation/gps_location/gps_location_page.dart
delete mode 100644 maplibre_gl_example/lib/scrolling_map.dart
create mode 100644 maplibre_gl_example/lib/shared/constants.dart
create mode 100644 maplibre_gl_example/lib/shared/extensions.dart
create mode 100644 maplibre_gl_example/lib/shared/shared.dart
create mode 100644 maplibre_gl_example/lib/shared/widgets/color_picker.dart
create mode 100644 maplibre_gl_example/lib/shared/widgets/example_button.dart
create mode 100644 maplibre_gl_example/lib/shared/widgets/map_example_scaffold.dart
diff --git a/maplibre_gl/lib/maplibre_gl.dart b/maplibre_gl/lib/maplibre_gl.dart
index 2591386ca..f7e930443 100644
--- a/maplibre_gl/lib/maplibre_gl.dart
+++ b/maplibre_gl/lib/maplibre_gl.dart
@@ -69,6 +69,7 @@ export 'package:maplibre_gl_platform_interface/maplibre_gl_platform_interface.da
LocationEngineAndroidProperties,
LocationEnginePlatforms,
LocationPriority,
+ LogoViewPosition,
MapLibreMethodChannel,
MapLibrePlatform,
MinMaxZoomPreference,
diff --git a/maplibre_gl/lib/src/annotation_manager.dart b/maplibre_gl/lib/src/annotation_manager.dart
index bf1f3b4b0..8ab46870d 100644
--- a/maplibre_gl/lib/src/annotation_manager.dart
+++ b/maplibre_gl/lib/src/annotation_manager.dart
@@ -296,24 +296,32 @@ class SymbolManager extends AnnotationManager {
/// If true, the icon will be visible even if it collides with other previously drawn symbols.
Future setIconAllowOverlap(bool value) async {
+ if (value == _iconAllowOverlap) return;
+
_iconAllowOverlap = value;
await _rebuildLayers();
}
/// If true, other symbols can be visible even if they collide with the icon.
Future setTextAllowOverlap(bool value) async {
+ if (value == _textAllowOverlap) return;
+
_textAllowOverlap = value;
await _rebuildLayers();
}
/// If true, the text will be visible even if it collides with other previously drawn symbols.
Future setIconIgnorePlacement(bool value) async {
+ if (value == _iconIgnorePlacement) return;
+
_iconIgnorePlacement = value;
await _rebuildLayers();
}
/// If true, other symbols can be visible even if they collide with the text.
Future setTextIgnorePlacement(bool value) async {
+ if (value == _textIgnorePlacement) return;
+
_textIgnorePlacement = value;
await _rebuildLayers();
}
diff --git a/maplibre_gl/lib/src/maplibre_map.dart b/maplibre_gl/lib/src/maplibre_map.dart
index e105ec5af..8ce2f03c0 100644
--- a/maplibre_gl/lib/src/maplibre_map.dart
+++ b/maplibre_gl/lib/src/maplibre_map.dart
@@ -35,6 +35,8 @@ class MapLibreMap extends StatefulWidget {
this.myLocationEnabled = false,
this.myLocationTrackingMode = MyLocationTrackingMode.none,
this.myLocationRenderMode = MyLocationRenderMode.normal,
+ this.logoEnabled = false,
+ this.logoViewPosition,
this.logoViewMargins,
this.compassViewPosition,
this.compassViewMargins,
@@ -88,9 +90,10 @@ class MapLibreMap extends StatefulWidget {
/// **Available only on Android. Has no effect on iOS or Web.**
final bool translucentTextureSurface;
- /// Defines the layer order of annotations displayed on map
+ /// Defines the layer order of annotations displayed on map.
+ /// Order them from bottom to top. Bottom annotation will be rendered first.
///
- /// Any annotation type can only be contained once, so 0 to 4 types
+ /// Any annotation type can only be contained once, so 0 to 4 types.
///
/// Note that setting this to be empty gives a big perfomance boost for
/// android. However if you do so annotations will not work.
@@ -207,6 +210,13 @@ class MapLibreMap extends StatefulWidget {
/// If this is set to a value other than [MyLocationRenderMode.normal], [myLocationEnabled] needs to be true.
final MyLocationRenderMode myLocationRenderMode;
+ /// True if the MapLibre logo should be shown on the map.
+ /// Defaults to false.
+ final bool logoEnabled;
+
+ /// Set the position for the Logo
+ final LogoViewPosition? logoViewPosition;
+
/// Set the layout margins for the Logo
final Point? logoViewMargins;
@@ -389,6 +399,8 @@ class _MapLibreMapOptions {
this.myLocationEnabled,
this.myLocationTrackingMode,
this.myLocationRenderMode,
+ this.logoEnabled,
+ this.logoViewPosition,
this.logoViewMargins,
this.compassViewPosition,
this.compassViewMargins,
@@ -415,6 +427,8 @@ class _MapLibreMapOptions {
myLocationEnabled: map.myLocationEnabled,
myLocationTrackingMode: map.myLocationTrackingMode,
myLocationRenderMode: map.myLocationRenderMode,
+ logoEnabled: map.logoEnabled,
+ logoViewPosition: map.logoViewPosition,
logoViewMargins: map.logoViewMargins,
compassViewPosition: map.compassViewPosition,
compassViewMargins: map.compassViewMargins,
@@ -450,6 +464,10 @@ class _MapLibreMapOptions {
final MyLocationRenderMode? myLocationRenderMode;
+ final bool? logoEnabled;
+
+ final LogoViewPosition? logoViewPosition;
+
final Point? logoViewMargins;
final CompassViewPosition? compassViewPosition;
@@ -506,6 +524,8 @@ class _MapLibreMapOptions {
addIfNonNull('myLocationEnabled', myLocationEnabled);
addIfNonNull('myLocationTrackingMode', myLocationTrackingMode?.index);
addIfNonNull('myLocationRenderMode', myLocationRenderMode?.index);
+ addIfNonNull('logoEnabled', logoEnabled);
+ addIfNonNull('logoViewPosition', logoViewPosition?.index);
addIfNonNull('logoViewMargins', pointToArray(logoViewMargins));
addIfNonNull('compassViewPosition', compassViewPosition?.index);
addIfNonNull('compassViewMargins', pointToArray(compassViewMargins));
diff --git a/maplibre_gl_example/lib/animate_camera.dart b/maplibre_gl_example/lib/animate_camera.dart
deleted file mode 100644
index d95210a67..000000000
--- a/maplibre_gl_example/lib/animate_camera.dart
+++ /dev/null
@@ -1,245 +0,0 @@
-// Copyright 2018 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-import 'package:flutter/foundation.dart';
-import 'package:flutter/material.dart';
-import 'package:maplibre_gl/maplibre_gl.dart';
-
-import 'page.dart';
-
-class AnimateCameraPage extends ExamplePage {
- const AnimateCameraPage({super.key})
- : super(const Icon(Icons.map), 'Camera control, animated');
-
- @override
- Widget build(BuildContext context) {
- return const AnimateCamera();
- }
-}
-
-class AnimateCamera extends StatefulWidget {
- const AnimateCamera({super.key});
-
- @override
- State createState() => AnimateCameraState();
-}
-
-class AnimateCameraState extends State {
- late MapLibreMapController mapController;
- var _fps = 30;
-
- void _onMapCreated(MapLibreMapController controller) {
- mapController = controller;
- }
-
- @override
- Widget build(BuildContext context) {
- final width = MediaQuery.of(context).size.width;
- final height = MediaQuery.of(context).size.height;
-
- return Column(
- children: [
- SizedBox(
- width: width,
- height: height * 0.5,
- child: MapLibreMap(
- onMapCreated: _onMapCreated,
- initialCameraPosition:
- const CameraPosition(target: LatLng(0.0, 0.0)),
- ),
- ),
- Expanded(
- child: Padding(
- padding: const EdgeInsets.all(8.0),
- child: SingleChildScrollView(
- child: Wrap(
- spacing: 4.0,
- runSpacing: 4.0,
- alignment: WrapAlignment.center,
- children: [
- TextButton(
- onPressed: () async {
- await mapController
- .animateCamera(
- CameraUpdate.newCameraPosition(
- const CameraPosition(
- bearing: 270.0,
- target: LatLng(51.5160895, -0.1294527),
- tilt: 30.0,
- zoom: 17.0,
- ),
- ),
- )
- .then(
- (result) => debugPrint(
- "mapController.animateCamera() returned $result"),
- );
- },
- child: const Text('newCameraPosition'),
- ),
- if (!kIsWeb)
- TextButton(
- onPressed: () async {
- await mapController
- .easeCamera(
- CameraUpdate.newCameraPosition(
- const CameraPosition(
- bearing: 270.0,
- target: LatLng(46.233487, 14.363610),
- tilt: 30.0,
- zoom: 17.0,
- ),
- ),
- duration: const Duration(seconds: 2),
- )
- .then(
- (result) => debugPrint(
- "mapController.easeCamera() returned $result"),
- );
- },
- child: const Text('easeCamera'),
- ),
- TextButton(
- onPressed: () async {
- await mapController
- .animateCamera(
- CameraUpdate.newLatLng(
- const LatLng(56.1725505, 10.1850512),
- ),
- duration: const Duration(seconds: 5),
- )
- .then((result) => debugPrint(
- "mapController.animateCamera() returned $result"));
- },
- child: const Text('newLatLng'),
- ),
- TextButton(
- onPressed: () async {
- await mapController.animateCamera(
- CameraUpdate.newLatLngBounds(
- LatLngBounds(
- southwest: const LatLng(-38.483935, 113.248673),
- northeast: const LatLng(-8.982446, 153.823821),
- ),
- left: 10,
- top: 5,
- bottom: 25,
- ),
- );
- },
- child: const Text('newLatLngBounds'),
- ),
- TextButton(
- onPressed: () async {
- await mapController.animateCamera(
- CameraUpdate.newLatLngZoom(
- const LatLng(37.4231613, -122.087159),
- 11.0,
- ),
- );
- },
- child: const Text('newLatLngZoom'),
- ),
- TextButton(
- onPressed: () async {
- await mapController.animateCamera(
- CameraUpdate.scrollBy(150.0, -225.0),
- );
- },
- child: const Text('scrollBy'),
- ),
- if (!kIsWeb)
- TextButton(
- onPressed: () async {
- await mapController.queryCameraPosition().then(
- (result) => debugPrint(
- "queryCameraPosition() returned $result"),
- );
- },
- child: const Text('queryCameraPosition'),
- ),
- TextButton(
- onPressed: () async {
- _fps = _fps == 30 ? 3 : 30;
- await mapController.setMaximumFps(_fps);
- },
- child: const Text('setMaximumFps'),
- ),
- TextButton(
- onPressed: () async {
- await mapController.animateCamera(
- CameraUpdate.zoomBy(
- -0.5,
- const Offset(30.0, 20.0),
- ),
- );
- },
- child: const Text('zoomBy with focus'),
- ),
- TextButton(
- onPressed: () async {
- await mapController.animateCamera(
- CameraUpdate.newLatLngZoom(const LatLng(48, 11), 5),
- duration: const Duration(milliseconds: 300),
- );
- },
- child: const Text('latlngZoom'),
- ),
- TextButton(
- onPressed: () async {
- await mapController.animateCamera(
- CameraUpdate.zoomBy(-0.5),
- );
- },
- child: const Text('zoomBy'),
- ),
- TextButton(
- onPressed: () async {
- await mapController.animateCamera(
- CameraUpdate.zoomIn(),
- );
- },
- child: const Text('zoomIn'),
- ),
- TextButton(
- onPressed: () async {
- await mapController.animateCamera(
- CameraUpdate.zoomOut(),
- );
- },
- child: const Text('zoomOut'),
- ),
- TextButton(
- onPressed: () async {
- await mapController.animateCamera(
- CameraUpdate.zoomTo(16.0),
- );
- },
- child: const Text('zoomTo'),
- ),
- TextButton(
- onPressed: () async {
- await mapController.animateCamera(
- CameraUpdate.bearingTo(45.0),
- );
- },
- child: const Text('bearingTo'),
- ),
- TextButton(
- onPressed: () async {
- await mapController.animateCamera(
- CameraUpdate.tiltTo(30.0),
- );
- },
- child: const Text('tiltTo'),
- ),
- ],
- ),
- ),
- ),
- ),
- ],
- );
- }
-}
diff --git a/maplibre_gl_example/lib/annotation_order_maps.dart b/maplibre_gl_example/lib/annotation_order_maps.dart
deleted file mode 100644
index ad6526c5d..000000000
--- a/maplibre_gl_example/lib/annotation_order_maps.dart
+++ /dev/null
@@ -1,152 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:maplibre_gl/maplibre_gl.dart';
-
-import 'page.dart';
-import 'util.dart';
-
-class AnnotationOrderPage extends ExamplePage {
- const AnnotationOrderPage({super.key})
- : super(const Icon(Icons.layers), 'Annotation order maps');
-
- @override
- Widget build(BuildContext context) => const AnnotationOrderBody();
-}
-
-class AnnotationOrderBody extends StatefulWidget {
- const AnnotationOrderBody({super.key});
-
- @override
- State createState() => _AnnotationOrderBodyState();
-}
-
-class _AnnotationOrderBodyState extends State {
- late MapLibreMapController controllerOne;
- late MapLibreMapController controllerTwo;
-
- final LatLng center = const LatLng(36.580664, 32.5563837);
-
- @override
- Widget build(BuildContext context) {
- return Column(
- children: [
- Card(
- child: Column(
- children: [
- const Padding(
- padding: EdgeInsets.only(bottom: 5.0),
- child: Text(
- 'This map has polygones (fill) above all other anotations (default behavior)'),
- ),
- Center(
- child: SizedBox(
- width: 250.0,
- height: 250.0,
- child: MapLibreMap(
- onMapCreated: onMapCreatedOne,
- onStyleLoadedCallback: () => onStyleLoaded(controllerOne),
- initialCameraPosition: CameraPosition(
- target: center,
- zoom: 5.0,
- ),
- annotationOrder: const [
- AnnotationType.line,
- AnnotationType.symbol,
- AnnotationType.circle,
- AnnotationType.fill,
- ],
- ),
- ),
- ),
- ],
- ),
- ),
- Card(
- child: Column(
- children: [
- const Padding(
- padding: EdgeInsets.only(bottom: 5.0, top: 5.0),
- child: Text(
- 'This map has polygones (fill) under all other anotations'),
- ),
- Center(
- child: SizedBox(
- width: 250.0,
- height: 250.0,
- child: MapLibreMap(
- onMapCreated: onMapCreatedTwo,
- onStyleLoadedCallback: () => onStyleLoaded(controllerTwo),
- initialCameraPosition: CameraPosition(
- target: center,
- zoom: 5.0,
- ),
- annotationOrder: const [
- AnnotationType.fill,
- AnnotationType.line,
- AnnotationType.symbol,
- AnnotationType.circle,
- ],
- ),
- ),
- ),
- ],
- ),
- ),
- ],
- );
- }
-
- void onMapCreatedOne(MapLibreMapController controller) {
- controllerOne = controller;
- }
-
- void onMapCreatedTwo(MapLibreMapController controller) {
- controllerTwo = controller;
- }
-
- Future onStyleLoaded(MapLibreMapController controller) async {
- await addImageFromAsset(
- controller, "custom-marker", "assets/symbols/custom-marker.png");
- controller.addSymbol(
- SymbolOptions(
- geometry: LatLng(
- center.latitude,
- center.longitude,
- ),
- iconImage: "custom-marker", // "airport-15",
- ),
- );
- controller.addLine(
- const LineOptions(
- draggable: false,
- lineColor: "#ff0000",
- lineWidth: 7.0,
- lineOpacity: 1,
- geometry: [
- LatLng(35.3649902, 32.0593003),
- LatLng(34.9475098, 31.1187944),
- LatLng(36.7108154, 30.7040582),
- LatLng(37.6995850, 33.6512083),
- LatLng(35.8648682, 33.6969227),
- LatLng(35.3814697, 32.0546447),
- ],
- ),
- );
- controller.addFill(
- const FillOptions(
- draggable: false,
- fillColor: "#008888",
- fillOpacity: 0.3,
- geometry: [
- [
- LatLng(35.3649902, 32.0593003),
- LatLng(34.9475098, 31.1187944),
- LatLng(36.7108154, 30.7040582),
- LatLng(37.6995850, 33.6512083),
- LatLng(35.8648682, 33.6969227),
- LatLng(35.3814697, 32.0546447),
- ]
- ],
- ),
- );
- }
-}
diff --git a/maplibre_gl_example/lib/attribution.dart b/maplibre_gl_example/lib/attribution.dart
deleted file mode 100644
index 893da03a6..000000000
--- a/maplibre_gl_example/lib/attribution.dart
+++ /dev/null
@@ -1,101 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:maplibre_gl/maplibre_gl.dart';
-
-import 'page.dart';
-
-class AttributionPage extends ExamplePage {
- const AttributionPage({super.key})
- : super(const Icon(Icons.thumb_up), 'Attribution');
-
- @override
- Widget build(BuildContext context) {
- return const AttributionBody();
- }
-}
-
-class AttributionBody extends StatefulWidget {
- const AttributionBody({super.key});
-
- @override
- State createState() => _AttributionBodyState();
-}
-
-class _AttributionBodyState extends State {
- AttributionButtonPosition? attributionButtonPosition;
- bool useDefaultAttributionPosition = true;
-
- @override
- Widget build(BuildContext context) {
- return Column(
- children: [
- const Text("Set attribution position"),
- Wrap(
- children: [
- buildDefaultPositionButton(),
- buildPositionButton(null),
- buildPositionButton(AttributionButtonPosition.topRight),
- buildPositionButton(AttributionButtonPosition.topLeft),
- buildPositionButton(AttributionButtonPosition.bottomRight),
- buildPositionButton(AttributionButtonPosition.bottomLeft),
- ],
- ),
- Expanded(
- child: buildMap(
- attributionButtonPosition,
- useDefaultAttributionPosition,
- ),
- ),
- ],
- );
- }
-
- ElevatedButton buildDefaultPositionButton() {
- return ElevatedButton(
- onPressed: () {
- setState(() {
- attributionButtonPosition = null;
- useDefaultAttributionPosition = true;
- });
- },
- child: const Text("Default"),
- );
- }
-
- ElevatedButton buildPositionButton(AttributionButtonPosition? position) {
- return ElevatedButton(
- onPressed: () {
- setState(() {
- attributionButtonPosition = position;
- useDefaultAttributionPosition = false;
- });
- },
- child: Text(position?.name ?? "Null (=platform default)"),
- );
- }
-
- MapLibreMap buildMap(
- AttributionButtonPosition? attributionButtonPosition,
- bool useDefaultAttributionPosition,
- ) {
- if (useDefaultAttributionPosition) {
- return MapLibreMap(
- key: UniqueKey(),
- initialCameraPosition: const CameraPosition(
- target: LatLng(-33.852, 151.211),
- zoom: 11.0,
- ),
- styleString: "assets/osm_style.json",
- );
- } else {
- return MapLibreMap(
- key: UniqueKey(),
- initialCameraPosition: const CameraPosition(
- target: LatLng(-33.852, 151.211),
- zoom: 11.0,
- ),
- styleString: "assets/osm_style.json",
- attributionButtonPosition: attributionButtonPosition,
- );
- }
- }
-}
diff --git a/maplibre_gl_example/lib/click_annotations.dart b/maplibre_gl_example/lib/click_annotations.dart
deleted file mode 100644
index 7e0a13d3e..000000000
--- a/maplibre_gl_example/lib/click_annotations.dart
+++ /dev/null
@@ -1,157 +0,0 @@
-// Copyright 2018 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-import 'package:flutter/material.dart';
-import 'package:maplibre_gl/maplibre_gl.dart';
-
-import 'page.dart';
-import 'util.dart';
-
-class ClickAnnotationPage extends ExamplePage {
- const ClickAnnotationPage({super.key})
- : super(const Icon(Icons.check_circle), 'Annotation tap');
-
- @override
- Widget build(BuildContext context) {
- return const ClickAnnotationBody();
- }
-}
-
-class ClickAnnotationBody extends StatefulWidget {
- const ClickAnnotationBody({super.key});
-
- @override
- State createState() => ClickAnnotationBodyState();
-}
-
-class ClickAnnotationBodyState extends State {
- ClickAnnotationBodyState();
-
- static const LatLng center = LatLng(-33.88, 151.16);
-
- MapLibreMapController? controller;
-
- void _onMapCreated(MapLibreMapController controller) {
- this.controller = controller;
- controller.onFillTapped.add(_onFillTapped);
- controller.onCircleTapped.add(_onCircleTapped);
- controller.onLineTapped.add(_onLineTapped);
- controller.onSymbolTapped.add(_onSymbolTapped);
- }
-
- @override
- void dispose() {
- controller?.onFillTapped.remove(_onFillTapped);
- controller?.onCircleTapped.remove(_onCircleTapped);
- controller?.onLineTapped.remove(_onLineTapped);
- controller?.onSymbolTapped.remove(_onSymbolTapped);
- super.dispose();
- }
-
- void _showSnackBar(String type, String id) {
- final snackBar = SnackBar(
- content: Text(
- 'Tapped $type $id',
- style: const TextStyle(
- fontSize: 18,
- fontWeight: FontWeight.bold,
- ),
- ),
- backgroundColor: Theme.of(context).primaryColor);
- ScaffoldMessenger.of(context).clearSnackBars();
- ScaffoldMessenger.of(context).showSnackBar(snackBar);
- }
-
- void _onFillTapped(Fill fill) {
- _showSnackBar('fill', fill.id);
- }
-
- void _onCircleTapped(Circle circle) {
- _showSnackBar('circle', circle.id);
- }
-
- void _onLineTapped(Line line) {
- _showSnackBar('line', line.id);
- }
-
- void _onSymbolTapped(Symbol symbol) {
- _showSnackBar('symbol', symbol.id);
- }
-
- Future _onStyleLoaded() async {
- await addImageFromAsset(
- controller!, "custom-marker", "assets/symbols/custom-marker.png");
- controller!.addCircle(
- const CircleOptions(
- geometry: LatLng(-33.881979408447314, 151.171361438502117),
- circleStrokeColor: "#00FF00",
- circleStrokeWidth: 2,
- circleRadius: 16,
- ),
- );
- controller!.addCircle(
- const CircleOptions(
- geometry: LatLng(-33.894372606072309, 151.17576679759523),
- circleStrokeColor: "#00FF00",
- circleStrokeWidth: 2,
- circleRadius: 30,
- ),
- );
- controller!.addSymbol(
- const SymbolOptions(
- geometry: LatLng(-33.894372606072309, 151.17576679759523),
- iconImage: "custom-marker", //"fast-food-15",
- iconSize: 2),
- );
- controller!.addLine(
- const LineOptions(
- geometry: [
- LatLng(-33.874867744475786, 151.170627211986584),
- LatLng(-33.881979408447314, 151.171361438502117),
- LatLng(-33.887058805548882, 151.175032571079726),
- LatLng(-33.894372606072309, 151.17576679759523),
- LatLng(-33.900060683994681, 151.15765587687909),
- ],
- lineColor: "#0000FF",
- lineWidth: 20,
- ),
- );
-
- controller!.addFill(
- const FillOptions(
- geometry: [
- [
- LatLng(-33.901517742631846, 151.178099204457737),
- LatLng(-33.872845324482071, 151.179025547977773),
- LatLng(-33.868230472039514, 151.147000529140399),
- LatLng(-33.883172899638311, 151.150838238009328),
- LatLng(-33.894158309528244, 151.14223647675135),
- LatLng(-33.904812805307806, 151.155999294764086),
- LatLng(-33.901517742631846, 151.178099204457737),
- ],
- ],
- fillColor: "#FF0000",
- fillOutlineColor: "#000000",
- ),
- );
- }
-
- @override
- Widget build(BuildContext context) {
- return MapLibreMap(
- annotationOrder: const [
- AnnotationType.fill,
- AnnotationType.line,
- AnnotationType.circle,
- AnnotationType.symbol,
- ],
- onMapCreated: _onMapCreated,
- onStyleLoadedCallback: _onStyleLoaded,
- initialCameraPosition: const CameraPosition(
- target: center,
- zoom: 12.0,
- ),
- );
- }
-}
diff --git a/maplibre_gl_example/lib/offline_region_map.dart b/maplibre_gl_example/lib/examples/advanced/offline_region_map.dart
similarity index 100%
rename from maplibre_gl_example/lib/offline_region_map.dart
rename to maplibre_gl_example/lib/examples/advanced/offline_region_map.dart
diff --git a/maplibre_gl_example/lib/offline_regions.dart b/maplibre_gl_example/lib/examples/advanced/offline_regions.dart
similarity index 51%
rename from maplibre_gl_example/lib/offline_regions.dart
rename to maplibre_gl_example/lib/examples/advanced/offline_regions.dart
index c09a2a0fe..5d714aa3a 100644
--- a/maplibre_gl_example/lib/offline_regions.dart
+++ b/maplibre_gl_example/lib/examples/advanced/offline_regions.dart
@@ -5,7 +5,8 @@ import 'package:flutter/material.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
import 'offline_region_map.dart';
-import 'page.dart';
+import '../../page.dart';
+import '../../shared/shared.dart';
final LatLngBounds hawaiiBounds = LatLngBounds(
southwest: const LatLng(17.26672, -161.14746),
@@ -101,22 +102,24 @@ final List allRegions = [
class OfflineRegionsPage extends ExamplePage {
const OfflineRegionsPage({super.key})
- : super(const Icon(Icons.map), 'Offline Regions');
+ : super(
+ const Icon(Icons.cloud_off),
+ 'Offline Regions',
+ category: ExampleCategory.advanced,
+ );
@override
- Widget build(BuildContext context) {
- return const OfflineRegionBody();
- }
+ Widget build(BuildContext context) => const _OfflineRegionBody();
}
-class OfflineRegionBody extends StatefulWidget {
- const OfflineRegionBody({super.key});
+class _OfflineRegionBody extends StatefulWidget {
+ const _OfflineRegionBody();
@override
- State createState() => _OfflineRegionsBodyState();
+ State<_OfflineRegionBody> createState() => _OfflineRegionsBodyState();
}
-class _OfflineRegionsBodyState extends State {
+class _OfflineRegionsBodyState extends State<_OfflineRegionBody> {
final List _items = [];
@override
@@ -127,60 +130,105 @@ class _OfflineRegionsBodyState extends State {
@override
Widget build(BuildContext context) {
- return Stack(
- children: [
- ListView.builder(
- padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 8),
- itemCount: _items.length,
- itemBuilder: (context, index) => Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- IconButton(
- icon: const Icon(Icons.map),
- onPressed: () => _goToMap(_items[index]),
- ),
- Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- _items[index].name,
- style: const TextStyle(
- fontWeight: FontWeight.bold,
- fontSize: 18,
+ return _items.isEmpty
+ ? const Center(
+ child: CircularProgressIndicator(),
+ )
+ : ListView.builder(
+ padding: const EdgeInsets.all(16),
+ itemCount: _items.length,
+ itemBuilder: (context, index) {
+ final item = _items[index];
+ return Card(
+ margin: const EdgeInsets.only(bottom: 12),
+ child: Column(
+ children: [
+ ListTile(
+ contentPadding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 8,
+ ),
+ leading: Container(
+ width: 48,
+ height: 48,
+ decoration: BoxDecoration(
+ color: item.isDownloaded
+ ? Theme.of(context).colorScheme.primaryContainer
+ : Theme.of(context)
+ .colorScheme
+ .surfaceContainerHighest,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Icon(
+ item.isDownloaded
+ ? Icons.cloud_done
+ : Icons.cloud_download,
+ color: item.isDownloaded
+ ? Theme.of(context).colorScheme.onPrimaryContainer
+ : Theme.of(context).colorScheme.onSurfaceVariant,
+ ),
+ ),
+ title: Text(
+ item.name,
+ style:
+ Theme.of(context).textTheme.titleMedium?.copyWith(
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ subtitle: Text(
+ 'Estimated tiles: ${item.estimatedTiles}',
+ style: Theme.of(context).textTheme.bodyMedium,
+ ),
+ trailing: item.isDownloading
+ ? const SizedBox(
+ width: 24,
+ height: 24,
+ child: CircularProgressIndicator(
+ strokeWidth: 2,
+ ),
+ )
+ : null,
),
- ),
- Text(
- 'Est. tiles: ${_items[index].estimatedTiles}',
- style: const TextStyle(
- fontSize: 16,
+ Padding(
+ padding: const EdgeInsets.all(12),
+ child: Row(
+ children: [
+ Expanded(
+ child: ExampleButton(
+ label: 'View Map',
+ icon: Icons.map_outlined,
+ onPressed: () => _goToMap(item),
+ style: ExampleButtonStyle.outlined,
+ ),
+ ),
+ const SizedBox(width: 8),
+ Expanded(
+ child: item.isDownloaded
+ ? ExampleButton(
+ label: 'Delete',
+ icon: Icons.delete_outline,
+ onPressed: item.isDownloading
+ ? null
+ : () => _deleteRegion(item, index),
+ style: ExampleButtonStyle.destructive,
+ )
+ : ExampleButton(
+ label: 'Download',
+ icon: Icons.download,
+ onPressed: item.isDownloading
+ ? null
+ : () => _downloadRegion(item, index),
+ style: ExampleButtonStyle.filled,
+ ),
+ ),
+ ],
+ ),
),
- ),
- ],
- ),
- const Spacer(),
- if (_items[index].isDownloading)
- const SizedBox(
- height: 16,
- width: 16,
- child: CircularProgressIndicator(),
- )
- else
- IconButton(
- icon: Icon(
- _items[index].isDownloaded
- ? Icons.delete
- : Icons.file_download,
- ),
- onPressed: _items[index].isDownloaded
- ? () => _deleteRegion(_items[index], index)
- : () => _downloadRegion(_items[index], index),
+ ],
),
- ],
- ),
- ),
- ],
- );
+ );
+ },
+ );
}
Future _updateListOfRegions() async {
diff --git a/maplibre_gl_example/lib/examples/advanced/pmtiles.dart b/maplibre_gl_example/lib/examples/advanced/pmtiles.dart
new file mode 100644
index 000000000..68a3bb2c2
--- /dev/null
+++ b/maplibre_gl_example/lib/examples/advanced/pmtiles.dart
@@ -0,0 +1,87 @@
+import 'dart:async';
+import 'package:flutter/material.dart';
+import 'package:maplibre_gl/maplibre_gl.dart';
+import '../../page.dart';
+import '../../shared/shared.dart';
+
+const _nullIsland = CameraPosition(target: LatLng(0, 0), zoom: 4.0);
+
+/// Example demonstrating PMTiles vector tiles format
+class PMTilesPage extends ExamplePage {
+ const PMTilesPage({super.key})
+ : super(
+ const Icon(Icons.map_outlined),
+ 'PMTiles',
+ category: ExampleCategory.advanced,
+ );
+
+ @override
+ Widget build(BuildContext context) => const _PMTilesBody();
+}
+
+class _PMTilesBody extends StatefulWidget {
+ const _PMTilesBody();
+
+ @override
+ State<_PMTilesBody> createState() => _PMTilesBodyState();
+}
+
+class _PMTilesBodyState extends State<_PMTilesBody> {
+ MapLibreMapController? _mapController;
+ bool _canInteractWithMap = false;
+ bool _canReset = false;
+
+ void _onMapCreated(MapLibreMapController controller) {
+ _mapController = controller;
+ }
+
+ void _onStyleLoaded() {
+ setState(() => _canInteractWithMap = true);
+ }
+
+ Future _moveCameraToLondon() async {
+ await _mapController?.animateCamera(
+ CameraUpdate.newCameraPosition(ExampleConstants.londonCameraPosition),
+ );
+ setState(() => _canReset = true);
+ }
+
+ Future _resetCamera() async {
+ await _mapController?.animateCamera(
+ CameraUpdate.newCameraPosition(_nullIsland),
+ );
+ setState(() => _canReset = false);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ body: MapLibreMap(
+ styleString: 'assets/pmtiles_style.json',
+ onMapCreated: _onMapCreated,
+ onStyleLoadedCallback: _onStyleLoaded,
+ initialCameraPosition: _nullIsland,
+ logoEnabled: true,
+ trackCameraPosition: true,
+ compassEnabled: true,
+ ),
+ floatingActionButton: ExampleButton(
+ label: _canReset ? 'Reset camera' : 'Go to London',
+ icon: _canReset ? Icons.refresh : Icons.flight_takeoff,
+ onPressed: _canInteractWithMap
+ ? _canReset
+ ? _resetCamera
+ : _moveCameraToLondon
+ : null,
+ style: ExampleButtonStyle.tonal,
+ ),
+ floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
+ );
+ }
+
+ @override
+ void dispose() {
+ _mapController?.dispose();
+ super.dispose();
+ }
+}
diff --git a/maplibre_gl_example/lib/examples/advanced/translucent_full_map.dart b/maplibre_gl_example/lib/examples/advanced/translucent_full_map.dart
new file mode 100644
index 000000000..da62be019
--- /dev/null
+++ b/maplibre_gl_example/lib/examples/advanced/translucent_full_map.dart
@@ -0,0 +1,108 @@
+import 'dart:async';
+import 'package:flutter/material.dart';
+import 'package:maplibre_gl/maplibre_gl.dart';
+import '../../page.dart';
+import '../../shared/shared.dart';
+
+const _nullIsland = CameraPosition(target: LatLng(0, 0), zoom: 4.0);
+
+/// Example demonstrating a translucent map with content underneath
+class TranslucentFullMapPage extends ExamplePage {
+ const TranslucentFullMapPage({super.key})
+ : super(
+ const Icon(Icons.layers),
+ 'Translucent map',
+ category: ExampleCategory.advanced,
+ );
+
+ @override
+ Widget build(BuildContext context) => const _TranslucentMapBody();
+}
+
+class _TranslucentMapBody extends StatefulWidget {
+ const _TranslucentMapBody();
+
+ @override
+ State<_TranslucentMapBody> createState() => _TranslucentMapBodyState();
+}
+
+class _TranslucentMapBodyState extends State<_TranslucentMapBody> {
+ MapLibreMapController? _mapController;
+ bool _canInteractWithMap = false;
+ bool _canReset = false;
+
+ void _onMapCreated(MapLibreMapController controller) {
+ _mapController = controller;
+ }
+
+ void _onStyleLoaded() {
+ setState(() => _canInteractWithMap = true);
+ }
+
+ Future _moveCameraToTokyo() async {
+ await _mapController?.animateCamera(
+ CameraUpdate.newCameraPosition(ExampleConstants.tokyoCameraPosition),
+ );
+ setState(() => _canReset = true);
+ }
+
+ Future _resetCamera() async {
+ await _mapController?.animateCamera(
+ CameraUpdate.newCameraPosition(ExampleConstants.defaultCameraPosition),
+ );
+ setState(() => _canReset = false);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ body: Stack(
+ children: [
+ const ColoredBox(
+ color: Colors.blue,
+ child: Center(
+ child: Text(
+ 'Any widget can be here',
+ style: TextStyle(
+ fontSize: 20,
+ color: Colors.white,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ ),
+ MapLibreMap(
+ styleString: 'assets/translucent_style.json',
+ onMapCreated: _onMapCreated,
+ onStyleLoadedCallback: _onStyleLoaded,
+ initialCameraPosition: ExampleConstants.defaultCameraPosition,
+ logoEnabled: true,
+ trackCameraPosition: true,
+ compassEnabled: true,
+ // This is a random color, for example purposes.
+ foregroundLoadColor: Colors.purple,
+ // This sets the map to be translucent.
+ translucentTextureSurface: true,
+ ),
+ ],
+ ),
+ floatingActionButton: ExampleButton(
+ label: _canReset ? 'Reset camera' : 'Go to Tokyo',
+ icon: _canReset ? Icons.refresh : Icons.location_searching,
+ onPressed: _canInteractWithMap
+ ? _canReset
+ ? _resetCamera
+ : _moveCameraToTokyo
+ : null,
+ style: ExampleButtonStyle.tonal,
+ ),
+ floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
+ );
+ }
+
+ @override
+ void dispose() {
+ _mapController?.dispose();
+ super.dispose();
+ }
+}
diff --git a/maplibre_gl_example/lib/examples/annotations/annotation_order_example.dart b/maplibre_gl_example/lib/examples/annotations/annotation_order_example.dart
new file mode 100644
index 000000000..00627d48a
--- /dev/null
+++ b/maplibre_gl_example/lib/examples/annotations/annotation_order_example.dart
@@ -0,0 +1,256 @@
+import 'package:flutter/material.dart';
+import 'package:maplibre_gl/maplibre_gl.dart';
+import '../../page.dart';
+import '../../shared/shared.dart';
+import '../../util.dart';
+
+/// Example demonstrating annotation rendering order control
+class AnnotationOrderExample extends ExamplePage {
+ const AnnotationOrderExample({super.key})
+ : super(
+ const Icon(Icons.layers),
+ 'Annotation Order',
+ category: ExampleCategory.annotations,
+ );
+
+ @override
+ Widget build(BuildContext context) => const _AnnotationOrderBody();
+}
+
+class _AnnotationOrderBody extends StatefulWidget {
+ const _AnnotationOrderBody();
+
+ @override
+ State<_AnnotationOrderBody> createState() => _AnnotationOrderBodyState();
+}
+
+class _AnnotationOrderBodyState extends State<_AnnotationOrderBody> {
+ MapLibreMapController? _controller;
+
+ // Default order: bottom to top
+ final List _annotationOrder = [
+ AnnotationType.fill,
+ AnnotationType.line,
+ AnnotationType.circle,
+ AnnotationType.symbol,
+ ];
+
+ final LatLng _center = const LatLng(36.580664, 32.5563837);
+
+ void _onMapCreated(MapLibreMapController controller) {
+ setState(() => _controller = controller);
+ }
+
+ Future _onStyleLoaded() async {
+ if (_controller == null) return;
+
+ await addImageFromAsset(
+ _controller!,
+ "custom-marker",
+ "assets/symbols/custom-marker.png",
+ );
+
+ await _controller!.addSymbol(
+ const SymbolOptions(
+ geometry: LatLng(35.5, 31.8),
+ iconImage: "custom-marker",
+ iconSize: 2.5,
+ iconOpacity: 1.0,
+ ),
+ );
+
+ await _controller!.addLine(
+ const LineOptions(
+ lineColor: "#ff0000",
+ lineWidth: 8.0,
+ lineOpacity: 1.0,
+ geometry: [
+ LatLng(37.5, 31.0),
+ LatLng(36.5, 31.3),
+ LatLng(36.0, 31.5),
+ LatLng(35.5, 31.8),
+ LatLng(35.0, 32.0),
+ LatLng(34.5, 31.5),
+ ],
+ ),
+ );
+
+ await _controller!.addFill(
+ const FillOptions(
+ fillColor: "#00aa88",
+ fillOpacity: 1.0,
+ fillOutlineColor: "#008866",
+ geometry: [
+ [
+ LatLng(35.3649902, 32.0593003),
+ LatLng(34.9475098, 31.1187944),
+ LatLng(36.7108154, 30.7040582),
+ LatLng(37.6995850, 33.6512083),
+ LatLng(35.3814697, 32.0546447),
+ ]
+ ],
+ ),
+ );
+
+ await _controller!.addCircle(
+ const CircleOptions(
+ geometry: LatLng(36.0, 31.5),
+ circleRadius: 13.0,
+ circleColor: "#4169E1",
+ circleOpacity: 1.0,
+ ),
+ );
+ }
+
+ void _recreateMap() {
+ setState(() {
+ _controller = null;
+ });
+ }
+
+ IconData _getIconForType(AnnotationType type) {
+ switch (type) {
+ case AnnotationType.fill:
+ return Icons.square;
+ case AnnotationType.line:
+ return Icons.show_chart;
+ case AnnotationType.symbol:
+ return Icons.place;
+ case AnnotationType.circle:
+ return Icons.circle;
+ }
+ }
+
+ String _getNameForType(AnnotationType type) {
+ switch (type) {
+ case AnnotationType.fill:
+ return 'Fill';
+ case AnnotationType.line:
+ return 'Line';
+ case AnnotationType.symbol:
+ return 'Symbol';
+ case AnnotationType.circle:
+ return 'Circle';
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return MapExampleScaffold(
+ mapHeightRatio: 0.4,
+ map: MapLibreMap(
+ key: ValueKey(_annotationOrder.toString()),
+ styleString: ExampleConstants.demoMapStyle,
+ onMapCreated: _onMapCreated,
+ onStyleLoadedCallback: _onStyleLoaded,
+ initialCameraPosition: CameraPosition(
+ target: _center,
+ zoom: 5.5,
+ ),
+ annotationOrder: _annotationOrder,
+ ),
+ controls: [
+ const InfoCard(
+ title: 'Drag to reorder Annotations',
+ subtitle: 'Change rendering order from bottom to top',
+ icon: Icons.layers,
+ ),
+ Card(
+ child: Padding(
+ padding: const EdgeInsets.all(8),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Icon(
+ Icons.drag_indicator,
+ color: Theme.of(context).colorScheme.primary,
+ size: 20,
+ ),
+ const SizedBox(width: 8),
+ Text(
+ 'Rendering order (Last on Top)',
+ style: Theme.of(context).textTheme.titleSmall?.copyWith(
+ fontWeight: FontWeight.w600,
+ color: Theme.of(context).colorScheme.primary,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 4),
+ ReorderableListView.builder(
+ shrinkWrap: true,
+ physics: const NeverScrollableScrollPhysics(),
+ itemCount: _annotationOrder.length,
+ onReorder: (oldIndex, newIndex) {
+ setState(() {
+ if (newIndex > oldIndex) {
+ newIndex -= 1;
+ }
+ final item = _annotationOrder.removeAt(oldIndex);
+ _annotationOrder.insert(newIndex, item);
+ _recreateMap();
+ });
+ },
+ itemBuilder: (context, index) {
+ final type = _annotationOrder[index];
+ return Container(
+ key: ValueKey(type),
+ margin: const EdgeInsets.only(bottom: 8),
+ child: Card(
+ elevation: 2,
+ child: ListTile(
+ leading: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Container(
+ width: 32,
+ height: 32,
+ decoration: BoxDecoration(
+ color: Theme.of(context)
+ .colorScheme
+ .primaryContainer,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Center(
+ child: Text(
+ '${index + 1}',
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ color: Theme.of(context)
+ .colorScheme
+ .onPrimaryContainer,
+ ),
+ ),
+ ),
+ ),
+ const SizedBox(width: 12),
+ Icon(
+ _getIconForType(type),
+ color: Theme.of(context).colorScheme.primary,
+ ),
+ ],
+ ),
+ title: Text(
+ _getNameForType(type),
+ style: Theme.of(context).textTheme.bodyLarge,
+ ),
+ trailing: Icon(
+ Icons.drag_handle,
+ color:
+ Theme.of(context).colorScheme.onSurfaceVariant,
+ ),
+ ),
+ ),
+ );
+ },
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+}
diff --git a/maplibre_gl_example/lib/examples/annotations/annotation_properties_example.dart b/maplibre_gl_example/lib/examples/annotations/annotation_properties_example.dart
new file mode 100644
index 000000000..bf30eb4b7
--- /dev/null
+++ b/maplibre_gl_example/lib/examples/annotations/annotation_properties_example.dart
@@ -0,0 +1,690 @@
+import 'package:flutter/material.dart';
+import 'package:maplibre_gl/maplibre_gl.dart';
+import 'package:maplibre_gl_example/util.dart';
+import '../../page.dart';
+import '../../shared/shared.dart';
+
+/// Example demonstrating dynamic annotation property updates
+class AnnotationPropertiesExample extends ExamplePage {
+ const AnnotationPropertiesExample({super.key})
+ : super(
+ const Icon(Icons.tune),
+ 'Annotation Properties',
+ category: ExampleCategory.annotations,
+ );
+
+ @override
+ Widget build(BuildContext context) => const _AnnotationPropertiesBody();
+}
+
+enum PropertyAnnotationType { symbol, circle, fill, line }
+
+class _AnnotationPropertiesBody extends StatefulWidget {
+ const _AnnotationPropertiesBody();
+
+ @override
+ State<_AnnotationPropertiesBody> createState() =>
+ _AnnotationPropertiesBodyState();
+}
+
+class _AnnotationPropertiesBodyState extends State<_AnnotationPropertiesBody> {
+ MapLibreMapController? _controller;
+ PropertyAnnotationType _currentType = PropertyAnnotationType.symbol;
+
+ // Single annotation of each type
+ Symbol? _symbol;
+ Circle? _circle;
+ Fill? _fill;
+ Line? _line;
+
+ // Symbol properties
+ double _iconSize = 2.0;
+ double _iconRotate = 0.0;
+ double _textSize = 12.0;
+ String _textField = 'Symbol';
+
+ // Circle properties
+ double _circleRadius = 20.0;
+ double _circleOpacity = 0.8;
+ double _circleStrokeWidth = 2.0;
+ String _circleColor = '#3498DB';
+
+ // Fill properties
+ double _fillOpacity = 0.6;
+ String _fillColor = '#E74C3C';
+ String _fillOutlineColor = '#C0392B';
+
+ // Line properties
+ double _lineWidth = 4.0;
+ double _lineOpacity = 0.9;
+ String _lineColor = '#2ECC71';
+ double _lineBlur = 0.0;
+
+ @override
+ void initState() {
+ super.initState();
+ }
+
+ void _onMapCreated(MapLibreMapController controller) {
+ setState(() => _controller = controller);
+ }
+
+ Future _onStyleLoaded() async {
+ await _addInitialAnnotations();
+ }
+
+ Future _addInitialAnnotations() async {
+ if (_controller == null) return;
+
+ try {
+ await addImageFromAsset(
+ _controller!,
+ "custom-marker",
+ "assets/symbols/custom-marker.png",
+ );
+
+ const center = ExampleConstants.sydneyCenter;
+
+ // Add one of each annotation type at different positions
+ _symbol = await _controller!.addSymbol(
+ SymbolOptions(
+ geometry: LatLng(center.latitude + 0.15, center.longitude),
+ iconImage: 'custom-marker',
+ iconSize: _iconSize,
+ iconRotate: _iconRotate,
+ textField: _textField,
+ textSize: _textSize,
+ textOffset: const Offset(0, 2),
+ ),
+ );
+
+ _circle = await _controller!.addCircle(
+ CircleOptions(
+ geometry: LatLng(center.latitude + 0.05, center.longitude),
+ circleRadius: _circleRadius,
+ circleColor: _circleColor,
+ circleOpacity: _circleOpacity,
+ circleStrokeWidth: _circleStrokeWidth,
+ circleStrokeColor: '#FFFFFF',
+ ),
+ );
+
+ _fill = await _controller!.addFill(
+ FillOptions(
+ geometry: [
+ _generatePolygon(
+ LatLng(center.latitude - 0.05, center.longitude),
+ )
+ ],
+ fillColor: _fillColor,
+ fillOpacity: _fillOpacity,
+ fillOutlineColor: _fillOutlineColor,
+ ),
+ );
+
+ _line = await _controller!.addLine(
+ LineOptions(
+ geometry: _generateLineString(
+ LatLng(center.latitude - 0.15, center.longitude),
+ ),
+ lineColor: _lineColor,
+ lineWidth: _lineWidth,
+ lineOpacity: _lineOpacity,
+ lineBlur: _lineBlur,
+ ),
+ );
+
+ setState(() {});
+ } catch (e) {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text('Error adding annotations: $e')),
+ );
+ }
+ }
+ }
+
+ List _generatePolygon(LatLng center) {
+ final points = [];
+ const size = 0.025;
+ points.add(LatLng(center.latitude - size, center.longitude - size));
+ points.add(LatLng(center.latitude - size, center.longitude + size));
+ points.add(LatLng(center.latitude + size, center.longitude + size));
+ points.add(LatLng(center.latitude + size, center.longitude - size));
+ points.add(LatLng(center.latitude - size, center.longitude - size));
+ return points;
+ }
+
+ List _generateLineString(LatLng center) {
+ final points = [];
+ const step = 0.02;
+ for (var i = 0; i < 5; i++) {
+ points.add(LatLng(
+ center.latitude + (i * step * 0.3),
+ center.longitude - (step * 2) + (i * step),
+ ));
+ }
+ return points;
+ }
+
+ Future _updateSymbolProperties() async {
+ if (_controller == null || _symbol == null) return;
+
+ try {
+ await _controller!.updateSymbol(
+ _symbol!,
+ SymbolOptions(
+ iconSize: _iconSize,
+ iconRotate: _iconRotate,
+ textField: _textField,
+ textSize: _textSize,
+ ),
+ );
+ } catch (e) {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text('Error updating symbol: $e')),
+ );
+ }
+ }
+ }
+
+ Future _updateCircleProperties() async {
+ if (_controller == null || _circle == null) return;
+
+ try {
+ await _controller!.updateCircle(
+ _circle!,
+ CircleOptions(
+ circleRadius: _circleRadius,
+ circleColor: _circleColor,
+ circleOpacity: _circleOpacity,
+ circleStrokeWidth: _circleStrokeWidth,
+ ),
+ );
+ } catch (e) {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text('Error updating circle: $e')),
+ );
+ }
+ }
+ }
+
+ Future _updateFillProperties() async {
+ if (_controller == null || _fill == null) return;
+
+ try {
+ await _controller!.updateFill(
+ _fill!,
+ FillOptions(
+ fillColor: _fillColor,
+ fillOpacity: _fillOpacity,
+ fillOutlineColor: _fillOutlineColor,
+ ),
+ );
+ } catch (e) {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text('Error updating fill: $e')),
+ );
+ }
+ }
+ }
+
+ Future _updateLineProperties() async {
+ if (_controller == null || _line == null) return;
+
+ try {
+ await _controller!.updateLine(
+ _line!,
+ LineOptions(
+ lineColor: _lineColor,
+ lineWidth: _lineWidth,
+ lineOpacity: _lineOpacity,
+ lineBlur: _lineBlur,
+ ),
+ );
+ } catch (e) {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text('Error updating line: $e')),
+ );
+ }
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return MapExampleScaffold(
+ map: MapLibreMap(
+ styleString: ExampleConstants.demoMapStyle,
+ onMapCreated: _onMapCreated,
+ onStyleLoadedCallback: _onStyleLoaded,
+ initialCameraPosition: const CameraPosition(
+ target: ExampleConstants.sydneyCenter,
+ zoom: 9,
+ ),
+ trackCameraPosition: true,
+ ),
+ controls: _controller == null
+ ? []
+ : [
+ Card(
+ child: Padding(
+ padding: const EdgeInsets.all(12),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ 'Annotation Type',
+ style: Theme.of(context).textTheme.titleSmall?.copyWith(
+ fontWeight: FontWeight.w600,
+ color: Theme.of(context).colorScheme.primary,
+ ),
+ ),
+ const SizedBox(height: 8),
+ ExampleSegmentedButton(
+ segments: const [
+ ExampleSegment(
+ value: PropertyAnnotationType.symbol,
+ label: 'Symbol',
+ icon: Icons.place,
+ ),
+ ExampleSegment(
+ value: PropertyAnnotationType.circle,
+ label: 'Circle',
+ icon: Icons.circle_outlined,
+ ),
+ ExampleSegment(
+ value: PropertyAnnotationType.fill,
+ label: 'Fill',
+ icon: Icons.square,
+ ),
+ ExampleSegment(
+ value: PropertyAnnotationType.line,
+ label: 'Line',
+ icon: Icons.timeline,
+ ),
+ ],
+ selected: _currentType,
+ onSelectionChanged: (type) {
+ setState(() => _currentType = type);
+ },
+ ),
+ ],
+ ),
+ ),
+ ),
+ if (_currentType == PropertyAnnotationType.symbol)
+ ..._buildSymbolControls(),
+ if (_currentType == PropertyAnnotationType.circle)
+ ..._buildCircleControls(),
+ if (_currentType == PropertyAnnotationType.fill)
+ ..._buildFillControls(),
+ if (_currentType == PropertyAnnotationType.line)
+ ..._buildLineControls(),
+ ],
+ );
+ }
+
+ List _buildSymbolControls() {
+ return [
+ ControlGroup(
+ title: 'Icon Properties',
+ children: [
+ ListTile(
+ title: Text('Icon Size: ${_iconSize.toStringAsFixed(1)}'),
+ subtitle: Slider(
+ value: _iconSize,
+ min: 0.5,
+ max: 4.0,
+ divisions: 35,
+ onChanged: (value) async {
+ setState(() => _iconSize = value);
+ await _updateSymbolProperties();
+ },
+ ),
+ ),
+ ListTile(
+ title: Text('Icon Rotation: ${_iconRotate.toStringAsFixed(0)}°'),
+ subtitle: Slider(
+ value: _iconRotate,
+ min: 0.0,
+ max: 360.0,
+ divisions: 72,
+ onChanged: (value) async {
+ setState(() => _iconRotate = value);
+ await _updateSymbolProperties();
+ },
+ ),
+ ),
+ ],
+ ),
+ ControlGroup(
+ title: 'Text Properties',
+ children: [
+ ListTile(
+ title: const Text('Text Content'),
+ subtitle: TextField(
+ controller: TextEditingController(text: _textField)
+ ..selection =
+ TextSelection.collapsed(offset: _textField.length),
+ onChanged: (value) async {
+ setState(() => _textField = value);
+ await _updateSymbolProperties();
+ },
+ decoration: const InputDecoration(
+ hintText: 'Enter text',
+ border: OutlineInputBorder(),
+ ),
+ ),
+ ),
+ ListTile(
+ title: Text('Text Size: ${_textSize.toStringAsFixed(1)}'),
+ subtitle: Slider(
+ value: _textSize,
+ min: 8.0,
+ max: 24.0,
+ divisions: 32,
+ onChanged: (value) async {
+ setState(() => _textSize = value);
+ await _updateSymbolProperties();
+ },
+ ),
+ ),
+ ],
+ ),
+ ControlGroup(
+ title: 'Actions',
+ children: [
+ ExampleButton(
+ label: 'Reset Properties',
+ onPressed: () async {
+ setState(() {
+ _iconSize = 2.0;
+ _iconRotate = 0.0;
+ _textSize = 12.0;
+ _textField = 'Symbol';
+ });
+ await _updateSymbolProperties();
+ },
+ ),
+ ],
+ ),
+ ];
+ }
+
+ List _buildCircleControls() {
+ return [
+ ControlGroup(
+ title: 'Circle Appearance',
+ children: [
+ ListTile(
+ title: Text('Radius: ${_circleRadius.toStringAsFixed(1)}'),
+ subtitle: Slider(
+ value: _circleRadius,
+ min: 5.0,
+ max: 50.0,
+ divisions: 45,
+ onChanged: (value) async {
+ setState(() => _circleRadius = value);
+ await _updateCircleProperties();
+ },
+ ),
+ ),
+ ListTile(
+ title:
+ Text('Opacity: ${(_circleOpacity * 100).toStringAsFixed(0)}%'),
+ subtitle: Slider(
+ value: _circleOpacity,
+ min: 0.0,
+ max: 1.0,
+ divisions: 20,
+ onChanged: (value) async {
+ setState(() => _circleOpacity = value);
+ await _updateCircleProperties();
+ },
+ ),
+ ),
+ ListTile(
+ title:
+ Text('Stroke Width: ${_circleStrokeWidth.toStringAsFixed(1)}'),
+ subtitle: Slider(
+ value: _circleStrokeWidth,
+ min: 0.0,
+ max: 10.0,
+ divisions: 20,
+ onChanged: (value) async {
+ setState(() => _circleStrokeWidth = value);
+ await _updateCircleProperties();
+ },
+ ),
+ ),
+ ListTile(
+ title: const Text('Color'),
+ trailing: Container(
+ width: 48,
+ height: 48,
+ decoration: BoxDecoration(
+ color: Color(int.parse(_circleColor.substring(1), radix: 16) +
+ 0xFF000000),
+ border: Border.all(color: Colors.grey),
+ borderRadius: BorderRadius.circular(4),
+ ),
+ ),
+ onTap: () => _pickColor('circle'),
+ ),
+ ],
+ ),
+ ControlGroup(
+ title: 'Actions',
+ children: [
+ ExampleButton(
+ label: 'Reset Properties',
+ onPressed: () async {
+ setState(() {
+ _circleRadius = 20.0;
+ _circleOpacity = 0.8;
+ _circleStrokeWidth = 2.0;
+ _circleColor = '#3498DB';
+ });
+ await _updateCircleProperties();
+ },
+ ),
+ ],
+ ),
+ ];
+ }
+
+ List _buildFillControls() {
+ return [
+ ControlGroup(
+ title: 'Fill Appearance',
+ children: [
+ ListTile(
+ title: Text('Opacity: ${(_fillOpacity * 100).toStringAsFixed(0)}%'),
+ subtitle: Slider(
+ value: _fillOpacity,
+ min: 0.0,
+ max: 1.0,
+ divisions: 20,
+ onChanged: (value) async {
+ setState(() => _fillOpacity = value);
+ await _updateFillProperties();
+ },
+ ),
+ ),
+ ListTile(
+ title: const Text('Fill Color'),
+ trailing: Container(
+ width: 48,
+ height: 48,
+ decoration: BoxDecoration(
+ color: Color(
+ int.parse(_fillColor.substring(1), radix: 16) + 0xFF000000),
+ border: Border.all(color: Colors.grey),
+ borderRadius: BorderRadius.circular(4),
+ ),
+ ),
+ onTap: () => _pickColor('fill'),
+ ),
+ ListTile(
+ title: const Text('Outline Color'),
+ trailing: Container(
+ width: 48,
+ height: 48,
+ decoration: BoxDecoration(
+ color: Color(
+ int.parse(_fillOutlineColor.substring(1), radix: 16) +
+ 0xFF000000),
+ border: Border.all(color: Colors.grey),
+ borderRadius: BorderRadius.circular(4),
+ ),
+ ),
+ onTap: () => _pickColor('fillOutline'),
+ ),
+ ],
+ ),
+ ControlGroup(
+ title: 'Actions',
+ children: [
+ ExampleButton(
+ label: 'Reset Properties',
+ onPressed: () async {
+ setState(() {
+ _fillOpacity = 0.6;
+ _fillColor = '#E74C3C';
+ _fillOutlineColor = '#C0392B';
+ });
+ await _updateFillProperties();
+ },
+ ),
+ ],
+ ),
+ ];
+ }
+
+ List _buildLineControls() {
+ return [
+ ControlGroup(
+ title: 'Line Appearance',
+ children: [
+ ListTile(
+ title: Text('Width: ${_lineWidth.toStringAsFixed(1)}'),
+ subtitle: Slider(
+ value: _lineWidth,
+ min: 1.0,
+ max: 20.0,
+ divisions: 38,
+ onChanged: (value) async {
+ setState(() => _lineWidth = value);
+ await _updateLineProperties();
+ },
+ ),
+ ),
+ ListTile(
+ title: Text('Opacity: ${(_lineOpacity * 100).toStringAsFixed(0)}%'),
+ subtitle: Slider(
+ value: _lineOpacity,
+ min: 0.0,
+ max: 1.0,
+ divisions: 20,
+ onChanged: (value) async {
+ setState(() => _lineOpacity = value);
+ await _updateLineProperties();
+ },
+ ),
+ ),
+ ListTile(
+ title: Text('Blur: ${_lineBlur.toStringAsFixed(1)}'),
+ subtitle: Slider(
+ value: _lineBlur,
+ min: 0.0,
+ max: 10.0,
+ divisions: 20,
+ onChanged: (value) async {
+ setState(() => _lineBlur = value);
+ await _updateLineProperties();
+ },
+ ),
+ ),
+ ListTile(
+ title: const Text('Color'),
+ trailing: Container(
+ width: 48,
+ height: 48,
+ decoration: BoxDecoration(
+ color: Color(
+ int.parse(_lineColor.substring(1), radix: 16) + 0xFF000000),
+ border: Border.all(color: Colors.grey),
+ borderRadius: BorderRadius.circular(4),
+ ),
+ ),
+ onTap: () => _pickColor('line'),
+ ),
+ ],
+ ),
+ ControlGroup(
+ title: 'Actions',
+ children: [
+ ExampleButton(
+ label: 'Reset Properties',
+ onPressed: () async {
+ setState(() {
+ _lineWidth = 4.0;
+ _lineOpacity = 0.9;
+ _lineColor = '#2ECC71';
+ _lineBlur = 0.0;
+ });
+ await _updateLineProperties();
+ },
+ ),
+ ],
+ ),
+ ];
+ }
+
+ Future _pickColor(String type) async {
+ String? currentHexColor;
+ switch (type) {
+ case 'circle':
+ currentHexColor = _circleColor;
+ case 'fill':
+ currentHexColor = _fillColor;
+ case 'fillOutline':
+ currentHexColor = _fillOutlineColor;
+ case 'line':
+ currentHexColor = _lineColor;
+ }
+
+ final selectedColor = await ColorPickerModal.showForHex(
+ context: context,
+ title: 'Select Color',
+ currentHexColor: currentHexColor,
+ );
+
+ if (selectedColor != null) {
+ switch (type) {
+ case 'circle':
+ setState(() => _circleColor = selectedColor);
+ await _updateCircleProperties();
+ case 'fill':
+ setState(() => _fillColor = selectedColor);
+ await _updateFillProperties();
+ case 'fillOutline':
+ setState(() => _fillOutlineColor = selectedColor);
+ await _updateFillProperties();
+ case 'line':
+ setState(() => _lineColor = selectedColor);
+ await _updateLineProperties();
+ }
+ }
+ }
+
+ @override
+ void dispose() {
+ _controller?.dispose();
+ super.dispose();
+ }
+}
diff --git a/maplibre_gl_example/lib/examples/annotations/annotations_example.dart b/maplibre_gl_example/lib/examples/annotations/annotations_example.dart
new file mode 100644
index 000000000..c58c8db5d
--- /dev/null
+++ b/maplibre_gl_example/lib/examples/annotations/annotations_example.dart
@@ -0,0 +1,545 @@
+import 'dart:math';
+import 'package:flutter/material.dart';
+import 'package:maplibre_gl/maplibre_gl.dart';
+import 'package:maplibre_gl_example/util.dart';
+import '../../page.dart';
+import '../../shared/shared.dart';
+
+/// Unified example for all annotation types (symbols, circles, fills, lines)
+class AnnotationsExample extends ExamplePage {
+ const AnnotationsExample({super.key})
+ : super(
+ const Icon(Icons.place),
+ 'Annotations',
+ category: ExampleCategory.annotations,
+ );
+
+ @override
+ Widget build(BuildContext context) => const _AnnotationsBody();
+}
+
+enum AnnotationType { symbol, circle, fill, line }
+
+class _AnnotationsBody extends StatefulWidget {
+ const _AnnotationsBody();
+
+ @override
+ State<_AnnotationsBody> createState() => _AnnotationsBodyState();
+}
+
+class _AnnotationsBodyState extends State<_AnnotationsBody> {
+ MapLibreMapController? _controller;
+ AnnotationType _currentType = AnnotationType.symbol;
+
+ final Map _symbols = {};
+ final Map _circles = {};
+ final Map _fills = {};
+ final Map _lines = {};
+
+ int _counter = 0;
+ String? _lastTappedAnnotation;
+
+ void _onMapCreated(MapLibreMapController controller) {
+ controller.onSymbolTapped.add(_onSymbolTapped);
+ controller.onCircleTapped.add(_onCircleTapped);
+ controller.onFillTapped.add(_onFillTapped);
+ controller.onLineTapped.add(_onLineTapped);
+
+ setState(() => _controller = controller);
+ }
+
+ Future _onStyleLoaded() async {
+ await addImageFromAsset(
+ _controller!,
+ "custom-marker",
+ "assets/symbols/custom-marker.png",
+ );
+ }
+
+ void _onSymbolTapped(Symbol symbol) {
+ final symbolId = _symbols.entries
+ .firstWhere((entry) => entry.value == symbol,
+ orElse: () => MapEntry('', symbol))
+ .key;
+ setState(() => _lastTappedAnnotation = 'Selected: $symbolId');
+ }
+
+ void _onCircleTapped(Circle circle) {
+ final circleId = _circles.entries
+ .firstWhere((entry) => entry.value == circle,
+ orElse: () => MapEntry('', circle))
+ .key;
+ setState(() => _lastTappedAnnotation = 'Selected: $circleId');
+ }
+
+ void _onFillTapped(Fill fill) {
+ final fillId = _fills.entries
+ .firstWhere((entry) => entry.value == fill,
+ orElse: () => MapEntry('', fill))
+ .key;
+ setState(() => _lastTappedAnnotation = 'Fill: $fillId');
+ }
+
+ void _onLineTapped(Line line) {
+ final lineId = _lines.entries
+ .firstWhere((entry) => entry.value == line,
+ orElse: () => MapEntry('', line))
+ .key;
+ setState(() => _lastTappedAnnotation = 'Line: $lineId');
+ }
+
+ Future _addAnnotation() async {
+ if (_controller == null) return;
+
+ final random = Random();
+ const center = ExampleConstants.sydneyCenter;
+
+ // Use different spread patterns for different annotation types
+ // to prevent overlapping and ensure visibility
+ double latOffset;
+ double lngOffset;
+ // Base Y offset for vertical separation
+ double baseLatOffset;
+
+ switch (_currentType) {
+ case AnnotationType.symbol:
+ baseLatOffset = 0.25;
+ latOffset = baseLatOffset + (random.nextDouble() - 0.5) * 0.06;
+ lngOffset = (random.nextDouble() - 0.5) * 0.25;
+ case AnnotationType.circle:
+ baseLatOffset = 0.08;
+ latOffset = baseLatOffset + (random.nextDouble() - 0.5) * 0.06;
+ lngOffset = (random.nextDouble() - 0.5) * 0.3;
+ case AnnotationType.fill:
+ baseLatOffset = -0.08;
+ latOffset = baseLatOffset + (random.nextDouble() - 0.5) * 0.06;
+ lngOffset = (random.nextDouble() - 0.5) * 0.35;
+ case AnnotationType.line:
+ baseLatOffset = -0.25;
+ latOffset = baseLatOffset + (random.nextDouble() - 0.5) * 0.06;
+ lngOffset = (random.nextDouble() - 0.5) * 0.35;
+ }
+
+ final lat = center.latitude + latOffset;
+ final lng = center.longitude + lngOffset;
+ final position = LatLng(lat, lng);
+
+ _counter++;
+
+ switch (_currentType) {
+ case AnnotationType.symbol:
+ final symbol = await _controller!.addSymbol(
+ SymbolOptions(
+ geometry: position,
+ iconImage: 'custom-marker',
+ iconSize: 2.0,
+ textField: 'Symbol $_counter',
+ textOffset: const Offset(0, 2),
+ ),
+ );
+ setState(() => _symbols['symbol_$_counter'] = symbol);
+ await _controller?.setSymbolIconAllowOverlap(true);
+ await _controller?.setSymbolTextAllowOverlap(true);
+
+ case AnnotationType.circle:
+ final circle = await _controller!.addCircle(
+ CircleOptions(
+ geometry: position,
+ circleRadius: 18.0,
+ circleColor: _randomColor(),
+ circleOpacity: 0.7,
+ ),
+ );
+ setState(() => _circles['circle_$_counter'] = circle);
+
+ case AnnotationType.fill:
+ final fill = await _controller!.addFill(
+ FillOptions(
+ geometry: _generatePolygon(position),
+ fillColor: _randomColor(),
+ fillOpacity: 0.6,
+ fillOutlineColor: '#000000',
+ ),
+ );
+ setState(() => _fills['fill_$_counter'] = fill);
+
+ case AnnotationType.line:
+ final line = await _controller!.addLine(
+ LineOptions(
+ geometry: _generateLineString(position),
+ lineColor: _randomColor(),
+ lineWidth: 8.0,
+ lineOpacity: 0.9,
+ ),
+ );
+ setState(() => _lines['line_$_counter'] = line);
+ }
+ }
+
+ Future _clearAnnotations() async {
+ if (_controller == null) return;
+
+ switch (_currentType) {
+ case AnnotationType.symbol:
+ if (_symbols.isNotEmpty) {
+ await _controller!.removeSymbols(_symbols.values.toList());
+ setState(() => _symbols.clear());
+ }
+ case AnnotationType.circle:
+ if (_circles.isNotEmpty) {
+ await _controller!.removeCircles(_circles.values.toList());
+ setState(() => _circles.clear());
+ }
+ case AnnotationType.fill:
+ if (_fills.isNotEmpty) {
+ await _controller!.removeFills(_fills.values.toList());
+ setState(() => _fills.clear());
+ }
+ case AnnotationType.line:
+ if (_lines.isNotEmpty) {
+ await _controller!.removeLines(_lines.values.toList());
+ setState(() => _lines.clear());
+ }
+ }
+ }
+
+ Future _batchAdd({int count = 5}) async {
+ if (_controller == null) return;
+
+ final random = Random();
+ const center = ExampleConstants.sydneyCenter;
+
+ for (var i = 0; i < count; i++) {
+ double latOffset;
+ double lngOffset;
+ double baseLatOffset;
+
+ switch (_currentType) {
+ case AnnotationType.symbol:
+ baseLatOffset = 0.25;
+ latOffset = baseLatOffset + (random.nextDouble() - 0.5) * 0.06;
+ lngOffset = (random.nextDouble() - 0.5) * 0.25;
+ case AnnotationType.circle:
+ baseLatOffset = 0.08;
+ latOffset = baseLatOffset + (random.nextDouble() - 0.5) * 0.06;
+ lngOffset = (random.nextDouble() - 0.5) * 0.3;
+ case AnnotationType.fill:
+ baseLatOffset = -0.08;
+ latOffset = baseLatOffset + (random.nextDouble() - 0.5) * 0.06;
+ lngOffset = (random.nextDouble() - 0.5) * 0.35;
+ case AnnotationType.line:
+ baseLatOffset = -0.25;
+ latOffset = baseLatOffset + (random.nextDouble() - 0.5) * 0.06;
+ lngOffset = (random.nextDouble() - 0.5) * 0.35;
+ }
+
+ final lat = center.latitude + latOffset;
+ final lng = center.longitude + lngOffset;
+ final position = LatLng(lat, lng);
+
+ _counter++;
+
+ switch (_currentType) {
+ case AnnotationType.symbol:
+ final symbol = await _controller!.addSymbol(
+ SymbolOptions(
+ geometry: position,
+ iconImage: 'custom-marker',
+ iconSize: 2.0,
+ textField: 'Symbol $_counter',
+ textOffset: const Offset(0, 2),
+ ),
+ );
+ _symbols['symbol_$_counter'] = symbol;
+
+ case AnnotationType.circle:
+ final circle = await _controller!.addCircle(
+ CircleOptions(
+ geometry: position,
+ circleRadius: 18.0,
+ circleColor: _randomColor(),
+ circleOpacity: 0.7,
+ ),
+ );
+ _circles['circle_$_counter'] = circle;
+
+ case AnnotationType.fill:
+ final fill = await _controller!.addFill(
+ FillOptions(
+ geometry: _generatePolygon(position),
+ fillColor: _randomColor(),
+ fillOpacity: 0.6,
+ fillOutlineColor: '#000000',
+ ),
+ );
+ _fills['fill_$_counter'] = fill;
+
+ case AnnotationType.line:
+ final line = await _controller!.addLine(
+ LineOptions(
+ geometry: _generateLineString(position),
+ lineColor: _randomColor(),
+ lineWidth: 8.0,
+ lineOpacity: 0.9,
+ ),
+ );
+ _lines['line_$_counter'] = line;
+ }
+ }
+
+ if (_currentType == AnnotationType.symbol) {
+ await _controller?.setSymbolIconAllowOverlap(true);
+ await _controller?.setSymbolTextAllowOverlap(true);
+ }
+
+ setState(() {});
+ }
+
+ Future _batchRemove({int count = 5}) async {
+ if (_controller == null) return;
+
+ switch (_currentType) {
+ case AnnotationType.symbol:
+ if (_symbols.isEmpty) return;
+ final toRemove =
+ _symbols.values.take(count.clamp(0, _symbols.length)).toList();
+ final keysToRemove = _symbols.entries
+ .where((e) => toRemove.contains(e.value))
+ .map((e) => e.key)
+ .toList();
+ await _controller!.removeSymbols(toRemove);
+ setState(() {
+ keysToRemove.forEach(_symbols.remove);
+ });
+
+ case AnnotationType.circle:
+ if (_circles.isEmpty) return;
+ final toRemove =
+ _circles.values.take(count.clamp(0, _circles.length)).toList();
+ final keysToRemove = _circles.entries
+ .where((e) => toRemove.contains(e.value))
+ .map((e) => e.key)
+ .toList();
+ await _controller!.removeCircles(toRemove);
+ setState(() {
+ keysToRemove.forEach(_circles.remove);
+ });
+
+ case AnnotationType.fill:
+ if (_fills.isEmpty) return;
+ final toRemove =
+ _fills.values.take(count.clamp(0, _fills.length)).toList();
+ final keysToRemove = _fills.entries
+ .where((e) => toRemove.contains(e.value))
+ .map((e) => e.key)
+ .toList();
+ await _controller!.removeFills(toRemove);
+ setState(() {
+ keysToRemove.forEach(_fills.remove);
+ });
+
+ case AnnotationType.line:
+ if (_lines.isEmpty) return;
+ final toRemove =
+ _lines.values.take(count.clamp(0, _lines.length)).toList();
+ final keysToRemove = _lines.entries
+ .where((e) => toRemove.contains(e.value))
+ .map((e) => e.key)
+ .toList();
+ await _controller!.removeLines(toRemove);
+ setState(() {
+ keysToRemove.forEach(_lines.remove);
+ });
+ }
+ }
+
+ String _randomColor() {
+ final colors = [
+ '#FF6B6B', // Red
+ '#4ECDC4', // Teal
+ '#45B7D1', // Blue
+ '#FFA07A', // Orange
+ '#98D8C8', // Green
+ '#F7DC6F', // Yellow
+ '#BB8FCE', // Purple
+ ];
+ return colors[Random().nextInt(colors.length)];
+ }
+
+ List> _generatePolygon(LatLng center) {
+ // Make polygon larger and more visible
+ const offset = 0.06;
+ return [
+ [
+ LatLng(center.latitude + offset, center.longitude - offset),
+ LatLng(center.latitude + offset, center.longitude + offset),
+ LatLng(center.latitude - offset, center.longitude + offset),
+ LatLng(center.latitude - offset, center.longitude - offset),
+ LatLng(center.latitude + offset, center.longitude - offset),
+ ]
+ ];
+ }
+
+ List _generateLineString(LatLng center) {
+ // Make line longer and more visible with more points for a curved appearance
+ const offset = 0.08;
+ return [
+ LatLng(center.latitude - offset, center.longitude - offset),
+ LatLng(center.latitude - offset * 0.3, center.longitude),
+ LatLng(center.latitude + offset * 0.3, center.longitude + offset * 0.5),
+ LatLng(center.latitude + offset, center.longitude + offset),
+ ];
+ }
+
+ int _getCurrentCount() {
+ switch (_currentType) {
+ case AnnotationType.symbol:
+ return _symbols.length;
+ case AnnotationType.circle:
+ return _circles.length;
+ case AnnotationType.fill:
+ return _fills.length;
+ case AnnotationType.line:
+ return _lines.length;
+ }
+ }
+
+ int _getTotalCount() {
+ return _symbols.length + _circles.length + _fills.length + _lines.length;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final hasController = _controller != null;
+ final count = _getCurrentCount();
+ final totalCount = _getTotalCount();
+
+ return MapExampleScaffold(
+ map: MapLibreMap(
+ styleString: ExampleConstants.demoMapStyle,
+ onMapCreated: _onMapCreated,
+ onStyleLoadedCallback: _onStyleLoaded,
+ initialCameraPosition: const CameraPosition(
+ target: ExampleConstants.sydneyCenter,
+ zoom: 8,
+ ),
+ trackCameraPosition: true,
+ ),
+ controls: [
+ InfoCard(
+ title: _lastTappedAnnotation ??
+ '${_currentType.name.capitalize()}s on Map',
+ subtitle:
+ '$count annotation${count == 1 ? "" : "s"}.${_lastTappedAnnotation == null ? " Tap an annotation to select." : ""}',
+ icon: _lastTappedAnnotation != null
+ ? Icons.touch_app
+ : Icons.info_outline,
+ ),
+
+ // Type Selector
+ Card(
+ child: Padding(
+ padding: const EdgeInsets.all(12),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ 'Annotation Type',
+ style: Theme.of(context).textTheme.titleSmall?.copyWith(
+ fontWeight: FontWeight.w600,
+ color: Theme.of(context).colorScheme.primary,
+ ),
+ ),
+ const SizedBox(height: 8),
+ ExampleSegmentedButton(
+ segments: const [
+ ExampleSegment(
+ value: AnnotationType.symbol,
+ label: 'Symbol',
+ icon: Icons.place,
+ ),
+ ExampleSegment(
+ value: AnnotationType.circle,
+ label: 'Circle',
+ icon: Icons.circle,
+ ),
+ ExampleSegment(
+ value: AnnotationType.fill,
+ label: 'Fill',
+ icon: Icons.square,
+ ),
+ ExampleSegment(
+ value: AnnotationType.line,
+ label: 'Line',
+ icon: Icons.show_chart,
+ ),
+ ],
+ selected: _currentType,
+ onSelectionChanged: (type) {
+ setState(() => _currentType = type);
+ },
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ ControlGroup(
+ title: 'Actions',
+ children: [
+ ExampleButton(
+ label: 'Add ${_currentType.name.capitalize()}',
+ icon: Icons.add,
+ onPressed: hasController ? _addAnnotation : null,
+ ),
+ ExampleButton(
+ label: 'Clear ${_currentType.name.capitalize()}s',
+ icon: Icons.delete_sweep,
+ onPressed: hasController && count > 0 ? _clearAnnotations : null,
+ style: ExampleButtonStyle.destructive,
+ ),
+ ],
+ ),
+
+ ControlGroup(
+ title: 'Batch Actions',
+ children: [
+ ExampleButton(
+ label: 'Add 10',
+ icon: Icons.add_box,
+ onPressed: hasController ? () => _batchAdd(count: 10) : null,
+ ),
+ ExampleButton(
+ label: 'Remove 10',
+ icon: Icons.remove_circle_outline,
+ onPressed: hasController && count >= 10
+ ? () => _batchRemove(count: 10)
+ : null,
+ style: ExampleButtonStyle.outlined,
+ ),
+ ExampleButton(
+ label: 'Clear all annotations',
+ icon: Icons.clear,
+ onPressed: hasController && totalCount > 0
+ ? () async {
+ _lastTappedAnnotation = null;
+ await _controller!.clearSymbols();
+ await _controller!.clearCircles();
+ await _controller!.clearFills();
+ await _controller!.clearLines();
+ setState(() {
+ _symbols.clear();
+ _circles.clear();
+ _fills.clear();
+ _lines.clear();
+ });
+ }
+ : null,
+ style: ExampleButtonStyle.destructive,
+ ),
+ ],
+ ),
+ ],
+ );
+ }
+}
diff --git a/maplibre_gl_example/lib/custom_marker.dart b/maplibre_gl_example/lib/examples/annotations/custom_marker.dart
similarity index 98%
rename from maplibre_gl_example/lib/custom_marker.dart
rename to maplibre_gl_example/lib/examples/annotations/custom_marker.dart
index ecbaf2fdc..2160bd010 100644
--- a/maplibre_gl_example/lib/custom_marker.dart
+++ b/maplibre_gl_example/lib/examples/annotations/custom_marker.dart
@@ -4,13 +4,14 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
-import 'page.dart';
+import '../../page.dart';
const randomMarkerNum = 100;
class CustomMarkerPage extends ExamplePage {
const CustomMarkerPage({super.key})
- : super(const Icon(Icons.place), 'Custom marker');
+ : super(const Icon(Icons.place), 'Custom marker',
+ category: ExampleCategory.annotations);
@override
Widget build(BuildContext context) {
diff --git a/maplibre_gl_example/lib/examples/basics/full_map_example.dart b/maplibre_gl_example/lib/examples/basics/full_map_example.dart
new file mode 100644
index 000000000..e9a9dc09e
--- /dev/null
+++ b/maplibre_gl_example/lib/examples/basics/full_map_example.dart
@@ -0,0 +1,86 @@
+import 'dart:async';
+import 'package:flutter/material.dart';
+import 'package:maplibre_gl/maplibre_gl.dart';
+import '../../page.dart';
+import '../../shared/shared.dart';
+
+/// Example demonstrating basic full-screen map
+class FullMapExample extends ExamplePage {
+ const FullMapExample({super.key})
+ : super(
+ const Icon(Icons.map),
+ 'Full screen map',
+ category: ExampleCategory.basics,
+ );
+
+ @override
+ Widget build(BuildContext context) => const _FullMapBody();
+}
+
+class _FullMapBody extends StatefulWidget {
+ const _FullMapBody();
+
+ @override
+ State<_FullMapBody> createState() => _FullMapBodyState();
+}
+
+class _FullMapBodyState extends State<_FullMapBody> {
+ MapLibreMapController? _mapController;
+ bool _canInteractWithMap = false;
+ bool canReset = false;
+
+ void _onMapCreated(MapLibreMapController controller) {
+ _mapController = controller;
+ setState(() => _canInteractWithMap = true);
+ }
+
+ Future _moveCameraToSanFrancisco() async {
+ await _mapController?.animateCamera(
+ CameraUpdate.newCameraPosition(
+ ExampleConstants.sanFranciscoCameraPosition,
+ ),
+ );
+ setState(() => canReset = true);
+ }
+
+ Future _resetCamera() async {
+ await _mapController?.animateCamera(
+ CameraUpdate.newCameraPosition(
+ ExampleConstants.defaultCameraPosition,
+ ),
+ );
+ setState(() => canReset = false);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ body: MapLibreMap(
+ styleString: ExampleConstants.demoMapStyle,
+ onMapCreated: _onMapCreated,
+ initialCameraPosition: ExampleConstants.defaultCameraPosition,
+ logoEnabled: true,
+ trackCameraPosition: true,
+ compassEnabled: true,
+ myLocationEnabled: true,
+ ),
+ floatingActionButton: ExampleButton(
+ label: canReset ? 'Reset camera' : 'Go to San Francisco',
+ icon: canReset ? Icons.refresh : Icons.flight_takeoff,
+ onPressed: _canInteractWithMap
+ ? canReset
+ ? _resetCamera
+ : _moveCameraToSanFrancisco
+ : null,
+ style: ExampleButtonStyle.tonal,
+ ),
+ floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
+ );
+ }
+
+ @override
+ void dispose() {
+ _mapController?.dispose();
+ super.dispose();
+ }
+}
diff --git a/maplibre_gl_example/lib/examples/basics/get_map_state.dart b/maplibre_gl_example/lib/examples/basics/get_map_state.dart
new file mode 100644
index 000000000..6b3bf51ce
--- /dev/null
+++ b/maplibre_gl_example/lib/examples/basics/get_map_state.dart
@@ -0,0 +1,164 @@
+import 'package:flutter/material.dart';
+import 'package:maplibre_gl/maplibre_gl.dart';
+
+import '../../page.dart';
+import '../../shared/shared.dart';
+
+class GetMapInfoPage extends ExamplePage {
+ const GetMapInfoPage({super.key})
+ : super(const Icon(Icons.info), 'Get map state',
+ category: ExampleCategory.basics);
+
+ @override
+ Widget build(BuildContext context) => const _GetMapInfoBody();
+}
+
+class _GetMapInfoBody extends StatefulWidget {
+ const _GetMapInfoBody();
+
+ @override
+ State<_GetMapInfoBody> createState() => _GetMapInfoBodyState();
+}
+
+class _GetMapInfoBodyState extends State<_GetMapInfoBody> {
+ MapLibreMapController? _controller;
+ String _displayData = '';
+ bool _isLoading = false;
+
+ void _onMapCreated(MapLibreMapController controller) {
+ setState(() => _controller = controller);
+ }
+
+ Future _displaySources() async {
+ if (_controller == null) return;
+
+ setState(() => _isLoading = true);
+
+ try {
+ final sources = await _controller!.getSourceIds();
+ setState(() {
+ _displayData = 'Sources:\n${sources.map((e) => '• $e').join('\n')}';
+ _isLoading = false;
+ });
+ } catch (e) {
+ setState(() {
+ _displayData = 'Error: $e';
+ _isLoading = false;
+ });
+ }
+ }
+
+ Future _displayLayers() async {
+ if (_controller == null) return;
+
+ setState(() => _isLoading = true);
+
+ try {
+ final layers = (await _controller!.getLayerIds()).cast();
+ setState(() {
+ _displayData = 'Layers:\n${layers.map((e) => '• $e').join('\n')}';
+ _isLoading = false;
+ });
+ } catch (e) {
+ setState(() {
+ _displayData = 'Error: $e';
+ _isLoading = false;
+ });
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final hasController = _controller != null;
+
+ return MapExampleScaffold(
+ map: MapLibreMap(
+ onMapCreated: _onMapCreated,
+ styleString: ExampleConstants.demoMapStyle,
+ initialCameraPosition: ExampleConstants.defaultCameraPosition,
+ ),
+ controls: [
+ if (!hasController)
+ const InfoCard(
+ title: 'Waiting for map',
+ subtitle: 'Map is initializing...',
+ icon: Icons.hourglass_empty,
+ ),
+ if (hasController) ...[
+ const SizedBox(height: 8),
+ ControlGroup(
+ title: 'Map Information',
+ vertical: false,
+ children: [
+ ExampleButton(
+ label: 'Get Layers',
+ onPressed: hasController ? _displayLayers : null,
+ icon: Icons.layers,
+ style: ExampleButtonStyle.filled,
+ ),
+ ExampleButton(
+ label: 'Get Sources',
+ onPressed: hasController ? _displaySources : null,
+ icon: Icons.source,
+ style: ExampleButtonStyle.filled,
+ ),
+ ],
+ ),
+ if (_isLoading)
+ const Card(
+ child: Padding(
+ padding: EdgeInsets.all(24.0),
+ child: Center(
+ child: CircularProgressIndicator(),
+ ),
+ ),
+ )
+ else if (_displayData.isNotEmpty)
+ Card(
+ child: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Icon(
+ Icons.info_outline,
+ color: Theme.of(context).colorScheme.primary,
+ ),
+ const SizedBox(width: 8),
+ Text(
+ 'Results',
+ style: Theme.of(context)
+ .textTheme
+ .titleMedium
+ ?.copyWith(fontWeight: FontWeight.bold),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ Container(
+ width: double.infinity,
+ padding: const EdgeInsets.all(12.0),
+ decoration: BoxDecoration(
+ color: Theme.of(context)
+ .colorScheme
+ .surfaceContainerHighest,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: SelectableText(
+ _displayData,
+ style: Theme.of(context).textTheme.bodySmall?.copyWith(
+ fontFamily: 'monospace',
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ],
+ );
+ }
+}
diff --git a/maplibre_gl_example/lib/examples/basics/gps_location_page.dart b/maplibre_gl_example/lib/examples/basics/gps_location_page.dart
new file mode 100644
index 000000000..c19860b73
--- /dev/null
+++ b/maplibre_gl_example/lib/examples/basics/gps_location_page.dart
@@ -0,0 +1,255 @@
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:location/location.dart';
+import 'package:maplibre_gl/maplibre_gl.dart';
+
+import '../../page.dart';
+import '../../shared/shared.dart';
+
+/// Example demonstrating GPS location tracking
+class GpsLocationPage extends ExamplePage {
+ const GpsLocationPage({super.key})
+ : super(
+ const Icon(Icons.gps_fixed),
+ 'GPS Location',
+ needsLocationPermission: false,
+ category: ExampleCategory.basics,
+ );
+
+ @override
+ Widget build(BuildContext context) => const _GpsLocationBody();
+}
+
+class _GpsLocationBody extends StatefulWidget {
+ const _GpsLocationBody();
+
+ @override
+ State<_GpsLocationBody> createState() => _GpsLocationBodyState();
+}
+
+class _GpsLocationBodyState extends State<_GpsLocationBody> {
+ MapLibreMapController? _controller;
+ LocationData? _currentLocation;
+ bool _isLoadingLocation = false;
+ bool _useHighAccuracy = false;
+ PermissionStatus? _permissionStatus;
+ MyLocationTrackingMode _trackingMode = MyLocationTrackingMode.none;
+ final Location _location = Location();
+
+ @override
+ void initState() {
+ super.initState();
+ unawaited(_checkPermission());
+ }
+
+ Future _checkPermission() async {
+ final status = await _location.hasPermission();
+ if (mounted) {
+ setState(() => _permissionStatus = status);
+ }
+ }
+
+ void _onMapCreated(MapLibreMapController controller) {
+ setState(() => _controller = controller);
+ }
+
+ Future _requestPermission() async {
+ final status = await _location.requestPermission();
+ if (mounted) {
+ setState(() => _permissionStatus = status);
+ }
+ }
+
+ Future _toggleAccuracy() async {
+ setState(() => _useHighAccuracy = !_useHighAccuracy);
+ }
+
+ Future _cycleTrackingMode() async {
+ const modes = MyLocationTrackingMode.values;
+ final currentIndex = modes.indexOf(_trackingMode);
+ final nextMode = modes[(currentIndex + 1) % modes.length];
+
+ if (_controller != null) {
+ await _controller!.updateMyLocationTrackingMode(nextMode);
+ setState(() => _trackingMode = nextMode);
+ }
+ }
+
+ String _getTrackingModeLabel() {
+ switch (_trackingMode) {
+ case MyLocationTrackingMode.none:
+ return 'None';
+ case MyLocationTrackingMode.tracking:
+ return 'Tracking';
+ case MyLocationTrackingMode.trackingCompass:
+ return 'Tracking + Compass';
+ case MyLocationTrackingMode.trackingGps:
+ return 'Tracking GPS';
+ }
+ }
+
+ String _getTrackingModeDescription() {
+ switch (_trackingMode) {
+ case MyLocationTrackingMode.none:
+ return 'No tracking active';
+ case MyLocationTrackingMode.tracking:
+ return 'Follow user location';
+ case MyLocationTrackingMode.trackingCompass:
+ return 'Follow location and rotation';
+ case MyLocationTrackingMode.trackingGps:
+ return 'GPS-based tracking';
+ }
+ }
+
+ String _formatCoordinate(double? value, {int decimals = 6}) {
+ if (value == null) return '--';
+ return value.toStringAsFixed(decimals);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final hasController = _controller != null;
+ final hasPermission = _permissionStatus == PermissionStatus.granted;
+ final hasLocation = _currentLocation != null;
+
+ return MapExampleScaffold(
+ map: MapLibreMap(
+ styleString: ExampleConstants.demoMapStyle,
+ onMapCreated: _onMapCreated,
+ initialCameraPosition: const CameraPosition(
+ target: LatLng(37.3, -121.8),
+ zoom: 7,
+ ),
+ trackCameraPosition: true,
+ myLocationEnabled: hasPermission,
+ myLocationTrackingMode: _trackingMode,
+ locationEnginePlatforms: _useHighAccuracy
+ ? const LocationEnginePlatforms(
+ androidPlatform: LocationEngineAndroidProperties(
+ interval: 1000,
+ displacement: 1,
+ priority: LocationPriority.highAccuracy,
+ ),
+ )
+ : LocationEnginePlatforms.defaultPlatform,
+ ),
+ controls: [
+ InfoCard(
+ title: 'GPS Location Tracking',
+ subtitle: hasPermission
+ ? 'Track your device location on the map'
+ : 'Location permission required',
+ icon: hasPermission ? Icons.gps_fixed : Icons.gps_off,
+ color: hasPermission ? null : Theme.of(context).colorScheme.error,
+ ),
+ const SizedBox(height: 8),
+ if (!hasPermission)
+ ControlGroup(
+ title: 'Permission Required',
+ vertical: false,
+ children: [
+ ExampleButton(
+ label: 'Grant Permission',
+ onPressed: _requestPermission,
+ icon: Icons.lock_open,
+ style: ExampleButtonStyle.filled,
+ ),
+ ],
+ ),
+ if (hasPermission) ...[
+ ControlGroup(
+ title: 'Tracking Mode',
+ vertical: true,
+ children: [
+ ListTile(
+ title: Text(_getTrackingModeLabel()),
+ subtitle: Text(_getTrackingModeDescription()),
+ trailing: FilledButton.tonal(
+ onPressed: hasController ? _cycleTrackingMode : null,
+ child: const Text('Change'),
+ ),
+ contentPadding: EdgeInsets.zero,
+ ),
+ ],
+ ),
+ const SizedBox(height: 8),
+ ControlGroup(
+ title: 'Settings',
+ vertical: true,
+ children: [
+ SwitchListTile(
+ title: const Text('High Accuracy Mode'),
+ subtitle: Text(_useHighAccuracy
+ ? 'GPS with high accuracy (Android only)'
+ : 'Default location settings'),
+ value: _useHighAccuracy,
+ onChanged: (_) => _toggleAccuracy(),
+ contentPadding: EdgeInsets.zero,
+ ),
+ ],
+ ),
+ if (hasLocation) ...[
+ const SizedBox(height: 8),
+ ControlGroup(
+ title: 'Current Location',
+ vertical: true,
+ children: [
+ _buildLocationTile(
+ 'Latitude',
+ _formatCoordinate(_currentLocation?.latitude),
+ Icons.pin_drop,
+ ),
+ _buildLocationTile(
+ 'Longitude',
+ _formatCoordinate(_currentLocation?.longitude),
+ Icons.pin_drop,
+ ),
+ if (_currentLocation?.accuracy != null)
+ _buildLocationTile(
+ 'Accuracy',
+ '${_currentLocation!.accuracy!.toStringAsFixed(1)} m',
+ Icons.radar,
+ ),
+ if (_currentLocation?.altitude != null)
+ _buildLocationTile(
+ 'Altitude',
+ '${_currentLocation!.altitude!.toStringAsFixed(1)} m',
+ Icons.terrain,
+ ),
+ if (_currentLocation?.speed != null &&
+ _currentLocation!.speed! > 0)
+ _buildLocationTile(
+ 'Speed',
+ '${_currentLocation!.speed!.toStringAsFixed(1)} m/s',
+ Icons.speed,
+ ),
+ ],
+ ),
+ ],
+ ],
+ ],
+ );
+ }
+
+ Widget _buildLocationTile(String label, String value, IconData icon) {
+ return ListTile(
+ leading: Icon(icon, size: 20),
+ title: Text(label),
+ trailing: SelectableText(
+ value,
+ style: const TextStyle(
+ fontFamily: 'monospace',
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ contentPadding: EdgeInsets.zero,
+ );
+ }
+
+ @override
+ void dispose() {
+ _controller?.dispose();
+ super.dispose();
+ }
+}
diff --git a/maplibre_gl_example/lib/multi_style_switch.dart b/maplibre_gl_example/lib/examples/basics/multi_style_switch.dart
similarity index 96%
rename from maplibre_gl_example/lib/multi_style_switch.dart
rename to maplibre_gl_example/lib/examples/basics/multi_style_switch.dart
index 8ce9b3a9c..541de815d 100644
--- a/maplibre_gl_example/lib/multi_style_switch.dart
+++ b/maplibre_gl_example/lib/examples/basics/multi_style_switch.dart
@@ -3,7 +3,7 @@ import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
-import 'page.dart';
+import '../../page.dart';
/// A page that demonstrates switching between multiple map styles in MapLibre GL.
///
@@ -14,7 +14,8 @@ import 'page.dart';
/// The styles include a remote style, an embedded minimal style, and styles loaded from assets.
class MultiStyleSwitchPage extends ExamplePage {
const MultiStyleSwitchPage({super.key})
- : super(const Icon(Icons.style), 'Multi style switch');
+ : super(const Icon(Icons.style), 'Multi style switch',
+ category: ExampleCategory.basics);
@override
Widget build(BuildContext context) => const _MultiStyleSwitchBody();
@@ -111,9 +112,10 @@ class _MultiStyleSwitchBodyState extends State<_MultiStyleSwitchBody> {
onMapCreated: _onMapCreated,
onStyleLoadedCallback: _onStyleLoaded,
onCameraIdle: _onCameraIdle,
+ attributionButtonPosition: AttributionButtonPosition.topRight,
),
Positioned(
- top: 0,
+ bottom: 0,
left: 0,
right: 0,
child: SafeArea(
diff --git a/maplibre_gl_example/lib/examples/camera/camera_bounds_example.dart b/maplibre_gl_example/lib/examples/camera/camera_bounds_example.dart
new file mode 100644
index 000000000..401375c87
--- /dev/null
+++ b/maplibre_gl_example/lib/examples/camera/camera_bounds_example.dart
@@ -0,0 +1,194 @@
+import 'package:flutter/material.dart';
+import 'package:maplibre_gl/maplibre_gl.dart';
+import '../../page.dart';
+import '../../shared/shared.dart';
+
+/// Example demonstrating camera bounds and constraints
+class CameraBoundsExample extends ExamplePage {
+ const CameraBoundsExample({super.key})
+ : super(
+ const Icon(Icons.crop),
+ 'Camera Bounds & Constraints',
+ category: ExampleCategory.camera,
+ );
+
+ @override
+ Widget build(BuildContext context) => const _CameraBoundsBody();
+}
+
+class _CameraBoundsBody extends StatefulWidget {
+ const _CameraBoundsBody();
+
+ @override
+ State<_CameraBoundsBody> createState() => _CameraBoundsBodyState();
+}
+
+class _CameraBoundsBodyState extends State<_CameraBoundsBody> {
+ MapLibreMapController? _controller;
+ LatLngBounds? _currentBounds;
+ double? _minZoom;
+ double? _maxZoom;
+
+ // Predefined bounds
+ static final _sydneyBounds = LatLngBounds(
+ southwest: const LatLng(-34.022631, 150.620685),
+ northeast: const LatLng(-33.571835, 151.325952),
+ );
+
+ static final _sanFranciscoBounds = LatLngBounds(
+ southwest: const LatLng(37.7, -122.5),
+ northeast: const LatLng(37.8, -122.4),
+ );
+
+ static final _europeBounds = LatLngBounds(
+ southwest: const LatLng(36.0, -10.0),
+ northeast: const LatLng(71.0, 40.0),
+ );
+
+ Future _onMapCreated(MapLibreMapController controller) async {
+ _controller = controller;
+ await _updateInfo();
+ }
+
+ Future _updateInfo() async {
+ if (_controller == null) return;
+ final bounds = await _controller!.getVisibleRegion();
+ if (mounted) {
+ setState(() => _currentBounds = bounds);
+ }
+ }
+
+ Future _setBounds(LatLngBounds bounds, String name) async {
+ if (_controller == null) return;
+
+ await _controller!.animateCamera(
+ CameraUpdate.newLatLngBounds(
+ bounds,
+ left: 50,
+ top: 50,
+ right: 50,
+ bottom: 50,
+ ),
+ );
+
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text('Camera moved to $name bounds'),
+ duration: const Duration(seconds: 1),
+ ),
+ );
+ }
+
+ _updateInfo();
+ }
+
+ Future _setMinZoom(double zoom) async {
+ if (_controller == null) return;
+ // Note: min/max zoom can be set via style, initial confing or options.
+ setState(() => _minZoom = zoom);
+ }
+
+ Future _setMaxZoom(double zoom) async {
+ if (_controller == null) return;
+ setState(() => _maxZoom = zoom);
+ }
+
+ Future _clearZoomConstraints() async {
+ if (_controller == null) return;
+ setState(() {
+ _minZoom = null;
+ _maxZoom = null;
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final hasController = _controller != null;
+ final boundsInfo = _currentBounds != null
+ ? 'SW: ${_currentBounds!.southwest.latitude.toStringAsFixed(2)}, ${_currentBounds!.southwest.longitude.toStringAsFixed(2)}\n'
+ 'NE: ${_currentBounds!.northeast.latitude.toStringAsFixed(2)}, ${_currentBounds!.northeast.longitude.toStringAsFixed(2)}'
+ : 'Loading...';
+
+ return MapExampleScaffold(
+ map: MapLibreMap(
+ styleString: ExampleConstants.demoMapStyle,
+ onMapCreated: _onMapCreated,
+ onCameraIdle: _updateInfo,
+ trackCameraPosition: true,
+ initialCameraPosition: ExampleConstants.defaultCameraPosition,
+ minMaxZoomPreference: MinMaxZoomPreference(_minZoom, _maxZoom),
+ ),
+
+ controls: [
+ InfoCard(
+ title: 'Visible Bounds',
+ subtitle: boundsInfo,
+ icon: Icons.crop,
+ ),
+ if (_minZoom != null || _maxZoom != null)
+ InfoCard(
+ title: 'Zoom Constraints',
+ subtitle:
+ 'Min: ${_minZoom?.toStringAsFixed(0) ?? "None"} | Max: ${_maxZoom?.toStringAsFixed(0) ?? "None"}',
+ icon: Icons.lock,
+ color: Colors.orange.shade100,
+ ),
+ const SizedBox(height: 8),
+ ControlGroup(
+
+ title: 'Move to Bounds',
+ children: [
+ ExampleButton(
+ label: 'Sydney',
+ icon: Icons.map,
+ onPressed: hasController
+ ? () => _setBounds(_sydneyBounds, 'Sydney')
+ : null,
+ ),
+ ExampleButton(
+ label: 'San Francisco',
+ icon: Icons.map,
+ onPressed: hasController
+ ? () => _setBounds(_sanFranciscoBounds, 'San Francisco')
+ : null,
+ ),
+ ExampleButton(
+ label: 'Europe',
+ icon: Icons.map,
+ onPressed: hasController
+ ? () => _setBounds(_europeBounds, 'Europe')
+ : null,
+ ),
+ ],
+ ),
+ const SizedBox(height: 8),
+ ControlGroup(
+ title: 'Zoom Limits',
+ children: [
+ ExampleButton(
+ label: 'Min Zoom: 3',
+ icon: Icons.zoom_out,
+ onPressed: hasController ? () => _setMinZoom(3) : null,
+ style: ExampleButtonStyle.tonal,
+ ),
+ ExampleButton(
+ label: 'Max Zoom: 8',
+ icon: Icons.zoom_in,
+ onPressed: hasController ? () => _setMaxZoom(8) : null,
+ style: ExampleButtonStyle.tonal,
+ ),
+ ExampleButton(
+ label: 'Clear Limits',
+ icon: Icons.lock_open,
+ onPressed: hasController && (_minZoom != null || _maxZoom != null)
+ ? _clearZoomConstraints
+ : null,
+ style: ExampleButtonStyle.outlined,
+ ),
+ ],
+ ),
+ ],
+ );
+ }
+}
diff --git a/maplibre_gl_example/lib/examples/camera/camera_controls_example.dart b/maplibre_gl_example/lib/examples/camera/camera_controls_example.dart
new file mode 100644
index 000000000..67a4560c0
--- /dev/null
+++ b/maplibre_gl_example/lib/examples/camera/camera_controls_example.dart
@@ -0,0 +1,240 @@
+import 'package:flutter/material.dart';
+import 'package:maplibre_gl/maplibre_gl.dart';
+import '../../page.dart';
+import '../../shared/shared.dart';
+
+/// Comprehensive camera control example with animated and instant movements
+class CameraControlsExample extends ExamplePage {
+ const CameraControlsExample({super.key})
+ : super(
+ const Icon(Icons.videocam),
+ 'Camera Controls',
+ category: ExampleCategory.camera,
+ );
+
+ @override
+ Widget build(BuildContext context) => const _CameraControlsBody();
+}
+
+class _CameraControlsBody extends StatefulWidget {
+ const _CameraControlsBody();
+
+ @override
+ State<_CameraControlsBody> createState() => _CameraControlsBodyState();
+}
+
+class _CameraControlsBodyState extends State<_CameraControlsBody> {
+ MapLibreMapController? _controller;
+ bool _isAnimated = true;
+ CameraPosition? _currentPosition;
+
+ void _onMapCreated(MapLibreMapController controller) {
+ _controller = controller;
+ }
+
+ void _onCameraIdle() {
+ final position = _controller?.cameraPosition;
+ if (mounted && position != null) {
+ setState(() => _currentPosition = position);
+ }
+ }
+
+ Future _moveCamera(CameraUpdate update) async {
+ if (_controller == null) return;
+
+ if (_isAnimated) {
+ await _controller!.animateCamera(update,
+ duration: ExampleConstants.cameraAnimationDuration);
+ } else {
+ await _controller!.moveCamera(update);
+ }
+ }
+
+ Future _zoomIn() => _moveCamera(CameraUpdate.zoomIn());
+ Future _zoomOut() => _moveCamera(CameraUpdate.zoomOut());
+
+ Future _tiltUp() async {
+ final current = _currentPosition?.tilt ?? 0;
+ await _moveCamera(CameraUpdate.tiltTo(current + 15));
+ }
+
+ Future _tiltDown() async {
+ final current = _currentPosition?.tilt ?? 0;
+ await _moveCamera(CameraUpdate.tiltTo((current - 15).clamp(0, 60)));
+ }
+
+ Future _rotateLeft() async {
+ final current = _currentPosition?.bearing ?? 0;
+ await _moveCamera(CameraUpdate.bearingTo(current - 30));
+ }
+
+ Future _rotateRight() async {
+ final current = _currentPosition?.bearing ?? 0;
+ await _moveCamera(CameraUpdate.bearingTo(current + 30));
+ }
+
+ Future _resetCamera() => _moveCamera(
+ CameraUpdate.newCameraPosition(ExampleConstants.defaultCameraPosition),
+ );
+
+ Future _goToSydney() => _moveCamera(
+ CameraUpdate.newCameraPosition(ExampleConstants.sydneyCameraPosition),
+ );
+ Future _goToSanFrancisco() => _moveCamera(
+ CameraUpdate.newCameraPosition(
+ ExampleConstants.sanFranciscoCameraPosition),
+ );
+
+ Future _goToLondon() => _moveCamera(
+ CameraUpdate.newCameraPosition(
+ ExampleConstants.londonCameraPosition,
+ ),
+ );
+
+ @override
+ Widget build(BuildContext context) {
+ final hasController = _controller != null;
+ final zoom = _currentPosition?.zoom.toStringAsFixed(1) ?? '--';
+ final tilt = _currentPosition?.tilt.toStringAsFixed(0) ?? '--';
+ final bearing = _currentPosition?.bearing.toStringAsFixed(0) ?? '--';
+
+ return MapExampleScaffold(
+ map: MapLibreMap(
+ styleString: ExampleConstants.demoMapStyle,
+ onMapCreated: _onMapCreated,
+ onCameraIdle: _onCameraIdle,
+ initialCameraPosition: ExampleConstants.defaultCameraPosition,
+ trackCameraPosition: true,
+ myLocationEnabled: true,
+ compassEnabled: true,
+ ),
+ controls: [
+ // Animation Mode Toggle
+ Card(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Row(
+ children: [
+ Icon(
+ _isAnimated ? Icons.play_circle : Icons.skip_next,
+ color: Theme.of(context).colorScheme.primary,
+ ),
+ const SizedBox(width: 12),
+ Text(
+ 'Animation',
+ style: Theme.of(context).textTheme.titleMedium,
+ ),
+ ],
+ ),
+ Switch(
+ value: _isAnimated,
+ onChanged: (value) => setState(() => _isAnimated = value),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ // Camera Info
+ InfoCard(
+ title: 'Camera Position',
+ subtitle: 'Zoom: $zoom | Tilt: $tilt° | Bearing: $bearing°',
+ icon: Icons.info_outline,
+ ),
+
+ const SizedBox(height: 8),
+
+ // Zoom Controls
+ ControlGroup(
+ title: 'Zoom',
+ children: [
+ ExampleButton(
+ label: 'Zoom In',
+ icon: Icons.add,
+ onPressed: hasController ? _zoomIn : null,
+ ),
+ ExampleButton(
+ label: 'Zoom Out',
+ icon: Icons.remove,
+ onPressed: hasController ? _zoomOut : null,
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 8),
+
+ // Tilt Controls
+ ControlGroup(
+ title: 'Tilt (Pitch)',
+ children: [
+ ExampleButton(
+ label: 'Tilt Up',
+ icon: Icons.arrow_upward,
+ onPressed: hasController ? _tiltUp : null,
+ ),
+ ExampleButton(
+ label: 'Tilt Down',
+ icon: Icons.arrow_downward,
+ onPressed: hasController ? _tiltDown : null,
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 8),
+
+ // Rotation Controls
+ ControlGroup(
+ title: 'Rotation (Bearing)',
+ children: [
+ ExampleButton(
+ label: 'Rotate Left',
+ icon: Icons.rotate_left,
+ onPressed: hasController ? _rotateLeft : null,
+ ),
+ ExampleButton(
+ label: 'Rotate Right',
+ icon: Icons.rotate_right,
+ onPressed: hasController ? _rotateRight : null,
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 8),
+
+ // Location Presets
+ ControlGroup(
+ title: 'Go To Location',
+ children: [
+ ExampleButton(
+ label: 'Null Island',
+ icon: Icons.place,
+ onPressed: hasController ? _resetCamera : null,
+ style: ExampleButtonStyle.tonal,
+ ),
+ ExampleButton(
+ label: 'Sydney',
+ icon: Icons.place,
+ onPressed: hasController ? _goToSydney : null,
+ style: ExampleButtonStyle.tonal,
+ ),
+ ExampleButton(
+ label: 'San Francisco',
+ icon: Icons.place,
+ onPressed: hasController ? _goToSanFrancisco : null,
+ style: ExampleButtonStyle.tonal,
+ ),
+ ExampleButton(
+ label: 'London',
+ icon: Icons.place,
+ onPressed: hasController ? _goToLondon : null,
+ style: ExampleButtonStyle.tonal,
+ ),
+ ],
+ ),
+ ],
+ );
+ }
+}
diff --git a/maplibre_gl_example/lib/examples/interaction/map_controls_example.dart b/maplibre_gl_example/lib/examples/interaction/map_controls_example.dart
new file mode 100644
index 000000000..d92e4f04e
--- /dev/null
+++ b/maplibre_gl_example/lib/examples/interaction/map_controls_example.dart
@@ -0,0 +1,229 @@
+import 'package:flutter/material.dart';
+import 'package:maplibre_gl/maplibre_gl.dart';
+import '../../page.dart';
+import '../../shared/shared.dart';
+
+/// Example demonstrating map UI controls and settings
+class MapControlsExample extends ExamplePage {
+ const MapControlsExample({super.key})
+ : super(
+ const Icon(Icons.settings),
+ 'Map Controls',
+ category: ExampleCategory.interaction,
+ );
+
+ @override
+ Widget build(BuildContext context) => const _MapControlsBody();
+}
+
+class _MapControlsBody extends StatefulWidget {
+ const _MapControlsBody();
+
+ @override
+ State<_MapControlsBody> createState() => _MapControlsBodyState();
+}
+
+class _MapControlsBodyState extends State<_MapControlsBody> {
+ MapLibreMapController? _controller;
+
+ // UI Settings
+ bool _compassEnabled = true;
+ CompassViewPosition _compassPosition = CompassViewPosition.topRight;
+
+ bool _logoEnabled = true;
+ LogoViewPosition _logoPosition = LogoViewPosition.bottomLeft;
+
+ AttributionButtonPosition _attributionPosition =
+ AttributionButtonPosition.bottomRight;
+
+ // Current Map State
+ CameraPosition? _currentPosition;
+
+ void _onMapCreated(MapLibreMapController controller) {
+ _controller = controller;
+ controller.addListener(_onMapChanged);
+ }
+
+ void _onMapChanged() {
+ final position = _controller?.cameraPosition;
+ if (mounted && position != null) {
+ setState(() => _currentPosition = position);
+ }
+ }
+
+ Future _updateCompass(bool enabled) async {
+ setState(() => _compassEnabled = enabled);
+ }
+
+ void _cycleCompassPosition() {
+ const positions = CompassViewPosition.values;
+ final currentIndex = positions.indexOf(_compassPosition);
+ setState(() {
+ _compassPosition = positions[(currentIndex + 1) % positions.length];
+ });
+ }
+
+ Future _updateLogo(bool enabled) async {
+ setState(() => _logoEnabled = enabled);
+ }
+
+ void _cycleLogoPosition() {
+ const positions = LogoViewPosition.values;
+ final currentIndex = positions.indexOf(_logoPosition);
+ setState(() {
+ _logoPosition = positions[(currentIndex + 1) % positions.length];
+ });
+ }
+
+ void _cycleAttributionPosition() {
+ const positions = AttributionButtonPosition.values;
+ final currentIndex = positions.indexOf(_attributionPosition);
+ setState(() {
+ _attributionPosition = positions[(currentIndex + 1) % positions.length];
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final zoom = _currentPosition?.zoom.toStringAsFixed(1) ?? '--';
+ final lat = _currentPosition?.target.latitude.toStringAsFixed(4) ?? '--';
+ final lng = _currentPosition?.target.longitude.toStringAsFixed(4) ?? '--';
+
+ return MapExampleScaffold(
+ map: MapLibreMap(
+ styleString: ExampleConstants.demoMapStyle,
+ onMapCreated: _onMapCreated,
+ initialCameraPosition: ExampleConstants.defaultCameraPosition,
+ trackCameraPosition: true,
+ compassEnabled: _compassEnabled,
+ compassViewPosition: _compassPosition,
+ logoEnabled: _logoEnabled,
+ logoViewPosition: _logoPosition,
+ attributionButtonPosition: _attributionPosition,
+ ),
+ controls: [
+ InfoCard(
+ title: 'Camera Info',
+ subtitle: 'Zoom: $zoom\nLat: $lat, Lng: $lng',
+ icon: Icons.camera,
+ ),
+ _buildControlCard(
+ title: 'Logo',
+ icon: Icons.image,
+ enabled: _logoEnabled,
+ position: _logoPosition.name.capitalize(),
+ onToggle: _updateLogo,
+ onChangePosition: _cycleLogoPosition,
+ ),
+ _buildControlCard(
+ title: 'Attribution',
+ icon: Icons.info_outline,
+ enabled: true,
+ position: _attributionPosition.name.capitalize(),
+ onChangePosition: _cycleAttributionPosition,
+ showToggle: false,
+ ),
+ _buildControlCard(
+ title: 'Compass',
+ icon: Icons.explore,
+ enabled: _compassEnabled,
+ position: _compassPosition.name.capitalize(),
+ onToggle: _updateCompass,
+ onChangePosition: _cycleCompassPosition,
+ ),
+ ],
+ );
+ }
+
+ Widget _buildControlCard({
+ required String title,
+ required IconData icon,
+ required bool enabled,
+ required String position,
+ required VoidCallback onChangePosition,
+ void Function(bool)? onToggle,
+ bool showToggle = true,
+ }) {
+ return Card(
+ child: Padding(
+ padding: const EdgeInsets.all(12.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Row(
+ children: [
+ Icon(icon, size: 20),
+ const SizedBox(width: 8),
+ Expanded(
+ child: Text(
+ title,
+ style: const TextStyle(
+ fontWeight: FontWeight.bold,
+ fontSize: 14,
+ ),
+ ),
+ ),
+ if (showToggle && onToggle != null)
+ Switch(
+ value: enabled,
+ onChanged: onToggle,
+ materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
+ ),
+ ],
+ ),
+ const SizedBox(height: 4),
+ Row(
+ children: [
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ 'Position',
+ style: TextStyle(
+ fontSize: 12,
+ color: Colors.grey[600],
+ ),
+ ),
+ const SizedBox(height: 2),
+ Text(
+ position,
+ style: const TextStyle(
+ fontSize: 13,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ ],
+ ),
+ ),
+ FilledButton.tonal(
+ onPressed: (!showToggle || enabled) ? onChangePosition : null,
+ style: FilledButton.styleFrom(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 12,
+ vertical: 8,
+ ),
+ minimumSize: Size.zero,
+ tapTargetSize: MaterialTapTargetSize.shrinkWrap,
+ ),
+ child: const Text(
+ 'Change',
+ style: TextStyle(fontSize: 12),
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ @override
+ void dispose() {
+ _controller?.removeListener(_onMapChanged);
+ _controller?.dispose();
+ super.dispose();
+ }
+}
diff --git a/maplibre_gl_example/lib/examples/interaction/map_gestures_example.dart b/maplibre_gl_example/lib/examples/interaction/map_gestures_example.dart
new file mode 100644
index 000000000..0fea6997b
--- /dev/null
+++ b/maplibre_gl_example/lib/examples/interaction/map_gestures_example.dart
@@ -0,0 +1,205 @@
+import 'dart:math' as math;
+
+import 'package:flutter/material.dart';
+import 'package:maplibre_gl/maplibre_gl.dart';
+import '../../page.dart';
+import '../../shared/shared.dart';
+
+/// Example demonstrating gesture and interaction configuration
+class MapGesturesExample extends ExamplePage {
+ const MapGesturesExample({super.key})
+ : super(
+ const Icon(Icons.touch_app),
+ 'Map Gestures',
+ category: ExampleCategory.interaction,
+ );
+
+ @override
+ Widget build(BuildContext context) => const _MapGesturesBody();
+}
+
+class _MapGesturesBody extends StatefulWidget {
+ const _MapGesturesBody();
+
+ @override
+ State<_MapGesturesBody> createState() => _MapGesturesBodyState();
+}
+
+class _MapGesturesBodyState extends State<_MapGesturesBody> {
+ MapLibreMapController? _controller;
+
+ // Gesture Settings
+ bool _rotateGesturesEnabled = true;
+ bool _scrollGesturesEnabled = true;
+ bool _tiltGesturesEnabled = true;
+ bool _zoomGesturesEnabled = true;
+ bool _doubleClickToZoomEnabled = true;
+
+ // Movement State
+ bool _isMoving = false;
+ String _lastGesture = 'None';
+
+ void _onMapCreated(MapLibreMapController controller) {
+ _controller = controller;
+ controller.addListener(_onMapChanged);
+ }
+
+ void _onMapChanged() {
+ final moving = _controller?.isCameraMoving ?? false;
+ if (mounted && moving != _isMoving) {
+ setState(() {
+ _isMoving = moving;
+ if (moving) {
+ _lastGesture = 'Camera moving...';
+ }
+ });
+ }
+ }
+
+ void _onCameraIdle() {
+ if (mounted) {
+ setState(() => _lastGesture = 'Camera idle');
+ }
+ }
+
+ void _onMapClick(math.Point point, LatLng coordinates) {
+ setState(() => _lastGesture = 'Map clicked');
+ }
+
+ void _onMapLongClick(math.Point point, LatLng coordinates) {
+ setState(() => _lastGesture = 'Map long pressed');
+ }
+
+ Future _updateRotateGestures(bool enabled) async {
+ setState(() => _rotateGesturesEnabled = enabled);
+ }
+
+ Future _updateScrollGestures(bool enabled) async {
+ setState(() => _scrollGesturesEnabled = enabled);
+ }
+
+ Future _updateTiltGestures(bool enabled) async {
+ setState(() => _tiltGesturesEnabled = enabled);
+ }
+
+ Future _updateZoomGestures(bool enabled) async {
+ setState(() => _zoomGesturesEnabled = enabled);
+ }
+
+ Future _updateDoubleClickZoom(bool enabled) async {
+ setState(() => _doubleClickToZoomEnabled = enabled);
+ }
+
+ Future _enableAllGestures() async {
+ setState(() {
+ _rotateGesturesEnabled = true;
+ _scrollGesturesEnabled = true;
+ _tiltGesturesEnabled = true;
+ _zoomGesturesEnabled = true;
+ _doubleClickToZoomEnabled = true;
+ });
+ }
+
+ Future _disableAllGestures() async {
+ setState(() {
+ _rotateGesturesEnabled = false;
+ _scrollGesturesEnabled = false;
+ _tiltGesturesEnabled = false;
+ _zoomGesturesEnabled = false;
+ _doubleClickToZoomEnabled = false;
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return MapExampleScaffold(
+ map: MapLibreMap(
+ styleString: ExampleConstants.demoMapStyle,
+ onMapCreated: _onMapCreated,
+ onMapClick: _onMapClick,
+ onMapLongClick: _onMapLongClick,
+ onCameraIdle: _onCameraIdle,
+ initialCameraPosition: ExampleConstants.defaultCameraPosition,
+ trackCameraPosition: true,
+ rotateGesturesEnabled: _rotateGesturesEnabled,
+ scrollGesturesEnabled: _scrollGesturesEnabled,
+ tiltGesturesEnabled: _tiltGesturesEnabled,
+ zoomGesturesEnabled: _zoomGesturesEnabled,
+ doubleClickZoomEnabled: _doubleClickToZoomEnabled,
+ ),
+ controls: [
+ InfoCard(
+ title: 'Gesture Status',
+ subtitle: _lastGesture,
+ icon: _isMoving ? Icons.touch_app : Icons.check_circle,
+ color: _isMoving ? Colors.blue.shade100 : Colors.green.shade100,
+ ),
+ ControlGroup(
+ title: 'Quick Actions',
+ children: [
+ ExampleButton(
+ label: 'Enable All',
+ icon: Icons.check_circle,
+ onPressed: _enableAllGestures,
+ style: ExampleButtonStyle.filled,
+ ),
+ ExampleButton(
+ label: 'Disable All',
+ icon: Icons.block,
+ onPressed: _disableAllGestures,
+ style: ExampleButtonStyle.destructive,
+ ),
+ ],
+ ),
+ ControlGroup(
+ title: 'Gesture Settings',
+ vertical: false,
+ children: [
+ SwitchListTile(
+ title: const Text('Rotate Gestures'),
+ subtitle: const Text('Two-finger rotation'),
+ value: _rotateGesturesEnabled,
+ onChanged: _updateRotateGestures,
+ contentPadding: EdgeInsets.zero,
+ ),
+ SwitchListTile(
+ title: const Text('Scroll Gestures'),
+ subtitle: const Text('Pan to move map'),
+ value: _scrollGesturesEnabled,
+ onChanged: _updateScrollGestures,
+ contentPadding: EdgeInsets.zero,
+ ),
+ SwitchListTile(
+ title: const Text('Tilt Gestures'),
+ subtitle: const Text('Two-finger tilt/pitch'),
+ value: _tiltGesturesEnabled,
+ onChanged: _updateTiltGestures,
+ contentPadding: EdgeInsets.zero,
+ ),
+ SwitchListTile(
+ title: const Text('Zoom Gestures'),
+ subtitle: const Text('Pinch to zoom'),
+ value: _zoomGesturesEnabled,
+ onChanged: _updateZoomGestures,
+ contentPadding: EdgeInsets.zero,
+ ),
+ SwitchListTile(
+ title: const Text('Double-Click Zoom'),
+ subtitle: const Text('Double tap to zoom in'),
+ value: _doubleClickToZoomEnabled,
+ onChanged: _updateDoubleClickZoom,
+ contentPadding: EdgeInsets.zero,
+ ),
+ ],
+ ),
+ ],
+ );
+ }
+
+ @override
+ void dispose() {
+ _controller?.removeListener(_onMapChanged);
+ _controller?.dispose();
+ super.dispose();
+ }
+}
diff --git a/maplibre_gl_example/lib/examples/layers/circle_layer_example.dart b/maplibre_gl_example/lib/examples/layers/circle_layer_example.dart
new file mode 100644
index 000000000..102032e5c
--- /dev/null
+++ b/maplibre_gl_example/lib/examples/layers/circle_layer_example.dart
@@ -0,0 +1,377 @@
+import 'dart:math';
+
+import 'package:flutter/material.dart';
+import 'package:maplibre_gl/maplibre_gl.dart';
+import '../../page.dart';
+import '../../shared/shared.dart';
+
+/// Example demonstrating circle layer properties
+class CircleLayerExample extends ExamplePage {
+ const CircleLayerExample({super.key})
+ : super(
+ const Icon(Icons.circle_outlined),
+ 'Circle Layer',
+ category: ExampleCategory.layers,
+ );
+
+ @override
+ Widget build(BuildContext context) => const _CircleLayerBody();
+}
+
+class _CircleLayerBody extends StatefulWidget {
+ const _CircleLayerBody();
+
+ @override
+ State<_CircleLayerBody> createState() => _CircleLayerBodyState();
+}
+
+class _CircleLayerBodyState extends State<_CircleLayerBody> {
+ MapLibreMapController? _controller;
+
+ static const _sourceId = 'circle_source';
+ static const _layerId = 'circle_layer';
+
+ // Circle properties
+ double _circleRadius = 20.0;
+ double _circleOpacity = 0.8;
+ double _circleStrokeWidth = 2.0;
+ double _circleStrokeOpacity = 1.0;
+ Color _circleColor = Colors.blue;
+ Color _circleStrokeColor = Colors.white;
+ double _circleBlur = 0.0;
+ double _circlePitchAlignment = 0.0; // 0 = viewport, 1 = map
+ double _circleTranslateX = 0.0;
+ double _circleTranslateY = 0.0;
+
+ @override
+ void initState() {
+ super.initState();
+ }
+
+ Future _onMapCreated(MapLibreMapController controller) async {
+ setState(() => _controller = controller);
+ }
+
+ Future _onStyleLoaded() async {
+ await _addCircleLayer();
+ }
+
+ Future _addCircleLayer() async {
+ if (_controller == null) return;
+
+ try {
+ // Add GeoJSON source with multiple points
+ await _controller!.addGeoJsonSource(
+ _sourceId,
+ {
+ 'type': 'FeatureCollection',
+ 'features': _generateRandomPoints(20),
+ },
+ );
+
+ // Add circle layer
+ await _controller!.addCircleLayer(
+ _sourceId,
+ _layerId,
+ CircleLayerProperties(
+ circleRadius: _circleRadius,
+ circleColor:
+ '#${_circleColor.toARGB32().toRadixString(16).substring(2)}',
+ circleOpacity: _circleOpacity,
+ circleStrokeWidth: _circleStrokeWidth,
+ circleStrokeColor:
+ '#${_circleStrokeColor.toARGB32().toRadixString(16).substring(2)}',
+ circleStrokeOpacity: _circleStrokeOpacity,
+ circleBlur: _circleBlur,
+ circlePitchAlignment: _circlePitchAlignment == 0 ? 'viewport' : 'map',
+ circleTranslate: [_circleTranslateX, _circleTranslateY],
+ ),
+ );
+
+ setState(() {});
+ } catch (e) {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text('Error adding circle layer: $e')),
+ );
+ }
+ }
+ }
+
+ List> _generateRandomPoints(int count) {
+ final random = Random();
+ final points = >[];
+
+ for (var i = 0; i < count; i++) {
+ final lat = -33.87 + (random.nextDouble() - 0.5) * 0.2;
+ final lng = 151.21 + (random.nextDouble() - 0.5) * 0.2;
+
+ points.add({
+ 'type': 'Feature',
+ 'properties': {},
+ 'geometry': {
+ 'type': 'Point',
+ 'coordinates': [lng, lat],
+ },
+ });
+ }
+
+ return points;
+ }
+
+ Future _updateLayer() async {
+ if (_controller == null) return;
+
+ try {
+ await _controller!.setLayerProperties(
+ _layerId,
+ CircleLayerProperties(
+ circleRadius: _circleRadius,
+ circleColor:
+ '#${_circleColor.toARGB32().toRadixString(16).substring(2)}',
+ circleOpacity: _circleOpacity,
+ circleStrokeWidth: _circleStrokeWidth,
+ circleStrokeColor:
+ '#${_circleStrokeColor.toARGB32().toRadixString(16).substring(2)}',
+ circleStrokeOpacity: _circleStrokeOpacity,
+ circleBlur: _circleBlur,
+ circlePitchAlignment: _circlePitchAlignment == 0 ? 'viewport' : 'map',
+ circleTranslate: [_circleTranslateX, _circleTranslateY],
+ ),
+ );
+ } catch (e) {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text('Error updating layer: $e')),
+ );
+ }
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return MapExampleScaffold(
+ map: MapLibreMap(
+ styleString: ExampleConstants.demoMapStyle,
+ onMapCreated: _onMapCreated,
+ onStyleLoadedCallback: _onStyleLoaded,
+ initialCameraPosition: const CameraPosition(
+ target: ExampleConstants.sydneyCenter,
+ zoom: 10,
+ ),
+ trackCameraPosition: true,
+ ),
+ controls: _controller == null
+ ? []
+ : [
+ ControlGroup(
+ title: 'Circle Size',
+ children: [
+ ListTile(
+ title: Text('Radius: ${_circleRadius.toStringAsFixed(1)}'),
+ subtitle: Slider(
+ value: _circleRadius,
+ min: 5.0,
+ max: 50.0,
+ divisions: 45,
+ onChanged: (value) async {
+ setState(() => _circleRadius = value);
+ await _updateLayer();
+ },
+ ),
+ ),
+ ListTile(
+ title: Text('Blur: ${_circleBlur.toStringAsFixed(1)}'),
+ subtitle: Slider(
+ value: _circleBlur,
+ min: 0.0,
+ max: 4.0,
+ divisions: 20,
+ onChanged: (value) async {
+ setState(() => _circleBlur = value);
+ await _updateLayer();
+ },
+ ),
+ ),
+ ],
+ ),
+ ControlGroup(
+ title: 'Circle Color',
+ children: [
+ ListTile(
+ title: const Text('Fill Color'),
+ trailing: Container(
+ width: 48,
+ height: 48,
+ decoration: BoxDecoration(
+ color: _circleColor,
+ border: Border.all(color: Colors.grey),
+ borderRadius: BorderRadius.circular(4),
+ ),
+ ),
+ onTap: () => _pickColor('fill'),
+ ),
+ ListTile(
+ title: Text(
+ 'Opacity: ${(_circleOpacity * 100).toStringAsFixed(0)}%'),
+ subtitle: Slider(
+ value: _circleOpacity,
+ min: 0.0,
+ max: 1.0,
+ divisions: 20,
+ onChanged: (value) async {
+ setState(() => _circleOpacity = value);
+ await _updateLayer();
+ },
+ ),
+ ),
+ ],
+ ),
+ ControlGroup(
+ title: 'Circle Stroke',
+ children: [
+ ListTile(
+ title: Text(
+ 'Stroke Width: ${_circleStrokeWidth.toStringAsFixed(1)}'),
+ subtitle: Slider(
+ value: _circleStrokeWidth,
+ min: 0.0,
+ max: 10.0,
+ divisions: 20,
+ onChanged: (value) async {
+ setState(() => _circleStrokeWidth = value);
+ await _updateLayer();
+ },
+ ),
+ ),
+ ListTile(
+ title: const Text('Stroke Color'),
+ trailing: Container(
+ width: 48,
+ height: 48,
+ decoration: BoxDecoration(
+ color: _circleStrokeColor,
+ border: Border.all(color: Colors.grey),
+ borderRadius: BorderRadius.circular(4),
+ ),
+ ),
+ onTap: () => _pickColor('stroke'),
+ ),
+ ListTile(
+ title: Text(
+ 'Stroke Opacity: ${(_circleStrokeOpacity * 100).toStringAsFixed(0)}%'),
+ subtitle: Slider(
+ value: _circleStrokeOpacity,
+ min: 0.0,
+ max: 1.0,
+ divisions: 20,
+ onChanged: (value) async {
+ setState(() => _circleStrokeOpacity = value);
+ await _updateLayer();
+ },
+ ),
+ ),
+ ],
+ ),
+ ControlGroup(
+ title: 'Circle Transform',
+ children: [
+ ListTile(
+ title: const Text('Pitch Alignment'),
+ subtitle: ExampleSegmentedButton(
+ segments: const [
+ ExampleSegment(
+ value: 0.0,
+ label: 'Viewport',
+ ),
+ ExampleSegment(
+ value: 1.0,
+ label: 'Map',
+ ),
+ ],
+ selected: _circlePitchAlignment,
+ onSelectionChanged: (value) async {
+ setState(() => _circlePitchAlignment = value);
+ await _updateLayer();
+ },
+ ),
+ ),
+ ListTile(
+ title: Text(
+ 'Translate X: ${_circleTranslateX.toStringAsFixed(0)}'),
+ subtitle: Slider(
+ value: _circleTranslateX,
+ min: -50.0,
+ max: 50.0,
+ divisions: 100,
+ onChanged: (value) async {
+ setState(() => _circleTranslateX = value);
+ await _updateLayer();
+ },
+ ),
+ ),
+ ListTile(
+ title: Text(
+ 'Translate Y: ${_circleTranslateY.toStringAsFixed(0)}'),
+ subtitle: Slider(
+ value: _circleTranslateY,
+ min: -50.0,
+ max: 50.0,
+ divisions: 100,
+ onChanged: (value) async {
+ setState(() => _circleTranslateY = value);
+ await _updateLayer();
+ },
+ ),
+ ),
+ ],
+ ),
+ ControlGroup(
+ title: 'Actions',
+ children: [
+ ExampleButton(
+ label: 'Reset Properties',
+ onPressed: () async {
+ setState(() {
+ _circleRadius = 20.0;
+ _circleOpacity = 0.8;
+ _circleStrokeWidth = 2.0;
+ _circleStrokeOpacity = 1.0;
+ _circleColor = Colors.blue;
+ _circleStrokeColor = Colors.white;
+ _circleBlur = 0.0;
+ _circlePitchAlignment = 0.0;
+ _circleTranslateX = 0.0;
+ _circleTranslateY = 0.0;
+ });
+ await _updateLayer();
+ },
+ ),
+ ],
+ ),
+ ],
+ );
+ }
+
+ Future _pickColor(String type) async {
+ final currentColor = type == 'fill' ? _circleColor : _circleStrokeColor;
+ final title = type == 'fill' ? 'Select Fill Color' : 'Select Stroke Color';
+
+ final selectedColor = await ColorPickerModal.show(
+ context: context,
+ title: title,
+ currentColor: currentColor,
+ );
+
+ if (selectedColor != null) {
+ setState(() {
+ if (type == 'fill') {
+ _circleColor = selectedColor;
+ } else {
+ _circleStrokeColor = selectedColor;
+ }
+ });
+ await _updateLayer();
+ }
+ }
+}
diff --git a/maplibre_gl_example/lib/examples/layers/fill_layer_example.dart b/maplibre_gl_example/lib/examples/layers/fill_layer_example.dart
new file mode 100644
index 000000000..5578905ec
--- /dev/null
+++ b/maplibre_gl_example/lib/examples/layers/fill_layer_example.dart
@@ -0,0 +1,298 @@
+import 'dart:math';
+
+import 'package:flutter/material.dart';
+import 'package:maplibre_gl/maplibre_gl.dart';
+import '../../page.dart';
+import '../../shared/shared.dart';
+
+/// Example demonstrating fill layer properties
+class FillLayerExample extends ExamplePage {
+ const FillLayerExample({super.key})
+ : super(
+ const Icon(Icons.square),
+ 'Fill Layer',
+ category: ExampleCategory.layers,
+ );
+
+ @override
+ Widget build(BuildContext context) => const _FillLayerBody();
+}
+
+class _FillLayerBody extends StatefulWidget {
+ const _FillLayerBody();
+
+ @override
+ State<_FillLayerBody> createState() => _FillLayerBodyState();
+}
+
+class _FillLayerBodyState extends State<_FillLayerBody> {
+ MapLibreMapController? _controller;
+
+ static const _sourceId = 'fill_source';
+ static const _layerId = 'fill_layer';
+
+ // Fill properties
+ double _fillOpacity = 0.6;
+ Color _fillColor = const Color(0xFF3498DB);
+ Color _fillOutlineColor = const Color(0xFF2C3E50);
+ double _fillTranslateX = 0.0;
+ double _fillTranslateY = 0.0;
+ bool _fillAntialias = true;
+
+ @override
+ void initState() {
+ super.initState();
+ }
+
+ void _onMapCreated(MapLibreMapController controller) {
+ setState(() => _controller = controller);
+ }
+
+ Future _onStyleLoaded() async {
+ await _addFillLayer();
+ }
+
+ Future _addFillLayer() async {
+ if (_controller == null) return;
+
+ try {
+ // Add GeoJSON source with multiple polygons
+ await _controller!.addGeoJsonSource(
+ _sourceId,
+ {
+ 'type': 'FeatureCollection',
+ 'features': _generateRandomPolygons(5),
+ },
+ );
+
+ // Add fill layer
+ await _controller!.addFillLayer(
+ _sourceId,
+ _layerId,
+ FillLayerProperties(
+ fillOpacity: _fillOpacity,
+ fillColor: '#${_fillColor.toARGB32().toRadixString(16).substring(2)}',
+ fillOutlineColor:
+ '#${_fillOutlineColor.toARGB32().toRadixString(16).substring(2)}',
+ fillTranslate: [_fillTranslateX, _fillTranslateY],
+ fillAntialias: _fillAntialias,
+ ),
+ );
+
+ setState(() {});
+ } catch (e) {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text('Error adding fill layer: $e')),
+ );
+ }
+ }
+ }
+
+ List> _generateRandomPolygons(int count) {
+ final random = Random();
+ final polygons = >[];
+
+ for (var i = 0; i < count; i++) {
+ final centerLat = -33.87 + (random.nextDouble() - 0.5) * 0.2;
+ final centerLng = 151.21 + (random.nextDouble() - 0.5) * 0.2;
+ final size = 0.01 + random.nextDouble() * 0.02;
+
+ polygons.add({
+ 'type': 'Feature',
+ 'properties': {},
+ 'geometry': {
+ 'type': 'Polygon',
+ 'coordinates': [
+ [
+ [centerLng - size, centerLat + size],
+ [centerLng + size, centerLat + size],
+ [centerLng + size, centerLat - size],
+ [centerLng - size, centerLat - size],
+ [centerLng - size, centerLat + size],
+ ]
+ ],
+ },
+ });
+ }
+
+ return polygons;
+ }
+
+ Future _updateLayer() async {
+ if (_controller == null) return;
+
+ try {
+ await _controller!.setLayerProperties(
+ _layerId,
+ FillLayerProperties(
+ fillOpacity: _fillOpacity,
+ fillColor: '#${_fillColor.toARGB32().toRadixString(16).substring(2)}',
+ fillOutlineColor:
+ '#${_fillOutlineColor.toARGB32().toRadixString(16).substring(2)}',
+ fillTranslate: [_fillTranslateX, _fillTranslateY],
+ fillAntialias: _fillAntialias,
+ ),
+ );
+ } catch (e) {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text('Error updating layer: $e')),
+ );
+ }
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return MapExampleScaffold(
+ map: MapLibreMap(
+ styleString: ExampleConstants.demoMapStyle,
+ onMapCreated: _onMapCreated,
+ onStyleLoadedCallback: _onStyleLoaded,
+ initialCameraPosition: const CameraPosition(
+ target: ExampleConstants.sydneyCenter,
+ zoom: 10,
+ ),
+ trackCameraPosition: true,
+ ),
+ controls: _controller == null
+ ? []
+ : [
+ ControlGroup(
+ title: 'Fill Color',
+ children: [
+ ListTile(
+ title: const Text('Fill Color'),
+ trailing: Container(
+ width: 48,
+ height: 48,
+ decoration: BoxDecoration(
+ color: _fillColor,
+ border: Border.all(color: Colors.grey),
+ borderRadius: BorderRadius.circular(4),
+ ),
+ ),
+ onTap: () => _pickColor('fill'),
+ ),
+ ListTile(
+ title: Text(
+ 'Opacity: ${(_fillOpacity * 100).toStringAsFixed(0)}%'),
+ subtitle: Slider(
+ value: _fillOpacity,
+ min: 0.0,
+ max: 1.0,
+ divisions: 20,
+ onChanged: (value) async {
+ setState(() => _fillOpacity = value);
+ await _updateLayer();
+ },
+ ),
+ ),
+ ],
+ ),
+ ControlGroup(
+ title: 'Fill Outline',
+ children: [
+ ListTile(
+ title: const Text('Outline Color'),
+ trailing: Container(
+ width: 48,
+ height: 48,
+ decoration: BoxDecoration(
+ color: _fillOutlineColor,
+ border: Border.all(color: Colors.grey),
+ borderRadius: BorderRadius.circular(4),
+ ),
+ ),
+ onTap: () => _pickColor('outline'),
+ ),
+ SwitchListTile(
+ title: const Text('Antialias'),
+ subtitle: const Text('Smooth edges'),
+ value: _fillAntialias,
+ onChanged: (value) async {
+ setState(() => _fillAntialias = value);
+ await _updateLayer();
+ },
+ ),
+ ],
+ ),
+ ControlGroup(
+ title: 'Fill Transform',
+ children: [
+ ListTile(
+ title: Text(
+ 'Translate X: ${_fillTranslateX.toStringAsFixed(0)}'),
+ subtitle: Slider(
+ value: _fillTranslateX,
+ min: -50.0,
+ max: 50.0,
+ divisions: 100,
+ onChanged: (value) async {
+ setState(() => _fillTranslateX = value);
+ await _updateLayer();
+ },
+ ),
+ ),
+ ListTile(
+ title: Text(
+ 'Translate Y: ${_fillTranslateY.toStringAsFixed(0)}'),
+ subtitle: Slider(
+ value: _fillTranslateY,
+ min: -50.0,
+ max: 50.0,
+ divisions: 100,
+ onChanged: (value) async {
+ setState(() => _fillTranslateY = value);
+ await _updateLayer();
+ },
+ ),
+ ),
+ ],
+ ),
+ ControlGroup(
+ title: 'Actions',
+ children: [
+ ExampleButton(
+ label: 'Reset Properties',
+ onPressed: () async {
+ setState(() {
+ _fillOpacity = 0.6;
+ _fillColor = const Color(0xFF3498DB);
+ _fillOutlineColor = const Color(0xFF2C3E50);
+ _fillTranslateX = 0.0;
+ _fillTranslateY = 0.0;
+ _fillAntialias = true;
+ });
+ await _updateLayer();
+ },
+ ),
+ ],
+ ),
+ ],
+ );
+ }
+
+ Future _pickColor(String type) async {
+ final currentColor = type == 'fill' ? _fillColor : _fillOutlineColor;
+ final title = type == 'fill' ? 'Select Fill Color' : 'Select Outline Color';
+
+ final selectedColor = await ColorPickerModal.show(
+ context: context,
+ title: title,
+ currentColor: currentColor,
+ );
+
+ if (selectedColor != null) {
+ setState(() {
+ if (type == 'fill') {
+ _fillColor = selectedColor;
+ } else {
+ _fillOutlineColor = selectedColor;
+ }
+ });
+ await _updateLayer();
+ }
+ }
+}
diff --git a/maplibre_gl_example/lib/examples/layers/line_layer_example.dart b/maplibre_gl_example/lib/examples/layers/line_layer_example.dart
new file mode 100644
index 000000000..382bcde68
--- /dev/null
+++ b/maplibre_gl_example/lib/examples/layers/line_layer_example.dart
@@ -0,0 +1,455 @@
+import 'dart:math';
+
+import 'package:flutter/material.dart';
+import 'package:maplibre_gl/maplibre_gl.dart';
+import '../../page.dart';
+import '../../shared/shared.dart';
+
+/// Example demonstrating line layer properties
+class LineLayerExample extends ExamplePage {
+ const LineLayerExample({super.key})
+ : super(
+ const Icon(Icons.timeline),
+ 'Line Layer',
+ category: ExampleCategory.layers,
+ );
+
+ @override
+ Widget build(BuildContext context) => const _LineLayerBody();
+}
+
+class _LineLayerBody extends StatefulWidget {
+ const _LineLayerBody();
+
+ @override
+ State<_LineLayerBody> createState() => _LineLayerBodyState();
+}
+
+class _LineLayerBodyState extends State<_LineLayerBody> {
+ MapLibreMapController? _controller;
+
+ static const _sourceId = 'line_source';
+ static const _layerId = 'line_layer';
+
+ // Line properties
+ double _lineWidth = 4.0;
+ double _lineOpacity = 0.9;
+ Color _lineColor = const Color(0xFFE74C3C);
+ double _lineBlur = 0.0;
+ double _lineGapWidth = 0.0;
+ double _lineOffset = 0.0;
+ double _lineTranslateX = 0.0;
+ double _lineTranslateY = 0.0;
+ String _lineCap = 'round'; // butt, round, square
+ String _lineJoin = 'round'; // bevel, round, miter
+ double _lineMiterLimit = 2.0;
+ double _lineRoundLimit = 1.05;
+
+ @override
+ void initState() {
+ super.initState();
+ }
+
+ void _onMapCreated(MapLibreMapController controller) {
+ setState(() => _controller = controller);
+ }
+
+ Future _onStyleLoaded() async {
+ await _addLineLayer();
+ }
+
+ Future _addLineLayer() async {
+ if (_controller == null) return;
+
+ try {
+ // Add GeoJSON source with multiple lines
+ await _controller!.addGeoJsonSource(
+ _sourceId,
+ {
+ 'type': 'FeatureCollection',
+ 'features': _generateRandomLines(5),
+ },
+ );
+
+ // Add line layer
+ await _controller!.addLineLayer(
+ _sourceId,
+ _layerId,
+ LineLayerProperties(
+ lineWidth: _lineWidth,
+ lineOpacity: _lineOpacity,
+ lineColor: '#${_lineColor.toARGB32().toRadixString(16).substring(2)}',
+ lineBlur: _lineBlur,
+ lineGapWidth: _lineGapWidth,
+ lineOffset: _lineOffset,
+ lineTranslate: [_lineTranslateX, _lineTranslateY],
+ lineCap: _lineCap,
+ lineJoin: _lineJoin,
+ lineMiterLimit: _lineMiterLimit,
+ lineRoundLimit: _lineRoundLimit,
+ ),
+ );
+
+ setState(() {});
+ } catch (e) {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text('Error adding line layer: $e')),
+ );
+ }
+ }
+ }
+
+ List> _generateRandomLines(int count) {
+ final random = Random();
+ final lines = >[];
+
+ for (var i = 0; i < count; i++) {
+ final startLat = -33.87 + (random.nextDouble() - 0.5) * 0.2;
+ final startLng = 151.21 + (random.nextDouble() - 0.5) * 0.2;
+
+ // Generate a curved line with multiple points
+ final coordinates = >[];
+ for (var j = 0; j < 5; j++) {
+ coordinates.add([
+ startLng + j * 0.02 + (random.nextDouble() - 0.5) * 0.01,
+ startLat + (random.nextDouble() - 0.5) * 0.04,
+ ]);
+ }
+
+ lines.add({
+ 'type': 'Feature',
+ 'properties': {},
+ 'geometry': {
+ 'type': 'LineString',
+ 'coordinates': coordinates,
+ },
+ });
+ }
+
+ return lines;
+ }
+
+ Future _updateLayer() async {
+ if (_controller == null) return;
+
+ try {
+ await _controller!.setLayerProperties(
+ _layerId,
+ LineLayerProperties(
+ lineWidth: _lineWidth,
+ lineOpacity: _lineOpacity,
+ lineColor: '#${_lineColor.toARGB32().toRadixString(16).substring(2)}',
+ lineBlur: _lineBlur,
+ lineGapWidth: _lineGapWidth,
+ lineOffset: _lineOffset,
+ lineTranslate: [_lineTranslateX, _lineTranslateY],
+ lineCap: _lineCap,
+ lineJoin: _lineJoin,
+ lineMiterLimit: _lineMiterLimit,
+ lineRoundLimit: _lineRoundLimit,
+ ),
+ );
+ } catch (e) {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text('Error updating layer: $e')),
+ );
+ }
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return MapExampleScaffold(
+ map: MapLibreMap(
+ styleString: ExampleConstants.demoMapStyle,
+ onMapCreated: _onMapCreated,
+ onStyleLoadedCallback: _onStyleLoaded,
+ initialCameraPosition: const CameraPosition(
+ target: ExampleConstants.sydneyCenter,
+ zoom: 10,
+ ),
+ trackCameraPosition: true,
+ ),
+ controls: _controller == null
+ ? []
+ : [
+ ControlGroup(
+ title: 'Line Appearance',
+ children: [
+ ListTile(
+ title: Text('Width: ${_lineWidth.toStringAsFixed(1)}'),
+ subtitle: Slider(
+ value: _lineWidth,
+ min: 1.0,
+ max: 20.0,
+ divisions: 38,
+ onChanged: (value) async {
+ setState(() => _lineWidth = value);
+ await _updateLayer();
+ },
+ ),
+ ),
+ ListTile(
+ title: Text('Blur: ${_lineBlur.toStringAsFixed(1)}'),
+ subtitle: Slider(
+ value: _lineBlur,
+ min: 0.0,
+ max: 10.0,
+ divisions: 20,
+ onChanged: (value) async {
+ setState(() => _lineBlur = value);
+ await _updateLayer();
+ },
+ ),
+ ),
+ ListTile(
+ title: const Text('Color'),
+ trailing: Container(
+ width: 48,
+ height: 48,
+ decoration: BoxDecoration(
+ color: _lineColor,
+ border: Border.all(color: Colors.grey),
+ borderRadius: BorderRadius.circular(4),
+ ),
+ ),
+ onTap: _pickColor,
+ ),
+ ListTile(
+ title: Text(
+ 'Opacity: ${(_lineOpacity * 100).toStringAsFixed(0)}%'),
+ subtitle: Slider(
+ value: _lineOpacity,
+ min: 0.0,
+ max: 1.0,
+ divisions: 20,
+ onChanged: (value) async {
+ setState(() => _lineOpacity = value);
+ await _updateLayer();
+ },
+ ),
+ ),
+ ],
+ ),
+ ControlGroup(
+ title: 'Line Style',
+ children: [
+ ListTile(
+ title: const Text('Cap Style'),
+ subtitle: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text('End of line appearance'),
+ const SizedBox(height: 8),
+ ExampleSegmentedButton(
+ segments: const [
+ ExampleSegment(
+ value: 'butt',
+ label: 'Butt',
+ ),
+ ExampleSegment(
+ value: 'round',
+ label: 'Round',
+ ),
+ ExampleSegment(
+ value: 'square',
+ label: 'Square',
+ ),
+ ],
+ selected: _lineCap,
+ onSelectionChanged: (value) async {
+ setState(() => _lineCap = value);
+ await _updateLayer();
+ },
+ ),
+ ],
+ ),
+ ),
+ ListTile(
+ title: const Text('Join Style'),
+ subtitle: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text('Line corner appearance'),
+ const SizedBox(height: 8),
+ ExampleSegmentedButton(
+ segments: const [
+ ExampleSegment(
+ value: 'bevel',
+ label: 'Bevel',
+ ),
+ ExampleSegment(
+ value: 'round',
+ label: 'Round',
+ ),
+ ExampleSegment(
+ value: 'miter',
+ label: 'Miter',
+ ),
+ ],
+ selected: _lineJoin,
+ onSelectionChanged: (value) async {
+ setState(() => _lineJoin = value);
+ await _updateLayer();
+ },
+ ),
+ ],
+ ),
+ ),
+ if (_lineJoin == 'miter')
+ ListTile(
+ title: Text(
+ 'Miter Limit: ${_lineMiterLimit.toStringAsFixed(1)}'),
+ subtitle: const Text('Sharp corner threshold'),
+ trailing: SizedBox(
+ width: 200,
+ child: Slider(
+ value: _lineMiterLimit,
+ min: 1.0,
+ max: 10.0,
+ divisions: 18,
+ onChanged: (value) async {
+ setState(() => _lineMiterLimit = value);
+ await _updateLayer();
+ },
+ ),
+ ),
+ ),
+ if (_lineJoin == 'round')
+ ListTile(
+ title: Text(
+ 'Round Limit: ${_lineRoundLimit.toStringAsFixed(2)}'),
+ subtitle: const Text('Round corner threshold'),
+ trailing: SizedBox(
+ width: 200,
+ child: Slider(
+ value: _lineRoundLimit,
+ min: 1.0,
+ max: 2.0,
+ divisions: 20,
+ onChanged: (value) async {
+ setState(() => _lineRoundLimit = value);
+ await _updateLayer();
+ },
+ ),
+ ),
+ ),
+ ],
+ ),
+ ControlGroup(
+ title: 'Line Layout',
+ children: [
+ ListTile(
+ title:
+ Text('Gap Width: ${_lineGapWidth.toStringAsFixed(1)}'),
+ subtitle: const Text('Space between parallel lines'),
+ trailing: SizedBox(
+ width: 200,
+ child: Slider(
+ value: _lineGapWidth,
+ min: 0.0,
+ max: 20.0,
+ divisions: 20,
+ onChanged: (value) async {
+ setState(() => _lineGapWidth = value);
+ await _updateLayer();
+ },
+ ),
+ ),
+ ),
+ ListTile(
+ title: Text('Offset: ${_lineOffset.toStringAsFixed(1)}'),
+ subtitle: const Text('Perpendicular shift'),
+ trailing: SizedBox(
+ width: 200,
+ child: Slider(
+ value: _lineOffset,
+ min: -20.0,
+ max: 20.0,
+ divisions: 40,
+ onChanged: (value) async {
+ setState(() => _lineOffset = value);
+ await _updateLayer();
+ },
+ ),
+ ),
+ ),
+ ],
+ ),
+ ControlGroup(
+ title: 'Line Transform',
+ children: [
+ ListTile(
+ title: Text(
+ 'Translate X: ${_lineTranslateX.toStringAsFixed(0)}'),
+ subtitle: Slider(
+ value: _lineTranslateX,
+ min: -50.0,
+ max: 50.0,
+ divisions: 100,
+ onChanged: (value) async {
+ setState(() => _lineTranslateX = value);
+ await _updateLayer();
+ },
+ ),
+ ),
+ ListTile(
+ title: Text(
+ 'Translate Y: ${_lineTranslateY.toStringAsFixed(0)}'),
+ subtitle: Slider(
+ value: _lineTranslateY,
+ min: -50.0,
+ max: 50.0,
+ divisions: 100,
+ onChanged: (value) async {
+ setState(() => _lineTranslateY = value);
+ await _updateLayer();
+ },
+ ),
+ ),
+ ],
+ ),
+ ControlGroup(
+ title: 'Actions',
+ children: [
+ ExampleButton(
+ label: 'Reset Properties',
+ onPressed: () async {
+ setState(() {
+ _lineWidth = 4.0;
+ _lineOpacity = 0.9;
+ _lineColor = const Color(0xFFE74C3C);
+ _lineBlur = 0.0;
+ _lineGapWidth = 0.0;
+ _lineOffset = 0.0;
+ _lineTranslateX = 0.0;
+ _lineTranslateY = 0.0;
+ _lineCap = 'round';
+ _lineJoin = 'round';
+ _lineMiterLimit = 2.0;
+ _lineRoundLimit = 1.05;
+ });
+ await _updateLayer();
+ },
+ ),
+ ],
+ ),
+ ],
+ );
+ }
+
+ Future _pickColor() async {
+ final selectedColor = await ColorPickerModal.show(
+ context: context,
+ title: 'Select Line Color',
+ currentColor: _lineColor,
+ );
+
+ if (selectedColor != null) {
+ setState(() => _lineColor = selectedColor);
+ await _updateLayer();
+ }
+ }
+}
diff --git a/maplibre_gl_example/lib/examples/layers/symbol_layer_example.dart b/maplibre_gl_example/lib/examples/layers/symbol_layer_example.dart
new file mode 100644
index 000000000..dac2e825a
--- /dev/null
+++ b/maplibre_gl_example/lib/examples/layers/symbol_layer_example.dart
@@ -0,0 +1,651 @@
+import 'dart:math';
+
+import 'package:flutter/material.dart';
+import 'package:maplibre_gl/maplibre_gl.dart';
+import 'package:maplibre_gl_example/util.dart';
+import '../../page.dart';
+import '../../shared/shared.dart';
+
+/// Example demonstrating symbol layer properties
+class SymbolLayerExample extends ExamplePage {
+ const SymbolLayerExample({super.key})
+ : super(
+ const Icon(Icons.place),
+ 'Symbol Layer',
+ category: ExampleCategory.layers,
+ );
+
+ @override
+ Widget build(BuildContext context) => const _SymbolLayerBody();
+}
+
+class _SymbolLayerBody extends StatefulWidget {
+ const _SymbolLayerBody();
+
+ @override
+ State<_SymbolLayerBody> createState() => _SymbolLayerBodyState();
+}
+
+class _SymbolLayerBodyState extends State<_SymbolLayerBody> {
+ MapLibreMapController? _controller;
+
+ static const _sourceId = 'symbol_source';
+ static const _layerId = 'symbol_layer';
+
+ // Text properties
+ double _textSize = 14.0;
+ Color _textColor = const Color(0xFF2C3E50);
+ double _textOpacity = 1.0;
+ double _textHaloWidth = 2.0;
+ Color _textHaloColor = Colors.white;
+ double _textHaloBlur = 1.0;
+ double _textRotate = 0.0;
+ double _textOffsetX = 0.0;
+ double _textOffsetY = 0.0;
+ String _textAnchor = 'center'; // center, left, right, top, bottom
+ String _textJustify = 'center'; // left, center, right
+ bool _textAllowOverlap = false;
+ bool _textIgnorePlacement = false;
+
+ // Icon properties
+ bool _showIcon = true;
+ double _iconSize = 1.0;
+ double _iconRotate = 0.0;
+ double _iconOffsetX = 0.0;
+ double _iconOffsetY = -1.5;
+ bool _iconAllowOverlap = false;
+ bool _iconIgnorePlacement = false;
+
+ // Symbol layout
+ String _symbolPlacement = 'point'; // point, line
+ double _symbolSpacing = 250.0;
+ bool _symbolAvoidEdges = false;
+
+ @override
+ void initState() {
+ super.initState();
+ }
+
+ void _onMapCreated(MapLibreMapController controller) {
+ setState(() => _controller = controller);
+ }
+
+ Future _onStyleLoaded() async {
+ await _addSymbolLayer();
+ }
+
+ Future _addSymbolLayer() async {
+ if (_controller == null) return;
+
+ try {
+ await addImageFromAsset(
+ _controller!,
+ "custom-marker",
+ "assets/symbols/custom-marker.png",
+ );
+
+ // Add GeoJSON source with points
+ await _controller!.addGeoJsonSource(
+ _sourceId,
+ {
+ 'type': 'FeatureCollection',
+ 'features': _generateRandomPoints(10),
+ },
+ );
+
+ // Add symbol layer
+ await _controller!.addSymbolLayer(
+ _sourceId,
+ _layerId,
+ SymbolLayerProperties(
+ // Icon properties
+ iconImage: _showIcon ? 'custom-marker' : null,
+ iconSize: _iconSize,
+ iconRotate: _iconRotate,
+ iconOffset: [_iconOffsetX, _iconOffsetY],
+ iconAllowOverlap: _iconAllowOverlap,
+ iconIgnorePlacement: _iconIgnorePlacement,
+ // Text properties
+ textField: '{name}',
+ textSize: _textSize,
+ textColor: '#${_textColor.toARGB32().toRadixString(16).substring(2)}',
+ textOpacity: _textOpacity,
+ textHaloWidth: _textHaloWidth,
+ textHaloColor:
+ '#${_textHaloColor.toARGB32().toRadixString(16).substring(2)}',
+ textHaloBlur: _textHaloBlur,
+ textRotate: _textRotate,
+ textOffset: [_textOffsetX, _textOffsetY],
+ textAnchor: _textAnchor,
+ textJustify: _textJustify,
+ textAllowOverlap: _textAllowOverlap,
+ textIgnorePlacement: _textIgnorePlacement,
+ symbolPlacement: _symbolPlacement,
+ symbolSpacing: _symbolSpacing,
+ symbolAvoidEdges: _symbolAvoidEdges,
+ ),
+ );
+
+ setState(() {});
+ } catch (e) {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text('Error adding symbol layer: $e')),
+ );
+ }
+ }
+ }
+
+ List> _generateRandomPoints(int count) {
+ final random = Random();
+ final points = >[];
+ final cities = [
+ 'Sydney',
+ 'Melbourne',
+ 'Brisbane',
+ 'Perth',
+ 'Adelaide',
+ 'Canberra',
+ 'Hobart',
+ 'Darwin',
+ 'Newcastle',
+ 'Wollongong',
+ ];
+
+ for (var i = 0; i < count; i++) {
+ points.add({
+ 'type': 'Feature',
+ 'properties': {
+ 'name': cities[i % cities.length],
+ },
+ 'geometry': {
+ 'type': 'Point',
+ 'coordinates': [
+ 151.21 + (random.nextDouble() - 0.5) * 0.3,
+ -33.87 + (random.nextDouble() - 0.5) * 0.3,
+ ],
+ },
+ });
+ }
+
+ return points;
+ }
+
+ Future _updateLayer() async {
+ if (_controller == null) return;
+
+ try {
+ await _controller!.setLayerProperties(
+ _layerId,
+ SymbolLayerProperties(
+ // Icon properties
+ iconImage: _showIcon ? 'custom-marker' : null,
+ iconSize: _iconSize,
+ iconRotate: _iconRotate,
+ iconOffset: [_iconOffsetX, _iconOffsetY],
+ iconAllowOverlap: _iconAllowOverlap,
+ iconIgnorePlacement: _iconIgnorePlacement,
+ // Text properties
+ textField: '{name}',
+ textSize: _textSize,
+ textColor: '#${_textColor.toARGB32().toRadixString(16).substring(2)}',
+ textOpacity: _textOpacity,
+ textHaloWidth: _textHaloWidth,
+ textHaloColor:
+ '#${_textHaloColor.toARGB32().toRadixString(16).substring(2)}',
+ textHaloBlur: _textHaloBlur,
+ textRotate: _textRotate,
+ textOffset: [_textOffsetX, _textOffsetY],
+ textAnchor: _textAnchor,
+ textJustify: _textJustify,
+ textAllowOverlap: _textAllowOverlap,
+ textIgnorePlacement: _textIgnorePlacement,
+ symbolPlacement: _symbolPlacement,
+ symbolSpacing: _symbolSpacing,
+ symbolAvoidEdges: _symbolAvoidEdges,
+ ),
+ );
+ } catch (e) {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text('Error updating layer: $e')),
+ );
+ }
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return MapExampleScaffold(
+ map: MapLibreMap(
+ styleString: ExampleConstants.demoMapStyle,
+ onMapCreated: _onMapCreated,
+ onStyleLoadedCallback: _onStyleLoaded,
+ initialCameraPosition: const CameraPosition(
+ target: ExampleConstants.sydneyCenter,
+ zoom: 10,
+ ),
+ trackCameraPosition: true,
+ ),
+ controls: _controller == null
+ ? []
+ : [
+ ControlGroup(
+ title: 'Icon Properties',
+ children: [
+ ListTile(
+ title: Text('Icon Size: ${_iconSize.toStringAsFixed(1)}'),
+ subtitle: Slider(
+ value: _iconSize,
+ min: 0.5,
+ max: 3.0,
+ divisions: 25,
+ onChanged: (value) async {
+ setState(() => _iconSize = value);
+ await _updateLayer();
+ },
+ ),
+ ),
+ ListTile(
+ title: Text(
+ 'Icon Rotation: ${_iconRotate.toStringAsFixed(0)}°'),
+ subtitle: Slider(
+ value: _iconRotate,
+ min: 0.0,
+ max: 360.0,
+ divisions: 72,
+ onChanged: (value) async {
+ setState(() => _iconRotate = value);
+ await _updateLayer();
+ },
+ ),
+ ),
+ ListTile(
+ title: Text(
+ 'Icon Offset X: ${_iconOffsetX.toStringAsFixed(1)}'),
+ subtitle: Slider(
+ value: _iconOffsetX,
+ min: -3.0,
+ max: 3.0,
+ divisions: 60,
+ onChanged: (value) async {
+ setState(() => _iconOffsetX = value);
+ await _updateLayer();
+ },
+ ),
+ ),
+ ListTile(
+ title: Text(
+ 'Icon Offset Y: ${_iconOffsetY.toStringAsFixed(1)}'),
+ subtitle: Slider(
+ value: _iconOffsetY,
+ min: -3.0,
+ max: 3.0,
+ divisions: 60,
+ onChanged: (value) async {
+ setState(() => _iconOffsetY = value);
+ await _updateLayer();
+ },
+ ),
+ ),
+ SwitchListTile(
+ title: const Text('Icon Allow Overlap'),
+ subtitle: const Text('Allow icons to overlap'),
+ value: _iconAllowOverlap,
+ onChanged: (value) async {
+ setState(() => _iconAllowOverlap = value);
+ await _updateLayer();
+ },
+ ),
+ SwitchListTile(
+ title: const Text('Icon Ignore Placement'),
+ subtitle: const Text('Ignore icon collision detection'),
+ value: _iconIgnorePlacement,
+ onChanged: (value) async {
+ setState(() => _iconIgnorePlacement = value);
+ await _updateLayer();
+ },
+ ),
+ ],
+ ),
+ ControlGroup(
+ title: 'Text Appearance',
+ children: [
+ ListTile(
+ title: Text('Size: ${_textSize.toStringAsFixed(1)}'),
+ subtitle: Slider(
+ value: _textSize,
+ min: 8.0,
+ max: 32.0,
+ divisions: 48,
+ onChanged: (value) async {
+ setState(() => _textSize = value);
+ await _updateLayer();
+ },
+ ),
+ ),
+ ListTile(
+ title: const Text('Text Color'),
+ trailing: Container(
+ width: 48,
+ height: 48,
+ decoration: BoxDecoration(
+ color: _textColor,
+ border: Border.all(color: Colors.grey),
+ borderRadius: BorderRadius.circular(4),
+ ),
+ ),
+ onTap: () => _pickColor(true),
+ ),
+ ListTile(
+ title: Text(
+ 'Opacity: ${(_textOpacity * 100).toStringAsFixed(0)}%'),
+ subtitle: Slider(
+ value: _textOpacity,
+ min: 0.0,
+ max: 1.0,
+ divisions: 20,
+ onChanged: (value) async {
+ setState(() => _textOpacity = value);
+ await _updateLayer();
+ },
+ ),
+ ),
+ ],
+ ),
+ ControlGroup(
+ title: 'Text Halo',
+ children: [
+ ListTile(
+ title: Text(
+ 'Halo Width: ${_textHaloWidth.toStringAsFixed(1)}'),
+ subtitle: Slider(
+ value: _textHaloWidth,
+ min: 0.0,
+ max: 4.0,
+ divisions: 20,
+ onChanged: (value) async {
+ setState(() => _textHaloWidth = value);
+ await _updateLayer();
+ },
+ ),
+ ),
+ ListTile(
+ title: const Text('Halo Color'),
+ trailing: Container(
+ width: 48,
+ height: 48,
+ decoration: BoxDecoration(
+ color: _textHaloColor,
+ border: Border.all(color: Colors.grey),
+ borderRadius: BorderRadius.circular(4),
+ ),
+ ),
+ onTap: () => _pickColor(false),
+ ),
+ ListTile(
+ title:
+ Text('Halo Blur: ${_textHaloBlur.toStringAsFixed(1)}'),
+ subtitle: Slider(
+ value: _textHaloBlur,
+ min: 0.0,
+ max: 4.0,
+ divisions: 20,
+ onChanged: (value) async {
+ setState(() => _textHaloBlur = value);
+ await _updateLayer();
+ },
+ ),
+ ),
+ ],
+ ),
+ ControlGroup(
+ title: 'Text Layout',
+ children: [
+ ListTile(
+ title: const Text('Anchor Position'),
+ subtitle: const Text('Text alignment relative to point'),
+ trailing: DropdownButton(
+ value: _textAnchor,
+ items: const [
+ DropdownMenuItem(
+ value: 'center', child: Text('Center')),
+ DropdownMenuItem(value: 'top', child: Text('Top')),
+ DropdownMenuItem(
+ value: 'bottom', child: Text('Bottom')),
+ DropdownMenuItem(value: 'left', child: Text('Left')),
+ DropdownMenuItem(value: 'right', child: Text('Right')),
+ DropdownMenuItem(
+ value: 'top-left', child: Text('Top Left')),
+ DropdownMenuItem(
+ value: 'top-right', child: Text('Top Right')),
+ DropdownMenuItem(
+ value: 'bottom-left', child: Text('Bottom Left')),
+ DropdownMenuItem(
+ value: 'bottom-right', child: Text('Bottom Right')),
+ ],
+ onChanged: (value) async {
+ if (value != null) {
+ setState(() => _textAnchor = value);
+ await _updateLayer();
+ }
+ },
+ ),
+ ),
+ ListTile(
+ title: const Text('Text Justify'),
+ subtitle: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text('Horizontal text alignment'),
+ const SizedBox(height: 8),
+ ExampleSegmentedButton(
+ segments: const [
+ ExampleSegment(
+ value: 'left',
+ label: 'Left',
+ ),
+ ExampleSegment(
+ value: 'center',
+ label: 'Center',
+ ),
+ ExampleSegment(
+ value: 'right',
+ label: 'Right',
+ ),
+ ],
+ selected: _textJustify,
+ onSelectionChanged: (value) async {
+ setState(() => _textJustify = value);
+ await _updateLayer();
+ },
+ ),
+ ],
+ ),
+ ),
+ ListTile(
+ title: Text('Rotation: ${_textRotate.toStringAsFixed(0)}°'),
+ subtitle: Slider(
+ value: _textRotate,
+ min: 0.0,
+ max: 360.0,
+ divisions: 72,
+ onChanged: (value) async {
+ setState(() => _textRotate = value);
+ await _updateLayer();
+ },
+ ),
+ ),
+ ],
+ ),
+ ControlGroup(
+ title: 'Text Position',
+ children: [
+ ListTile(
+ title:
+ Text('Offset X: ${_textOffsetX.toStringAsFixed(1)} em'),
+ subtitle: Slider(
+ value: _textOffsetX,
+ min: -2.0,
+ max: 2.0,
+ divisions: 40,
+ onChanged: (value) async {
+ setState(() => _textOffsetX = value);
+ await _updateLayer();
+ },
+ ),
+ ),
+ ListTile(
+ title:
+ Text('Offset Y: ${_textOffsetY.toStringAsFixed(1)} em'),
+ subtitle: Slider(
+ value: _textOffsetY,
+ min: -2.0,
+ max: 2.0,
+ divisions: 40,
+ onChanged: (value) async {
+ setState(() => _textOffsetY = value);
+ await _updateLayer();
+ },
+ ),
+ ),
+ ],
+ ),
+ ControlGroup(
+ title: 'Symbol Behavior',
+ children: [
+ SwitchListTile(
+ title: const Text('Allow Overlap'),
+ subtitle: const Text('Allow symbols to overlap'),
+ value: _textAllowOverlap,
+ onChanged: (value) async {
+ setState(() => _textAllowOverlap = value);
+ await _updateLayer();
+ },
+ ),
+ SwitchListTile(
+ title: const Text('Ignore Placement'),
+ subtitle: const Text('Ignore collision detection'),
+ value: _textIgnorePlacement,
+ onChanged: (value) async {
+ setState(() => _textIgnorePlacement = value);
+ await _updateLayer();
+ },
+ ),
+ SwitchListTile(
+ title: const Text('Avoid Edges'),
+ subtitle: const Text('Keep symbols from map edges'),
+ value: _symbolAvoidEdges,
+ onChanged: (value) async {
+ setState(() => _symbolAvoidEdges = value);
+ await _updateLayer();
+ },
+ ),
+ ListTile(
+ title: const Text('Placement Type'),
+ subtitle: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text('Point or line placement'),
+ const SizedBox(height: 8),
+ ExampleSegmentedButton(
+ segments: const [
+ ExampleSegment(
+ value: 'point',
+ label: 'Point',
+ ),
+ ExampleSegment(
+ value: 'line',
+ label: 'Line',
+ ),
+ ],
+ selected: _symbolPlacement,
+ onSelectionChanged: (value) async {
+ setState(() => _symbolPlacement = value);
+ await _updateLayer();
+ },
+ ),
+ ],
+ ),
+ ),
+ if (_symbolPlacement == 'line')
+ ListTile(
+ title: Text(
+ 'Symbol Spacing: ${_symbolSpacing.toStringAsFixed(0)} px'),
+ subtitle: Slider(
+ value: _symbolSpacing,
+ min: 50.0,
+ max: 500.0,
+ divisions: 45,
+ onChanged: (value) async {
+ setState(() => _symbolSpacing = value);
+ await _updateLayer();
+ },
+ ),
+ ),
+ ],
+ ),
+ ControlGroup(
+ title: 'Actions',
+ children: [
+ ExampleButton(
+ label: 'Reset Properties',
+ onPressed: () async {
+ setState(() {
+ // Icon properties
+ _showIcon = true;
+ _iconSize = 1.0;
+ _iconRotate = 0.0;
+ _iconOffsetX = 0.0;
+ _iconOffsetY = -1.5;
+ _iconAllowOverlap = false;
+ _iconIgnorePlacement = false;
+ // Text properties
+ _textSize = 14.0;
+ _textColor = const Color(0xFF2C3E50);
+ _textOpacity = 1.0;
+ _textHaloWidth = 2.0;
+ _textHaloColor = Colors.white;
+ _textHaloBlur = 1.0;
+ _textRotate = 0.0;
+ _textOffsetX = 0.0;
+ _textOffsetY = 0.0;
+ _textAnchor = 'center';
+ _textJustify = 'center';
+ _textAllowOverlap = false;
+ _textIgnorePlacement = false;
+ _symbolPlacement = 'point';
+ _symbolSpacing = 250.0;
+ _symbolAvoidEdges = false;
+ });
+ await _updateLayer();
+ },
+ ),
+ ],
+ ),
+ ],
+ );
+ }
+
+ Future _pickColor(bool isTextColor) async {
+ final currentColor = isTextColor ? _textColor : _textHaloColor;
+ final title = isTextColor ? 'Select Text Color' : 'Select Halo Color';
+
+ final selectedColor = await ColorPickerModal.show(
+ context: context,
+ title: title,
+ currentColor: currentColor,
+ );
+
+ if (selectedColor != null) {
+ setState(() {
+ if (isTextColor) {
+ _textColor = selectedColor;
+ } else {
+ _textHaloColor = selectedColor;
+ }
+ });
+ await _updateLayer();
+ }
+ }
+}
diff --git a/maplibre_gl_example/lib/sources.dart b/maplibre_gl_example/lib/examples/layers/various_sources.dart
similarity index 98%
rename from maplibre_gl_example/lib/sources.dart
rename to maplibre_gl_example/lib/examples/layers/various_sources.dart
index 4ea55ed1e..6ca3a6dda 100644
--- a/maplibre_gl_example/lib/sources.dart
+++ b/maplibre_gl_example/lib/examples/layers/various_sources.dart
@@ -2,7 +2,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
-import 'page.dart';
+import '../../page.dart';
class StyleInfo {
final String name;
@@ -17,8 +17,10 @@ class StyleInfo {
required this.position});
}
-class Sources extends ExamplePage {
- const Sources({super.key}) : super(const Icon(Icons.map), 'Various Sources');
+class VariousSources extends ExamplePage {
+ const VariousSources({super.key})
+ : super(const Icon(Icons.map), 'Various Sources',
+ category: ExampleCategory.layers);
@override
Widget build(BuildContext context) {
diff --git a/maplibre_gl_example/lib/full_map.dart b/maplibre_gl_example/lib/full_map.dart
deleted file mode 100644
index 729b95813..000000000
--- a/maplibre_gl_example/lib/full_map.dart
+++ /dev/null
@@ -1,53 +0,0 @@
-import 'dart:async';
-
-import 'package:flutter/material.dart';
-import 'package:maplibre_gl/maplibre_gl.dart';
-
-import 'page.dart';
-
-const _nullIsland = CameraPosition(target: LatLng(0, 0), zoom: 4.0);
-
-class FullMapPage extends ExamplePage {
- const FullMapPage({super.key})
- : super(const Icon(Icons.map), 'Full screen map');
-
- @override
- Widget build(BuildContext context) {
- return const FullMap();
- }
-}
-
-class FullMap extends StatefulWidget {
- const FullMap({super.key});
-
- @override
- State createState() => FullMapState();
-}
-
-class FullMapState extends State {
- final Completer mapController = Completer();
- bool canInteractWithMap = false;
-
- @override
- Widget build(BuildContext context) {
- return Scaffold(
- floatingActionButtonLocation:
- FloatingActionButtonLocation.miniCenterFloat,
- floatingActionButton: canInteractWithMap
- ? FloatingActionButton(
- onPressed: _moveCameraToNullIsland,
- mini: true,
- child: const Icon(Icons.restore),
- )
- : null,
- body: MapLibreMap(
- onMapCreated: (controller) => mapController.complete(controller),
- initialCameraPosition: _nullIsland,
- onStyleLoadedCallback: () => setState(() => canInteractWithMap = true),
- ),
- );
- }
-
- void _moveCameraToNullIsland() => mapController.future.then(
- (c) => c.animateCamera(CameraUpdate.newCameraPosition(_nullIsland)));
-}
diff --git a/maplibre_gl_example/lib/get_map_informations.dart b/maplibre_gl_example/lib/get_map_informations.dart
deleted file mode 100644
index 5819f741d..000000000
--- a/maplibre_gl_example/lib/get_map_informations.dart
+++ /dev/null
@@ -1,120 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:maplibre_gl/maplibre_gl.dart';
-
-import 'page.dart';
-
-class GetMapInfoPage extends ExamplePage {
- const GetMapInfoPage({super.key})
- : super(const Icon(Icons.info), 'Get map state');
-
- @override
- Widget build(BuildContext context) {
- return const GetMapInfoBody();
- }
-}
-
-class GetMapInfoBody extends StatefulWidget {
- const GetMapInfoBody({super.key});
-
- @override
- State createState() => _GetMapInfoBodyState();
-}
-
-class _GetMapInfoBodyState extends State {
- MapLibreMapController? controller;
- String data = '';
-
- void onMapCreated(MapLibreMapController controller) {
- setState(() {
- this.controller = controller;
- });
- }
-
- Future displaySources() async {
- if (controller == null) {
- return;
- }
- final sources = await controller!.getSourceIds();
- setState(() {
- data = 'Sources: ${sources.map((e) => '"$e"').join(', ')}';
- });
- }
-
- Future displayLayers() async {
- if (controller == null) {
- return;
- }
- final layers = (await controller!.getLayerIds()).cast();
- setState(() {
- data = 'Layers: ${layers.map((e) => '"$e"').join(', ')}';
- });
- }
-
- @override
- Widget build(BuildContext context) {
- return Column(
- mainAxisAlignment: MainAxisAlignment.spaceEvenly,
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- Center(
- child: SizedBox(
- width: 300.0,
- height: 200.0,
- child: MapLibreMap(
- initialCameraPosition: const CameraPosition(
- target: LatLng(-33.852, 151.211),
- zoom: 11.0,
- ),
- onMapCreated: onMapCreated,
- compassEnabled: false,
- annotationOrder: const [],
- myLocationEnabled: false,
- styleString: '''{
- "version": 8,
- "sources": {
- "OSM": {
- "type": "raster",
- "tiles": [
- "https://a.tile.openstreetmap.org/{z}/{x}/{y}.png",
- "https://b.tile.openstreetmap.org/{z}/{x}/{y}.png",
- "https://c.tile.openstreetmap.org/{z}/{x}/{y}.png"
- ],
- "tileSize": 256,
- "attribution": "© OpenStreetMap contributors",
- "maxzoom": 18
- }
- },
- "layers": [
- {
- "id": "OSM-layer",
- "source": "OSM",
- "type": "raster"
- }
- ]
- }''',
- ),
- ),
- ),
- const Center(child: Text('© OpenStreetMap contributors')),
- Expanded(
- child: SingleChildScrollView(
- child: Column(
- children: [
- const SizedBox(height: 30),
- Center(child: Text(data)),
- const SizedBox(height: 30),
- ElevatedButton(
- onPressed: controller == null ? null : displayLayers,
- child: const Text('Get map layers'),
- ),
- ElevatedButton(
- onPressed: controller == null ? null : displaySources,
- child: const Text('Get map sources'),
- )
- ],
- ),
- )),
- ],
- );
- }
-}
diff --git a/maplibre_gl_example/lib/given_bounds.dart b/maplibre_gl_example/lib/given_bounds.dart
deleted file mode 100644
index ba3f277ee..000000000
--- a/maplibre_gl_example/lib/given_bounds.dart
+++ /dev/null
@@ -1,77 +0,0 @@
-// Copyright 2018 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-import 'package:flutter/material.dart';
-import 'package:maplibre_gl/maplibre_gl.dart';
-
-import 'page.dart';
-
-class GivenBoundsPage extends ExamplePage {
- const GivenBoundsPage({super.key})
- : super(const Icon(Icons.map_sharp), 'Changing given bounds');
-
- @override
- Widget build(BuildContext context) {
- return const GivenBounds();
- }
-}
-
-class GivenBounds extends StatefulWidget {
- const GivenBounds({super.key});
-
- @override
- State createState() => GivenBoundsState();
-}
-
-class GivenBoundsState extends State {
- late MapLibreMapController mapController;
-
- void _onMapCreated(MapLibreMapController controller) {
- mapController = controller;
- }
-
- @override
- Widget build(BuildContext context) {
- final width = MediaQuery.of(context).size.width;
- final height = MediaQuery.of(context).size.height;
-
- return Column(
- children: [
- SizedBox(
- width: width,
- height: height * 0.5,
- child: MapLibreMap(
- onMapCreated: _onMapCreated,
- initialCameraPosition:
- const CameraPosition(target: LatLng(0.0, 0.0)),
- ),
- ),
- TextButton(
- onPressed: () async {
- await mapController.setCameraBounds(
- west: 5.98865807458,
- south: 47.3024876979,
- east: 15.0169958839,
- north: 54.983104153,
- padding: 25,
- );
- },
- child: const Text('Set bounds to Germany'),
- ),
- TextButton(
- onPressed: () async {
- await mapController.setCameraBounds(
- west: -18,
- south: -40,
- east: 54,
- north: 40,
- padding: 25,
- );
- },
- child: const Text('Set bounds to Africa'),
- ),
- ],
- );
- }
-}
diff --git a/maplibre_gl_example/lib/layer.dart b/maplibre_gl_example/lib/layer.dart
deleted file mode 100644
index d7eb8e4ab..000000000
--- a/maplibre_gl_example/lib/layer.dart
+++ /dev/null
@@ -1,483 +0,0 @@
-import 'dart:async';
-import 'dart:math';
-
-import 'package:flutter/material.dart';
-import 'package:maplibre_gl/maplibre_gl.dart';
-import 'package:maplibre_gl_example/page.dart';
-
-import 'util.dart';
-
-class LayerPage extends ExamplePage {
- const LayerPage({super.key}) : super(const Icon(Icons.share), 'Layer');
-
- @override
- Widget build(BuildContext context) => const LayerBody();
-}
-
-class LayerBody extends StatefulWidget {
- const LayerBody({super.key});
-
- @override
- State createState() => LayerState();
-}
-
-class LayerState extends State {
- static const LatLng center = LatLng(-33.86711, 151.1947171);
-
- late MapLibreMapController controller;
- Timer? bikeTimer;
- Timer? filterTimer;
- int filteredId = 0;
- bool linesVisible = true;
- bool fillsVisible = true;
- bool symbolsVisible = true;
- bool circlesVisible = true;
- bool linesRed = false;
- bool fillsRed = true;
- bool symbolsRed = false;
- bool circlesRed = false;
-
- @override
- Widget build(BuildContext context) {
- final width = MediaQuery.of(context).size.width;
- final height = MediaQuery.of(context).size.height;
-
- return Column(
- children: [
- SizedBox(
- width: width,
- height: height * 0.5,
- child: MapLibreMap(
- dragEnabled: false,
- myLocationEnabled: true,
- onMapCreated: _onMapCreated,
- onMapClick: (point, latLong) {
- debugPrint(point.toString() + latLong.toString());
- },
- onStyleLoadedCallback: _onStyleLoadedCallback,
- initialCameraPosition: const CameraPosition(
- target: center,
- zoom: 11.0,
- ),
- )),
- Expanded(
- child: SingleChildScrollView(
- child: Padding(
- padding: const EdgeInsets.all(8.0),
- child: Wrap(
- spacing: 4.0,
- runSpacing: 4.0,
- alignment: WrapAlignment.center,
- children: [
- TextButton(
- onPressed: () async {
- await controller
- .setLayerProperties(
- "lines",
- LineLayerProperties.fromJson(
- {"visibility": linesVisible ? "none" : "visible"},
- ),
- )
- .then(
- (value) =>
- setState(() => linesVisible = !linesVisible),
- );
- },
- child: const Text('toggle line visibility'),
- ),
- TextButton(
- onPressed: () async {
- await controller
- .setLayerProperties(
- "lines",
- LineLayerProperties.fromJson(
- {"line-color": linesRed ? "#0000ff" : "#ff0000"},
- ),
- )
- .then(
- (value) => setState(() => linesRed = !linesRed));
- },
- child: const Text('toggle line color'),
- ),
- TextButton(
- onPressed: () async {
- await controller
- .setLayerProperties(
- "fills",
- FillLayerProperties.fromJson(
- {"visibility": fillsVisible ? "none" : "visible"},
- ),
- )
- .then(
- (value) =>
- setState(() => fillsVisible = !fillsVisible),
- );
- },
- child: const Text('toggle fill visibility'),
- ),
- TextButton(
- onPressed: () async {
- await controller
- .setLayerProperties(
- "fills",
- FillLayerProperties.fromJson(
- {"fill-color": fillsRed ? "#0000ff" : "#ff0000"},
- ),
- )
- .then(
- (value) => setState(() => fillsRed = !fillsRed),
- );
- },
- child: const Text('toggle fill color'),
- ),
- TextButton(
- onPressed: () async {
- await controller
- .setLayerProperties(
- "circles",
- CircleLayerProperties.fromJson(
- {
- "visibility":
- circlesVisible ? "none" : "visible"
- },
- ),
- )
- .then(
- (value) => setState(
- () => circlesVisible = !circlesVisible),
- );
- },
- child: const Text('toggle circle visibility'),
- ),
- TextButton(
- onPressed: () async {
- await controller
- .setLayerProperties(
- "circles",
- CircleLayerProperties.fromJson(
- {
- "circle-color":
- circlesRed ? "#0000ff" : "#ff0000"
- },
- ),
- )
- .then(
- (value) => setState(() => circlesRed = !circlesRed),
- );
- },
- child: const Text('toggle circle color'),
- ),
- TextButton(
- onPressed: () async {
- await controller
- .setLayerProperties(
- "symbols",
- SymbolLayerProperties.fromJson(
- {
- "visibility":
- symbolsVisible ? "none" : "visible"
- },
- ),
- )
- .then(
- (value) => setState(
- () => symbolsVisible = !symbolsVisible),
- );
- },
- child: const Text('toggle (non-moving) symbols visibility'),
- ),
- ],
- ),
- ),
- ),
- ),
- ],
- );
- }
-
- void _onMapCreated(MapLibreMapController controller) {
- this.controller = controller;
- controller.onFeatureTapped.add(onFeatureTap);
- }
-
- void onFeatureTap(
- Point point,
- LatLng latLng,
- String id,
- String layerId,
- Annotation? annotation,
- ) {
- final snackBar = SnackBar(
- content: Text(
- 'Tapped feature with id $id on layer $layerId at $latLng.\nAnnotation is ${annotation != null ? 'present' : 'null'}',
- style: const TextStyle(fontSize: 14),
- ),
- backgroundColor: Theme.of(context).primaryColor,
- );
- ScaffoldMessenger.of(context).clearSnackBars();
- ScaffoldMessenger.of(context).showSnackBar(snackBar);
- }
-
- Future _onStyleLoadedCallback() async {
- await addImageFromAsset(
- controller, "custom-marker", "assets/symbols/custom-marker.png");
- await controller.addGeoJsonSource("points", _points);
- await controller.addGeoJsonSource("moving", _movingFeature(0));
-
- //new style of adding sources
- await controller.addSource("fills", GeojsonSourceProperties(data: _fills));
-
- await controller.addFillLayer(
- "fills",
- "fills",
- const FillLayerProperties(fillColor: [
- Expressions.interpolate,
- ['exponential', 0.5],
- [Expressions.zoom],
- 11,
- 'red',
- 18,
- 'green'
- ], fillOpacity: 0.4),
- filter: ['==', 'id', filteredId],
- );
-
- await controller.addFillExtrusionLayer(
- "fills",
- "fills-extrusion",
- const FillExtrusionLayerProperties(
- fillExtrusionHeight: 300,
- fillExtrusionColor: [
- Expressions.interpolate,
- ['exponential', 0.5],
- [Expressions.zoom],
- 11,
- 'red',
- 18,
- 'blue'
- ],
- ),
- belowLayerId: "water",
- filter: ['==', 'id', 2],
- );
-
- await controller.addLineLayer(
- "fills",
- "lines",
- LineLayerProperties(
- lineColor: Colors.lightBlue.toHexStringRGB(),
- lineWidth: [
- Expressions.interpolate,
- ["linear"],
- [Expressions.zoom],
- 11.0,
- 2.0,
- 20.0,
- 10.0
- ]),
- );
-
- await controller.addCircleLayer(
- "fills",
- "circles",
- CircleLayerProperties(
- circleRadius: 4,
- circleColor: Colors.blue.toHexStringRGB(),
- ),
- );
-
- await controller.addSymbolLayer(
- "points",
- "symbols",
- const SymbolLayerProperties(
- iconImage: "custom-marker", // "{type}-15",
- iconSize: 2,
- iconAllowOverlap: true,
- ),
- );
-
- await controller.addSymbolLayer(
- "moving",
- "moving",
- SymbolLayerProperties(
- textField: [Expressions.get, "name"],
- textHaloWidth: 1,
- textSize: 10,
- textHaloColor: Colors.white.toHexStringRGB(),
- textOffset: [
- Expressions.literal,
- [0, 2]
- ],
- iconImage: "custom-marker",
- // "bicycle-15",
- iconSize: 2,
- iconAllowOverlap: true,
- textAllowOverlap: true,
- ),
- minzoom: 11,
- );
-
- bikeTimer = Timer.periodic(const Duration(milliseconds: 10), (t) async {
- await controller.setGeoJsonSource(
- "moving", _movingFeature(t.tick / 2000));
- });
-
- filterTimer = Timer.periodic(const Duration(seconds: 3), (t) async {
- filteredId = filteredId == 0 ? 1 : 0;
- await controller.setFilter('fills', ['==', 'id', filteredId]);
- });
- }
-
- @override
- void dispose() {
- bikeTimer?.cancel();
- filterTimer?.cancel();
- super.dispose();
- }
-}
-
-Map _movingFeature(double t) {
- List makeLatLong(double t) {
- final angle = t * 2 * pi;
- const r = 0.025;
- const centerX = 151.1849;
- const centerY = -33.8748;
- return [
- centerX + r * sin(angle),
- centerY + r * cos(angle),
- ];
- }
-
- return {
- "type": "FeatureCollection",
- "features": [
- {
- "type": "Feature",
- "properties": {"name": "POGAČAR Tadej"},
- "id": 10,
- "geometry": {"type": "Point", "coordinates": makeLatLong(t)}
- },
- {
- "type": "Feature",
- "properties": {"name": "VAN AERT Wout"},
- "id": 11,
- "geometry": {"type": "Point", "coordinates": makeLatLong(t + 0.15)}
- },
- ]
- };
-}
-
-final Map _fills = {
- "type": "FeatureCollection",
- "features": [
- {
- "type": "Feature",
- "id": 0, // web currently only supports number ids
- "properties": {'id': 0},
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [151.178099204457737, -33.901517742631846],
- [151.179025547977773, -33.872845324482071],
- [151.147000529140399, -33.868230472039514],
- [151.150838238009328, -33.883172899638311],
- [151.14223647675135, -33.894158309528244],
- [151.155999294764086, -33.904812805307806],
- [151.178099204457737, -33.901517742631846]
- ],
- [
- [151.162657925954278, -33.879168932438581],
- [151.155323416087612, -33.890737666431583],
- [151.173659690754278, -33.897637567778119],
- [151.162657925954278, -33.879168932438581]
- ]
- ]
- }
- },
- {
- "type": "Feature",
- "id": 2,
- "properties": {'id': 2},
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [151.121824791363, -33.885947459842846],
- [151.121824791363, -33.89768020458625],
- [151.13561641336742, -33.89768020458625],
- [151.13561641336742, -33.885947459842846],
- [151.121824791363, -33.885947459842846]
- ]
- ],
- }
- },
- {
- "type": "Feature",
- "id": 1,
- "properties": {'id': 1},
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [151.18735077583878, -33.891143558434102],
- [151.197374605989864, -33.878357032551868],
- [151.213021560372084, -33.886475683791488],
- [151.204953599518745, -33.899463918807818],
- [151.18735077583878, -33.891143558434102]
- ]
- ]
- }
- }
- ]
-};
-
-const Map _points = {
- "type": "FeatureCollection",
- "features": [
- {
- "type": "Feature",
- "id": 2,
- "properties": {
- "type": "restaurant",
- },
- "geometry": {
- "type": "Point",
- "coordinates": [151.184913929732943, -33.874874486427181]
- }
- },
- {
- "type": "Feature",
- "id": 3,
- "properties": {
- "type": "airport",
- },
- "geometry": {
- "type": "Point",
- "coordinates": [151.215730044667879, -33.874616048776858]
- }
- },
- {
- "type": "Feature",
- "id": 4,
- "properties": {
- "type": "bakery",
- },
- "geometry": {
- "type": "Point",
- "coordinates": [151.228803547973598, -33.892188026142584]
- }
- },
- {
- "type": "Feature",
- "id": 5,
- "properties": {
- "type": "college",
- },
- "geometry": {
- "type": "Point",
- "coordinates": [151.186470299174118, -33.902781145804774]
- }
- }
- ]
-};
diff --git a/maplibre_gl_example/lib/layer_manipulation.dart b/maplibre_gl_example/lib/layer_manipulation.dart
deleted file mode 100644
index 7af7d1339..000000000
--- a/maplibre_gl_example/lib/layer_manipulation.dart
+++ /dev/null
@@ -1,330 +0,0 @@
-import 'dart:async';
-import 'dart:math';
-
-import 'package:flutter/foundation.dart';
-import 'package:flutter/material.dart';
-import 'package:maplibre_gl/maplibre_gl.dart';
-
-import 'page.dart';
-
-class LayerManipulationPage extends ExamplePage {
- const LayerManipulationPage({super.key})
- : super(const Icon(Icons.layers), 'Layer Manipulation');
-
- @override
- Widget build(BuildContext context) {
- return const LayerManipulation();
- }
-}
-
-class LayerManipulation extends StatefulWidget {
- const LayerManipulation({super.key});
-
- @override
- State createState() => _LayerManipulationState();
-}
-
-class _LayerManipulationState extends State {
- MapLibreMapController? mapController;
- String currentStyle = '';
- int currentFilterId = 0;
- bool isGeoJsonSourceMode = true;
- int animationStep = 0;
- Timer? _animationTimer;
- bool _isAnimating = false;
-
- // Sample GeoJSON data for editGeoJsonSource
- final Map _sampleGeoJsonData = {
- "type": "FeatureCollection",
- "features": [
- {
- "type": "Feature",
- "geometry": {
- "type": "Point",
- "coordinates": [14.363610, 46.233487, 0.0]
- }
- },
- ]
- };
-
- // Animated GeoJSON data that changes over time
- Map _getAnimatedGeoJsonData(int step) {
- final baseData = Map.from(_sampleGeoJsonData);
- final features = List.from(baseData['features']);
-
- for (var i = 0; i < features.length; i++) {
- final feature = features[i];
- final coords = List.from(feature['geometry']['coordinates']);
- final angle = (step * 0.1) + (i * 0.5);
- final radius = 0.5 + (i * 0.1);
-
- coords[0] = coords[0] + radius * cos(angle);
- coords[1] = coords[1] + radius * sin(angle);
-
- feature['geometry']['coordinates'] = coords;
- }
-
- baseData['features'] = features;
- return baseData;
- }
-
- void _onMapCreated(MapLibreMapController controller) {
- mapController = controller;
- }
-
- Future _onStyleLoadedCallback() async {
- if (mapController == null) return;
-
- // Add a GeoJSON source
- await mapController!.addSource(
- "sample-data",
- GeojsonSourceProperties(data: _sampleGeoJsonData),
- );
-
- // Add a circle layer
- await mapController!.addCircleLayer(
- "sample-data",
- "sample-circles",
- const CircleLayerProperties(
- circleRadius: 8,
- circleColor: "#ff0000",
- circleStrokeColor: "#ffffff",
- circleStrokeWidth: 2,
- ),
- );
-
- // Add a symbol layer for labels
- await mapController!.addSymbolLayer(
- "sample-data",
- "sample-labels",
- const SymbolLayerProperties(
- textField: ["get", "name"],
- textFont: ["Open Sans Regular"],
- textSize: 12,
- textColor: "#000000",
- textHaloColor: "#ffffff",
- textHaloWidth: 1,
- ),
- );
-
- // Get and display current style
- await _getCurrentStyle();
- }
-
- Future _getCurrentStyle() async {
- if (mapController == null) return;
-
- try {
- final style = await mapController!.getStyle();
- setState(() {
- currentStyle = style ?? 'No style available';
- });
- } catch (e) {
- setState(() {
- currentStyle = 'Error getting style: $e';
- });
- }
- }
-
- void _toggleAnimation() {
- if (_isAnimating) {
- _stopAnimation();
- } else {
- _startAnimation();
- }
- }
-
- void _startAnimation() {
- if (_animationTimer != null) return;
-
- setState(() {
- isGeoJsonSourceMode = true;
- _isAnimating = true;
- });
-
- _animationTimer =
- Timer.periodic(const Duration(milliseconds: 100), (timer) async {
- if (mapController != null) {
- final newData = _getAnimatedGeoJsonData(animationStep);
- await mapController!.setGeoJsonSource("sample-data", newData);
-
- setState(() {
- animationStep++;
- });
- }
- });
- }
-
- void _stopAnimation() {
- _animationTimer?.cancel();
- _animationTimer = null;
-
- setState(() {
- _isAnimating = false;
- });
- }
-
- Future _editGeoJsonUrl() async {
- if (mapController == null) return;
-
- // Stop animation when switching to URL mode
- _stopAnimation();
-
- setState(() {
- isGeoJsonSourceMode = false;
- });
-
- // Use a public GeoJSON URL (earthquakes data)
- await mapController!.editGeoJsonUrl(
- "sample-data",
- "https://docs.mapbox.com/mapbox-gl-js/assets/earthquakes.geojson",
- );
- }
-
- Future _setLayerFilter() async {
- if (mapController == null) return;
-
- // Cycle through different filters
- final filters = [
- '["all", ["==", "name", "International Date Line"]]', // Don't show International Date Line
- '["all", ["!=", "name", "International Date Line"]]', // Do show International Date Line
- ];
-
- final filter = filters[currentFilterId % filters.length];
- await mapController!.setLayerFilter("countries-fill", filter);
-
- setState(() {
- currentFilterId++;
- });
- }
-
- @override
- void dispose() {
- _animationTimer?.cancel();
- super.dispose();
- }
-
- @override
- Widget build(BuildContext context) {
- return Scaffold(
- body: Column(
- children: [
- // Map
- Expanded(
- flex: 3,
- child: MapLibreMap(
- styleString: MapLibreStyles.demo,
- onMapCreated: _onMapCreated,
- onStyleLoadedCallback: _onStyleLoadedCallback,
- initialCameraPosition: const CameraPosition(
- target: LatLng(50.0, 10.0),
- zoom: 3.0,
- ),
- ),
- ),
-
- // Controls
- Expanded(
- flex: 2,
- child: SingleChildScrollView(
- padding: const EdgeInsets.all(16.0),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- 'Layer Manipulation Demo',
- style: Theme.of(context).textTheme.headlineSmall,
- ),
- const SizedBox(height: 16),
-
- // Control buttons
- Wrap(
- spacing: 8.0,
- runSpacing: 8.0,
- children: [
- ElevatedButton(
- onPressed: _toggleAnimation,
- style: ElevatedButton.styleFrom(
- backgroundColor:
- _isAnimating ? Colors.red : Colors.green,
- foregroundColor: Colors.white,
- ),
- child: Text(_isAnimating
- ? 'Stop Animation'
- : 'Start Animation'),
- ),
- if (!kIsWeb) ...[
- ElevatedButton(
- onPressed: _editGeoJsonUrl,
- child: const Text('Show Earthquakes'),
- ),
- ElevatedButton(
- onPressed: _setLayerFilter,
- child: const Text('Toggle Country Fill'),
- ),
- ElevatedButton(
- onPressed: _getCurrentStyle,
- child: const Text('Get Style'),
- ),
- ],
- ],
- ),
-
- const SizedBox(height: 16),
-
- // Status information
- Container(
- padding: const EdgeInsets.all(8.0),
- decoration: BoxDecoration(
- color: Colors.grey[100],
- borderRadius: BorderRadius.circular(4.0),
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- 'Current Mode: ${isGeoJsonSourceMode ? "GeoJSON Source" : "GeoJSON URL"}',
- style: const TextStyle(fontWeight: FontWeight.bold),
- ),
- Text('Filter ID: $currentFilterId'),
- Text('Animation Step: $animationStep'),
- Text(
- 'Animation Status: ${_isAnimating ? "Running" : "Stopped"}'),
- ],
- ),
- ),
-
- const SizedBox(height: 8),
-
- // Style information
- if (currentStyle.isNotEmpty) ...[
- const Text(
- 'Current Style:',
- style: TextStyle(fontWeight: FontWeight.bold),
- ),
- Container(
- height: 100,
- padding: const EdgeInsets.all(8.0),
- decoration: BoxDecoration(
- color: Colors.grey[50],
- border: Border.all(color: Colors.grey[300]!),
- borderRadius: BorderRadius.circular(4.0),
- ),
- child: SingleChildScrollView(
- child: Text(
- currentStyle,
- style: const TextStyle(
- fontFamily: 'monospace', fontSize: 10),
- ),
- ),
- ),
- ],
- ],
- ),
- ),
- ),
- ],
- ),
- );
- }
-}
diff --git a/maplibre_gl_example/lib/line.dart b/maplibre_gl_example/lib/line.dart
deleted file mode 100644
index 3ff8b1872..000000000
--- a/maplibre_gl_example/lib/line.dart
+++ /dev/null
@@ -1,251 +0,0 @@
-// Copyright 2018 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-import 'dart:async';
-import 'dart:math';
-
-import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
-import 'package:maplibre_gl/maplibre_gl.dart';
-
-import 'page.dart';
-
-class LinePage extends ExamplePage {
- const LinePage({super.key}) : super(const Icon(Icons.share), 'Line');
-
- @override
- Widget build(BuildContext context) {
- return const LineBody();
- }
-}
-
-class LineBody extends StatefulWidget {
- const LineBody({super.key});
-
- @override
- State createState() => LineBodyState();
-}
-
-class LineBodyState extends State {
- LineBodyState();
-
- static const LatLng center = LatLng(-33.86711, 151.1947171);
-
- MapLibreMapController? controller;
- int _lineCount = 0;
- Line? _selectedLine;
- final String _linePatternImage = "assets/fill/cat_silhouette_pattern.png";
-
- void _onMapCreated(MapLibreMapController controller) {
- this.controller = controller;
- controller.onLineTapped.add(_onLineTapped);
- controller.onFeatureHover.add(_onFeatureHover);
- }
-
- @override
- void dispose() {
- controller?.onLineTapped.remove(_onLineTapped);
- controller?.onFeatureHover.remove(_onFeatureHover);
- super.dispose();
- }
-
- /// Adds an asset image to the currently displayed style
- Future addImageFromAsset(String name, String assetName) async {
- final bytes = await rootBundle.load(assetName);
- final list = bytes.buffer.asUint8List();
- return controller!.addImage(name, list);
- }
-
- Future _onLineTapped(Line line) async {
- await _updateSelectedLine(
- const LineOptions(lineColor: "#ff0000"),
- );
- setState(() {
- _selectedLine = line;
- });
- await _updateSelectedLine(
- const LineOptions(lineColor: "#ffe100"),
- );
- }
-
- Future _onFeatureHover(
- Point point,
- LatLng latLng,
- String id,
- Annotation? annotation,
- HoverEventType eventType,
- ) async {
- if (annotation is! Line) return;
- if (eventType == HoverEventType.enter) {
- controller!.updateLine(
- annotation,
- const LineOptions(
- lineWidth: 16,
- lineColor: "#8B0000",
- ));
- } else if (eventType == HoverEventType.leave) {
- controller!.updateLine(
- annotation,
- const LineOptions(
- lineWidth: 14,
- lineColor: "#ff0000",
- ));
- }
- }
-
- Future _updateSelectedLine(LineOptions changes) async {
- if (_selectedLine != null) controller!.updateLine(_selectedLine!, changes);
- }
-
- Future _add() async {
- await controller!.addLine(
- const LineOptions(
- geometry: [
- LatLng(-33.86711, 151.1947171),
- LatLng(-33.86711, 151.1947171),
- LatLng(-32.86711, 151.1947171),
- LatLng(-33.86711, 152.1947171),
- ],
- lineColor: "#ff0000",
- lineWidth: 14.0,
- lineOpacity: 0.5,
- draggable: true),
- );
- setState(() {
- _lineCount += 1;
- });
- }
-
- Future _move() async {
- final currentStart = _selectedLine!.options.geometry![0];
- final currentEnd = _selectedLine!.options.geometry![1];
- final end =
- LatLng(currentEnd.latitude + 0.001, currentEnd.longitude + 0.001);
- final start =
- LatLng(currentStart.latitude - 0.001, currentStart.longitude - 0.001);
- await controller!
- .updateLine(_selectedLine!, LineOptions(geometry: [start, end]));
- }
-
- Future _remove() async {
- await controller!.removeLine(_selectedLine!);
- setState(() {
- _selectedLine = null;
- _lineCount -= 1;
- });
- }
-
- Future _changeLinePattern() async {
- final current =
- _selectedLine!.options.linePattern == null ? "assetImage" : null;
- await _updateSelectedLine(
- LineOptions(linePattern: current),
- );
- }
-
- Future _changeAlpha() async {
- var current = _selectedLine!.options.lineOpacity;
- current ??= 1.0;
-
- await _updateSelectedLine(
- LineOptions(lineOpacity: current < 0.1 ? 1.0 : current * 0.75),
- );
- }
-
- Future _toggleVisible() async {
- var current = _selectedLine!.options.lineOpacity;
- current ??= 1.0;
- await _updateSelectedLine(
- LineOptions(lineOpacity: current == 0.0 ? 1.0 : 0.0),
- );
- }
-
- Future _onStyleLoadedCallback() async {
- addImageFromAsset("assetImage", _linePatternImage);
- await controller!.addLine(
- const LineOptions(
- geometry: [LatLng(37.4220, -122.0841), LatLng(37.4240, -122.0941)],
- lineColor: "#ff0000",
- lineWidth: 14.0,
- lineOpacity: 0.5,
- ),
- );
- }
-
- @override
- Widget build(BuildContext context) {
- final width = MediaQuery.of(context).size.width;
- final height = MediaQuery.of(context).size.height;
-
- return Column(
- children: [
- SizedBox(
- width: width,
- height: height * 0.5,
- child: MapLibreMap(
- onMapCreated: _onMapCreated,
- onStyleLoadedCallback: _onStyleLoadedCallback,
- initialCameraPosition: const CameraPosition(
- target: LatLng(-33.852, 151.211),
- zoom: 11.0,
- ),
- ),
- ),
- Expanded(
- child: SingleChildScrollView(
- child: Padding(
- padding: const EdgeInsets.all(8.0),
- child: Wrap(
- spacing: 4.0,
- runSpacing: 4.0,
- alignment: WrapAlignment.center,
- children: [
- TextButton(
- onPressed: (_lineCount == 12) ? null : _add,
- child: const Text('add'),
- ),
- TextButton(
- onPressed: (_selectedLine == null) ? null : _remove,
- child: const Text('remove'),
- ),
- TextButton(
- onPressed: (_selectedLine == null)
- ? null
- : () async {
- await _move();
- },
- child: const Text('move'),
- ),
- TextButton(
- onPressed:
- (_selectedLine == null) ? null : _changeLinePattern,
- child: const Text('change line-pattern'),
- ),
- TextButton(
- onPressed: (_selectedLine == null) ? null : _changeAlpha,
- child: const Text('change alpha'),
- ),
- TextButton(
- onPressed: (_selectedLine == null) ? null : _toggleVisible,
- child: const Text('toggle visible'),
- ),
- TextButton(
- onPressed: (_selectedLine == null)
- ? null
- : () {
- final latLngs =
- controller!.getLineLatLngs(_selectedLine!);
- debugPrint('Current geometry: $latLngs');
- },
- child: const Text('print current LatLng'),
- ),
- ],
- ),
- ),
- ),
- ),
- ],
- );
- }
-}
diff --git a/maplibre_gl_example/lib/localized_map.dart b/maplibre_gl_example/lib/localized_map.dart
deleted file mode 100644
index cd80d9995..000000000
--- a/maplibre_gl_example/lib/localized_map.dart
+++ /dev/null
@@ -1,78 +0,0 @@
-import 'dart:async';
-
-import 'package:flutter/material.dart';
-import 'package:maplibre_gl/maplibre_gl.dart';
-
-import 'page.dart';
-
-class LocalizedMapPage extends ExamplePage {
- const LocalizedMapPage({super.key})
- : super(const Icon(Icons.map), 'Localized screen map');
-
- @override
- Widget build(BuildContext context) {
- return const LocalizedMap();
- }
-}
-
-class LocalizedMap extends StatefulWidget {
- const LocalizedMap({super.key});
-
- @override
- State createState() => LocalizedMapState();
-}
-
-class LocalizedMapState extends State {
- final _mapReadyCompleter = Completer();
-
- var _mapLanguage = "en";
-
- @override
- Widget build(BuildContext context) {
- return Scaffold(
- body: Column(
- children: [
- DropdownButton(
- value: _mapLanguage,
- icon: const Icon(Icons.arrow_drop_down),
- elevation: 16,
- onChanged: (value) async {
- if (value == null) return;
-
- setState(() => _mapLanguage = value);
- await _setMapLanguage();
- },
- items: ["en", "de", "es", "pl"]
- .map>((String value) {
- return DropdownMenuItem(
- value: value,
- child: Text(value),
- );
- }).toList(),
- ),
- Expanded(
- child: MapLibreMap(
- onMapCreated: _onMapCreated,
- initialCameraPosition:
- const CameraPosition(target: LatLng(0.0, 0.0)),
- onStyleLoadedCallback: _onStyleLoadedCallback,
- ),
- ),
- ],
- ),
- );
- }
-
- void _onMapCreated(MapLibreMapController controller) {
- _mapReadyCompleter.complete(controller);
- }
-
- Future _onStyleLoadedCallback() async {
- await _setMapLanguage();
- }
-
- Future _setMapLanguage() async {
- final controller = await _mapReadyCompleter.future;
- controller.setMapLanguage(_mapLanguage);
- }
-}
diff --git a/maplibre_gl_example/lib/main.dart b/maplibre_gl_example/lib/main.dart
index 00fff994d..bce2815c4 100644
--- a/maplibre_gl_example/lib/main.dart
+++ b/maplibre_gl_example/lib/main.dart
@@ -1,70 +1,110 @@
-// Copyright 2018 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
+import 'dart:async' show unawaited;
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:location/location.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
-import 'package:maplibre_gl_example/attribution.dart';
-import 'package:maplibre_gl_example/get_map_informations.dart';
-import 'package:maplibre_gl_example/given_bounds.dart';
-import 'package:maplibre_gl_example/localized_map.dart';
-import 'package:maplibre_gl_example/no_location_permission_page.dart';
-import 'package:maplibre_gl_example/pmtiles.dart';
-import 'package:maplibre_gl_example/presentation/gps_location/gps_location_page.dart';
-import 'package:maplibre_gl_example/translucent_full_map.dart';
-
-import 'animate_camera.dart';
-import 'annotation_order_maps.dart';
-import 'click_annotations.dart';
-import 'custom_marker.dart';
-import 'full_map.dart';
-import 'layer.dart';
-import 'layer_manipulation.dart';
-import 'line.dart';
-import 'map_ui.dart';
-import 'move_camera.dart';
-import 'offline_regions.dart';
+
+// Page system
import 'page.dart';
-import 'place_batch.dart';
-import 'place_circle.dart';
-import 'place_fill.dart';
-import 'place_source.dart';
-import 'place_symbol.dart';
-import 'scrolling_map.dart';
-import 'sources.dart';
-import 'multi_style_switch.dart';
+
+// Basics examples
+import 'examples/basics/full_map_example.dart';
+import 'examples/basics/multi_style_switch.dart';
+import 'examples/basics/get_map_state.dart';
+import 'examples/basics/gps_location_page.dart';
+
+// Camera examples
+import 'examples/camera/camera_controls_example.dart';
+import 'examples/camera/camera_bounds_example.dart';
+
+// Interaction examples
+import 'examples/interaction/map_controls_example.dart';
+import 'examples/interaction/map_gestures_example.dart';
+
+// Annotations examples
+import 'examples/annotations/annotations_example.dart';
+import 'examples/annotations/annotation_order_example.dart';
+import 'examples/annotations/annotation_properties_example.dart';
+import 'examples/annotations/custom_marker.dart';
+
+// Layers examples
+import 'examples/layers/circle_layer_example.dart';
+import 'examples/layers/fill_layer_example.dart';
+import 'examples/layers/line_layer_example.dart';
+import 'examples/layers/symbol_layer_example.dart';
+import 'examples/layers/various_sources.dart';
+
+// Advanced examples
+import 'examples/advanced/offline_regions.dart';
+import 'examples/advanced/pmtiles.dart';
+import 'examples/advanced/translucent_full_map.dart';
+
+void main() {
+ runApp(const MapLibreExampleApp());
+}
+
+class MapLibreExampleApp extends StatelessWidget {
+ const MapLibreExampleApp({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ title: 'MapLibre Examples',
+ debugShowCheckedModeBanner: false,
+ theme: ThemeData(
+ useMaterial3: true,
+ colorScheme: ColorScheme.fromSeed(
+ seedColor: const Color(0xFF1976D2),
+ brightness: Brightness.light,
+ ),
+ ),
+ darkTheme: ThemeData(
+ useMaterial3: true,
+ colorScheme: ColorScheme.fromSeed(
+ seedColor: const Color(0xFF1976D2),
+ brightness: Brightness.dark,
+ ),
+ ),
+ themeMode: ThemeMode.system,
+ home: const MapsDemo(),
+ );
+ }
+}
final List _allPages = [
- const MapUiPage(),
- const FullMapPage(),
- const TranslucentFullMapPage(),
- const PMTilesPage(),
+ // Basics
+ const FullMapExample(),
const MultiStyleSwitchPage(),
- const LocalizedMapPage(),
- const AnimateCameraPage(),
- const MoveCameraPage(),
- const PlaceSymbolPage(),
- const PlaceSourcePage(),
- const LinePage(),
- const LayerPage(),
- const LayerManipulationPage(),
- const PlaceCirclePage(),
- const PlaceFillPage(),
- const ScrollingMapPage(),
- const OfflineRegionsPage(),
- const AnnotationOrderPage(),
- const CustomMarkerPage(),
- const BatchAddPage(),
- const ClickAnnotationPage(),
- const Sources(),
- const GivenBoundsPage(),
- const GetMapInfoPage(),
- const NoLocationPermissionPage(),
- const AttributionPage(),
const GpsLocationPage(),
+ const GetMapInfoPage(),
+
+ // Camera
+ const CameraControlsExample(),
+ const CameraBoundsExample(),
+
+ // Interaction
+ const MapControlsExample(),
+ const MapGesturesExample(),
+
+ // Annotations
+ const AnnotationsExample(),
+ const AnnotationPropertiesExample(),
+ const AnnotationOrderExample(),
+ const CustomMarkerPage(),
+
+ // Layers
+ const SymbolLayerExample(),
+ const CircleLayerExample(),
+ const FillLayerExample(),
+ const LineLayerExample(),
+ const VariousSources(),
+
+ // Advanced
+ const PMTilesPage(),
+ const OfflineRegionsPage(),
+ const TranslucentFullMapPage(),
];
class MapsDemo extends StatefulWidget {
@@ -75,6 +115,12 @@ class MapsDemo extends StatefulWidget {
}
class _MapsDemoState extends State {
+ @override
+ void initState() {
+ super.initState();
+ unawaited(initHybridComposition());
+ }
+
/// Determine the android version of the phone and turn off HybridComposition
/// on older sdk versions to improve performance for these
///
@@ -109,26 +155,133 @@ class _MapsDemoState extends State {
}
}
+ Map> _groupByCategory() {
+ final grouped = >{};
+ for (final page in _allPages) {
+ grouped.putIfAbsent(page.category, () => []).add(page);
+ }
+ return grouped;
+ }
+
@override
Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ final groupedPages = _groupByCategory();
+
return Scaffold(
- appBar: AppBar(title: const Text('MapLibre examples')),
- body: ListView.builder(
- itemCount: _allPages.length + 1,
- itemBuilder: (_, int index) => index == _allPages.length
- ? const AboutListTile(
- applicationName: "flutter-maplibre-gl example",
- )
- : ListTile(
- leading: _allPages[index].leading,
- title: Text(_allPages[index].title),
- onTap: () => _pushPage(context, _allPages[index]),
+ body: CustomScrollView(
+ slivers: [
+ const SliverAppBar.large(
+ title: Text('MapLibre Examples'),
+ floating: true,
+ snap: true,
+ ),
+ SliverToBoxAdapter(
+ child: Padding(
+ padding: const EdgeInsets.only(
+ left: 16.0,
+ right: 16.0,
+ bottom: 16.0,
+ ),
+ child: Card(
+ child: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Icon(
+ Icons.info_outline,
+ color: theme.colorScheme.primary,
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Text(
+ 'Explore ${_allPages.length} interactive examples',
+ style: theme.textTheme.titleMedium?.copyWith(
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 8),
+ Text(
+ 'Learn how to use MapLibre GL with Flutter through categorized examples.',
+ style: theme.textTheme.bodyMedium?.copyWith(
+ color: theme.colorScheme.onSurfaceVariant,
+ ),
+ ),
+ ],
+ ),
+ ),
),
+ ),
+ ),
+ SliverList(
+ delegate: SliverChildBuilderDelegate(
+ (context, index) {
+ const categories = ExampleCategory.values;
+ if (index >= categories.length) {
+ // About tile at the end
+ return const Padding(
+ padding:
+ EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
+ child: AboutListTile(
+ icon: Icon(Icons.info),
+ applicationName: "MapLibre GL Flutter",
+ aboutBoxChildren: [
+ Text(
+ 'MapLibre GL Flutter is an open-source Flutter plugin for embedding interactive maps using the MapLibre GL Native library.'),
+ SizedBox(height: 8),
+ Text(
+ 'This example app showcases various features and capabilities of the MapLibre GL Flutter plugin through interactive examples.'),
+ ],
+ ),
+ );
+ }
+
+ final category = categories[index];
+ final pages = groupedPages[category] ?? [];
+
+ if (pages.isEmpty) return const SizedBox.shrink();
+
+ return Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16.0, vertical: 4.0),
+ child: Card(
+ clipBehavior: Clip.antiAlias,
+ child: ExpansionTile(
+ leading: Icon(
+ category.icon,
+ color: theme.colorScheme.primary,
+ ),
+ title: Text(
+ category.label,
+ style: theme.textTheme.titleMedium?.copyWith(
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ subtitle: Text('${pages.length} examples'),
+ children: pages
+ .map((page) => ListTile(
+ leading: page.leading,
+ title: Text(page.title),
+ trailing: const Icon(Icons.chevron_right),
+ onTap: () => _pushPage(context, page),
+ ))
+ .toList(),
+ ),
+ ),
+ );
+ },
+ childCount: ExampleCategory.values.length + 1,
+ ),
+ ),
+ const SliverPadding(padding: EdgeInsets.only(bottom: 16)),
+ ],
),
);
}
}
-
-void main() {
- runApp(const MaterialApp(home: MapsDemo()));
-}
diff --git a/maplibre_gl_example/lib/map_ui.dart b/maplibre_gl_example/lib/map_ui.dart
deleted file mode 100644
index 8256db9cd..000000000
--- a/maplibre_gl_example/lib/map_ui.dart
+++ /dev/null
@@ -1,512 +0,0 @@
-// Copyright 2018 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-import 'package:flutter/material.dart';
-import 'package:collection/collection.dart';
-import 'package:maplibre_gl/maplibre_gl.dart';
-
-import 'page.dart';
-
-final LatLngBounds sydneyBounds = LatLngBounds(
- southwest: const LatLng(-34.022631, 150.620685),
- northeast: const LatLng(-33.571835, 151.325952),
-);
-
-class MapUiPage extends ExamplePage {
- const MapUiPage({super.key}) : super(const Icon(Icons.map), 'User interface');
-
- @override
- Widget build(BuildContext context) {
- return const MapUiBody();
- }
-}
-
-class MapUiBody extends StatefulWidget {
- const MapUiBody({super.key});
-
- @override
- State createState() => MapUiBodyState();
-}
-
-class MapUiBodyState extends State {
- MapUiBodyState();
-
- static const CameraPosition _kInitialPosition = CameraPosition(
- target: LatLng(-33.852, 151.211),
- zoom: 11.0,
- );
-
- MapLibreMapController? mapController;
- CameraPosition _position = _kInitialPosition;
- bool _isMoving = false;
- bool _compassEnabled = true;
- bool _mapExpanded = true;
- CameraTargetBounds _cameraTargetBounds = CameraTargetBounds.unbounded;
- MinMaxZoomPreference _minMaxZoomPreference = MinMaxZoomPreference.unbounded;
- int _styleStringIndex = 0;
-
- // Style string can a reference to a local or remote resources.
- // On Android the raw JSON can also be passed via a styleString, on iOS this is not supported.
- final List _styleStrings = [MapLibreStyles.demo, "assets/style.json"];
- final List _styleStringLabels = [
- "MapLibre demo style",
- "Local style file"
- ];
- bool _rotateGesturesEnabled = true;
- bool _scrollGesturesEnabled = true;
- bool? _doubleClickToZoomEnabled;
- bool _tiltGesturesEnabled = true;
- bool _zoomGesturesEnabled = true;
- bool _myLocationEnabled = true;
- bool _telemetryEnabled = true;
- bool _countriesVisible = true;
- MyLocationTrackingMode _myLocationTrackingMode = MyLocationTrackingMode.none;
- MyLocationRenderMode _myLocationRenderMode = MyLocationRenderMode.normal;
- List? _featureQueryFilter;
- Fill? _selectedFill;
-
- void _onMapChanged() {
- setState(() {
- _extractMapInfo();
- });
- }
-
- void _extractMapInfo() {
- final position = mapController!.cameraPosition;
- if (position != null) _position = position;
- _isMoving = mapController!.isCameraMoving;
- }
-
- @override
- void dispose() {
- mapController?.removeListener(_onMapChanged);
- super.dispose();
- }
-
- Widget _myLocationTrackingModeCycler() {
- final nextType = MyLocationTrackingMode.values[
- (_myLocationTrackingMode.index + 1) %
- MyLocationTrackingMode.values.length];
- return TextButton(
- child: Text('change to $nextType'),
- onPressed: () {
- setState(() {
- _myLocationTrackingMode = nextType;
- });
- },
- );
- }
-
- Widget _myLocationRenderModeCycler() {
- final nextType = MyLocationRenderMode.values[
- (_myLocationRenderMode.index + 1) % MyLocationRenderMode.values.length];
- return TextButton(
- onPressed: _myLocationEnabled || nextType == MyLocationRenderMode.normal
- ? () {
- setState(() {
- _myLocationRenderMode = nextType;
- });
- }
- : null,
- child: Text('change to $nextType'),
- );
- }
-
- Widget _queryFilterToggler() {
- return TextButton(
- child: Text(
- 'filter zoo on click ${_featureQueryFilter == null ? 'disabled' : 'enabled'}'),
- onPressed: () {
- setState(() {
- if (_featureQueryFilter == null) {
- _featureQueryFilter = [
- "==",
- ["get", "type"],
- "zoo"
- ];
- } else {
- _featureQueryFilter = null;
- }
- });
- },
- );
- }
-
- Widget _mapSizeToggler() {
- return TextButton(
- child: Text('${_mapExpanded ? 'shrink' : 'expand'} map'),
- onPressed: () {
- setState(() {
- _mapExpanded = !_mapExpanded;
- });
- },
- );
- }
-
- Widget _compassToggler() {
- return TextButton(
- child: Text('${_compassEnabled ? 'disable' : 'enable'} compasss'),
- onPressed: () {
- setState(() {
- _compassEnabled = !_compassEnabled;
- });
- },
- );
- }
-
- Widget _latLngBoundsToggler() {
- return TextButton(
- child: Text(
- _cameraTargetBounds.bounds == null
- ? 'bound camera target'
- : 'release camera target',
- ),
- onPressed: () {
- setState(() {
- _cameraTargetBounds = _cameraTargetBounds.bounds == null
- ? CameraTargetBounds(sydneyBounds)
- : CameraTargetBounds.unbounded;
- });
- },
- );
- }
-
- Widget _zoomBoundsToggler() {
- return TextButton(
- child: Text(_minMaxZoomPreference.minZoom == null
- ? 'bound zoom'
- : 'release zoom'),
- onPressed: () {
- setState(() {
- _minMaxZoomPreference = _minMaxZoomPreference.minZoom == null
- ? const MinMaxZoomPreference(12.0, 16.0)
- : MinMaxZoomPreference.unbounded;
- });
- },
- );
- }
-
- Widget _setStyleToSatellite() {
- return TextButton(
- child: Text(
- 'change map style to ${_styleStringLabels[(_styleStringIndex + 1) % _styleStringLabels.length]}'),
- onPressed: () {
- setState(() {
- _styleStringIndex = (_styleStringIndex + 1) % _styleStrings.length;
- });
- },
- );
- }
-
- Widget _rotateToggler() {
- return TextButton(
- child: Text('${_rotateGesturesEnabled ? 'disable' : 'enable'} rotate'),
- onPressed: () {
- setState(() {
- _rotateGesturesEnabled = !_rotateGesturesEnabled;
- });
- },
- );
- }
-
- Widget _scrollToggler() {
- return TextButton(
- child: Text('${_scrollGesturesEnabled ? 'disable' : 'enable'} scroll'),
- onPressed: () {
- setState(() {
- _scrollGesturesEnabled = !_scrollGesturesEnabled;
- });
- },
- );
- }
-
- Widget _doubleClickToZoomToggler() {
- final stateInfo = _doubleClickToZoomEnabled == null
- ? "disable"
- : _doubleClickToZoomEnabled!
- ? 'unset'
- : 'enable';
- return TextButton(
- child: Text('$stateInfo double click to zoom'),
- onPressed: () {
- setState(() {
- if (_doubleClickToZoomEnabled == null) {
- _doubleClickToZoomEnabled = false;
- } else if (!_doubleClickToZoomEnabled!) {
- _doubleClickToZoomEnabled = true;
- } else {
- _doubleClickToZoomEnabled = null;
- }
- });
- },
- );
- }
-
- Widget _tiltToggler() {
- return TextButton(
- child: Text('${_tiltGesturesEnabled ? 'disable' : 'enable'} tilt'),
- onPressed: () {
- setState(() {
- _tiltGesturesEnabled = !_tiltGesturesEnabled;
- });
- },
- );
- }
-
- Widget _zoomToggler() {
- return TextButton(
- child: Text('${_zoomGesturesEnabled ? 'disable' : 'enable'} zoom'),
- onPressed: () {
- setState(() {
- _zoomGesturesEnabled = !_zoomGesturesEnabled;
- });
- },
- );
- }
-
- Widget _myLocationToggler() {
- return TextButton(
- onPressed: _myLocationRenderMode == MyLocationRenderMode.normal
- ? () {
- setState(() {
- _myLocationEnabled = !_myLocationEnabled;
- });
- }
- : null,
- child: Text('${_myLocationEnabled ? 'disable' : 'enable'} my location'),
- );
- }
-
- Widget _telemetryToggler() {
- return TextButton(
- child: Text('${_telemetryEnabled ? 'disable' : 'enable'} telemetry'),
- onPressed: () async {
- setState(() {
- _telemetryEnabled = !_telemetryEnabled;
- });
- await mapController?.setTelemetryEnabled(_telemetryEnabled);
- },
- );
- }
-
- Widget _visibleRegionGetter() {
- return TextButton(
- child: const Text('get currently visible region'),
- onPressed: () async {
- final result = await mapController!.getVisibleRegion();
- if (!mounted) return;
-
- ScaffoldMessenger.of(context).showSnackBar(SnackBar(
- content: Text("SW: ${result.southwest} NE: ${result.northeast}"),
- ));
- },
- );
- }
-
- Widget _sourceFeaturesGetter() {
- return TextButton(
- child: const Text('get source features (maplibre)'),
- onPressed: () async {
- final result = await mapController!
- .querySourceFeatures("maplibre", "centroids", null);
- debugPrint(result.toString());
- },
- );
- }
-
- Widget _layerVisibilityToggler() {
- return TextButton(
- child: const Text('toggle layer visibility'),
- onPressed: () async {
- _countriesVisible = !_countriesVisible;
- mapController?.setLayerVisibility('countries-fill', _countriesVisible);
- },
- );
- }
-
- Future _clearFill() async {
- if (_selectedFill != null) {
- await mapController!.removeFill(_selectedFill!);
- setState(() {
- _selectedFill = null;
- });
- }
- }
-
- Future _drawFill(List features) async {
- final Map? feature =
- features.firstWhereOrNull((f) => f['geometry']['type'] == 'Polygon');
-
- if (feature != null) {
- final List> geometry = feature['geometry']['coordinates']
- .map(
- (ll) => ll.map((l) => LatLng(l[1], l[0])).toList().cast())
- .toList()
- .cast>();
- final fill = await mapController!.addFill(FillOptions(
- geometry: geometry,
- fillColor: "#FF0000",
- fillOutlineColor: "#FF0000",
- fillOpacity: 0.6,
- ));
- setState(() {
- _selectedFill = fill;
- });
- }
- }
-
- @override
- Widget build(BuildContext context) {
- final width = MediaQuery.of(context).size.width;
- final height =
- _mapExpanded ? MediaQuery.of(context).size.height / 2 : 200.0;
-
- final maplibreMap = MapLibreMap(
- onMapCreated: onMapCreated,
- initialCameraPosition: _kInitialPosition,
- trackCameraPosition: true,
- compassEnabled: _compassEnabled,
- cameraTargetBounds: _cameraTargetBounds,
- minMaxZoomPreference: _minMaxZoomPreference,
- styleString: _styleStrings[_styleStringIndex],
- rotateGesturesEnabled: _rotateGesturesEnabled,
- scrollGesturesEnabled: _scrollGesturesEnabled,
- tiltGesturesEnabled: _tiltGesturesEnabled,
- zoomGesturesEnabled: _zoomGesturesEnabled,
- doubleClickZoomEnabled: _doubleClickToZoomEnabled,
- myLocationEnabled: _myLocationEnabled,
- myLocationTrackingMode: _myLocationTrackingMode,
- myLocationRenderMode: _myLocationRenderMode,
- onMapClick: (point, latLng) async {
- debugPrint(
- "Map click: ${point.x},${point.y} ${latLng.latitude}/${latLng.longitude}");
- debugPrint("Filter $_featureQueryFilter");
- final features = await mapController!
- .queryRenderedFeatures(point, [], _featureQueryFilter);
- if (!mounted) return;
-
- debugPrint('# features: ${features.length}');
- _clearFill();
- if (features.isEmpty && _featureQueryFilter != null) {
- if (context.mounted) {
- ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
- content: Text('QueryRenderedFeatures: No features found!')));
- }
- } else if (features.isNotEmpty) {
- _drawFill(features);
- }
- },
- onMapLongClick: (point, latLng) async {
- debugPrint(
- "Map long press: ${point.x},${point.y} ${latLng.latitude}/${latLng.longitude}");
- final convertedPoint = await mapController!.toScreenLocation(latLng);
- final convertedLatLng = await mapController!.toLatLng(point);
- debugPrint(
- "Map long press converted: ${convertedPoint.x},${convertedPoint.y} ${convertedLatLng.latitude}/${convertedLatLng.longitude}");
- final metersPerPixel =
- await mapController!.getMetersPerPixelAtLatitude(latLng.latitude);
-
- debugPrint(
- "Map long press The distance measured in meters at latitude ${latLng.latitude} is $metersPerPixel m");
-
- final features =
- await mapController!.queryRenderedFeatures(point, [], null);
- if (features.isNotEmpty) {
- debugPrint("Features in map: ${features[0]}");
- }
- },
- onCameraTrackingDismissed: () {
- setState(() {
- _myLocationTrackingMode = MyLocationTrackingMode.none;
- });
- },
- onUserLocationUpdated: (location) {
- debugPrint(
- "new location: ${location.position}, alt.: ${location.altitude}, bearing: ${location.bearing}, speed: ${location.speed}, horiz. accuracy: ${location.horizontalAccuracy}, vert. accuracy: ${location.verticalAccuracy}");
- },
- );
-
- final controlWidgets = [];
-
- if (mapController != null) {
- controlWidgets.addAll(
- [
- _mapSizeToggler(),
- _queryFilterToggler(),
- _compassToggler(),
- _myLocationTrackingModeCycler(),
- _myLocationRenderModeCycler(),
- _latLngBoundsToggler(),
- _setStyleToSatellite(),
- _zoomBoundsToggler(),
- _rotateToggler(),
- _scrollToggler(),
- _doubleClickToZoomToggler(),
- _tiltToggler(),
- _zoomToggler(),
- _myLocationToggler(),
- _telemetryToggler(),
- _visibleRegionGetter(),
- _layerVisibilityToggler(),
- _sourceFeaturesGetter(),
- ],
- );
- }
- return Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- Stack(
- children: [
- Center(
- child: SizedBox(
- width: width,
- height: height,
- child: maplibreMap,
- ),
- ),
- Align(
- alignment: Alignment.topLeft,
- child: Padding(
- padding: const EdgeInsets.all(8.0),
- child: Text(
- 'Lat: ${_position.target.latitude.toStringAsFixed(4)}, '
- 'Lng: ${_position.target.longitude.toStringAsFixed(4)}, '
- 'Zoom: ${_position.zoom.toStringAsFixed(2)}'
- '${_isMoving ? " (moving)" : ""}',
- style: const TextStyle(
- backgroundColor: Colors.white,
- fontSize: 16.0,
- ),
- ),
- ),
- ),
- ],
- ),
- Expanded(
- child: SingleChildScrollView(
- child: Padding(
- padding: const EdgeInsets.all(8.0),
- child: Wrap(
- spacing: 4.0,
- runSpacing: 4.0,
- alignment: WrapAlignment.center,
- children: controlWidgets,
- ),
- ),
- ),
- )
- ],
- );
- }
-
- Future onMapCreated(MapLibreMapController controller) async {
- mapController = controller;
- mapController!.addListener(_onMapChanged);
- _extractMapInfo();
-
- await mapController!.getTelemetryEnabled().then((isEnabled) => setState(() {
- _telemetryEnabled = isEnabled;
- }));
- }
-}
diff --git a/maplibre_gl_example/lib/move_camera.dart b/maplibre_gl_example/lib/move_camera.dart
deleted file mode 100644
index 052788937..000000000
--- a/maplibre_gl_example/lib/move_camera.dart
+++ /dev/null
@@ -1,185 +0,0 @@
-// Copyright 2018 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-import 'package:flutter/material.dart';
-import 'package:maplibre_gl/maplibre_gl.dart';
-
-import 'page.dart';
-
-class MoveCameraPage extends ExamplePage {
- const MoveCameraPage({super.key})
- : super(const Icon(Icons.map), 'Camera control');
-
- @override
- Widget build(BuildContext context) {
- return const MoveCamera();
- }
-}
-
-class MoveCamera extends StatefulWidget {
- const MoveCamera({super.key});
-
- @override
- State createState() => MoveCameraState();
-}
-
-class MoveCameraState extends State {
- late MapLibreMapController mapController;
-
- void _onMapCreated(MapLibreMapController controller) {
- mapController = controller;
- }
-
- @override
- Widget build(BuildContext context) {
- final width = MediaQuery.of(context).size.width;
- final height = MediaQuery.of(context).size.height;
-
- return Column(
- children: [
- SizedBox(
- width: width,
- height: height * 0.5,
- child: MapLibreMap(
- onMapCreated: _onMapCreated,
- onCameraIdle: () => debugPrint("onCameraIdle"),
- initialCameraPosition:
- const CameraPosition(target: LatLng(0.0, 0.0)),
- ),
- ),
- Padding(
- padding: const EdgeInsets.all(8.0),
- child: SingleChildScrollView(
- child: Wrap(
- spacing: 4.0,
- runSpacing: 4.0,
- alignment: WrapAlignment.center,
- children: [
- TextButton(
- onPressed: () async {
- await mapController.moveCamera(
- CameraUpdate.newCameraPosition(
- const CameraPosition(
- bearing: 270.0,
- target: LatLng(51.5160895, -0.1294527),
- tilt: 30.0,
- zoom: 17.0,
- ),
- ),
- );
- },
- child: const Text('newCameraPosition'),
- ),
- TextButton(
- onPressed: () async {
- await mapController.moveCamera(
- CameraUpdate.newLatLng(
- const LatLng(56.1725505, 10.1850512),
- ),
- );
- },
- child: const Text('newLatLng'),
- ),
- TextButton(
- onPressed: () async {
- await mapController.moveCamera(
- CameraUpdate.newLatLngBounds(
- LatLngBounds(
- southwest: const LatLng(-38.483935, 113.248673),
- northeast: const LatLng(-8.982446, 153.823821),
- ),
- left: 10,
- top: 5,
- bottom: 25,
- ),
- );
- },
- child: const Text('newLatLngBounds'),
- ),
- TextButton(
- onPressed: () async {
- await mapController.moveCamera(
- CameraUpdate.newLatLngZoom(
- const LatLng(37.4231613, -122.087159),
- 11.0,
- ),
- );
- },
- child: const Text('newLatLngZoom'),
- ),
- TextButton(
- onPressed: () async {
- await mapController.moveCamera(
- CameraUpdate.scrollBy(150.0, -225.0),
- );
- },
- child: const Text('scrollBy'),
- ),
- TextButton(
- onPressed: () async {
- await mapController.moveCamera(
- CameraUpdate.zoomBy(
- -0.5,
- const Offset(30.0, 20.0),
- ),
- );
- },
- child: const Text('zoomBy with focus'),
- ),
- TextButton(
- onPressed: () async {
- await mapController.moveCamera(
- CameraUpdate.zoomBy(-0.5),
- );
- },
- child: const Text('zoomBy'),
- ),
- TextButton(
- onPressed: () async {
- await mapController.moveCamera(
- CameraUpdate.zoomIn(),
- );
- },
- child: const Text('zoomIn'),
- ),
- TextButton(
- onPressed: () async {
- await mapController.moveCamera(
- CameraUpdate.zoomOut(),
- );
- },
- child: const Text('zoomOut'),
- ),
- TextButton(
- onPressed: () async {
- await mapController.moveCamera(
- CameraUpdate.zoomTo(16.0),
- );
- },
- child: const Text('zoomTo'),
- ),
- TextButton(
- onPressed: () async {
- await mapController.moveCamera(
- CameraUpdate.bearingTo(45.0),
- );
- },
- child: const Text('bearingTo'),
- ),
- TextButton(
- onPressed: () async {
- await mapController.moveCamera(
- CameraUpdate.tiltTo(30.0),
- );
- },
- child: const Text('tiltTo'),
- ),
- ],
- ),
- ),
- )
- ],
- );
- }
-}
diff --git a/maplibre_gl_example/lib/no_location_permission_page.dart b/maplibre_gl_example/lib/no_location_permission_page.dart
deleted file mode 100644
index f0c283ca5..000000000
--- a/maplibre_gl_example/lib/no_location_permission_page.dart
+++ /dev/null
@@ -1,39 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:maplibre_gl/maplibre_gl.dart';
-
-import 'page.dart';
-
-class NoLocationPermissionPage extends ExamplePage {
- const NoLocationPermissionPage({super.key})
- : super(
- const Icon(Icons.gps_off),
- 'Using a map without user location/permission',
- needsLocationPermission: false,
- );
-
- @override
- Widget build(BuildContext context) {
- return const NoLocationPermissionBody();
- }
-}
-
-class NoLocationPermissionBody extends StatefulWidget {
- const NoLocationPermissionBody({super.key});
-
- @override
- State createState() =>
- _NoLocationPermissionBodyState();
-}
-
-class _NoLocationPermissionBodyState extends State {
- @override
- Widget build(BuildContext context) {
- return MapLibreMap(
- initialCameraPosition: const CameraPosition(
- target: LatLng(-33.852, 151.211),
- zoom: 11.0,
- ),
- styleString: "assets/osm_style.json",
- );
- }
-}
diff --git a/maplibre_gl_example/lib/page.dart b/maplibre_gl_example/lib/page.dart
index 45b88af19..caab76694 100644
--- a/maplibre_gl_example/lib/page.dart
+++ b/maplibre_gl_example/lib/page.dart
@@ -1,18 +1,31 @@
-// Copyright 2018 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
import 'package:flutter/material.dart';
+/// Category for organizing examples
+enum ExampleCategory {
+ basics('Basics', Icons.map),
+ camera('Camera', Icons.videocam),
+ interaction('Interaction', Icons.touch_app),
+ annotations('Annotations', Icons.location_on),
+ layers('Layers & Sources', Icons.layers),
+ advanced('Advanced', Icons.science);
+
+ const ExampleCategory(this.label, this.icon);
+
+ final String label;
+ final IconData icon;
+}
+
abstract class ExamplePage extends StatelessWidget {
const ExamplePage(
this.leading,
this.title, {
this.needsLocationPermission = true,
+ this.category = ExampleCategory.basics,
super.key,
});
final Widget leading;
final String title;
final bool needsLocationPermission;
+ final ExampleCategory category;
}
diff --git a/maplibre_gl_example/lib/place_batch.dart b/maplibre_gl_example/lib/place_batch.dart
deleted file mode 100644
index 0a70a405e..000000000
--- a/maplibre_gl_example/lib/place_batch.dart
+++ /dev/null
@@ -1,188 +0,0 @@
-// Copyright 2018 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-import 'package:flutter/material.dart';
-import 'package:maplibre_gl/maplibre_gl.dart';
-
-import 'page.dart';
-import 'util.dart';
-
-const fillOptions = [
- FillOptions(
- geometry: [
- [
- LatLng(-33.719, 151.150),
- LatLng(-33.858, 151.150),
- LatLng(-33.866, 151.401),
- LatLng(-33.747, 151.328),
- LatLng(-33.719, 151.150),
- ],
- [
- LatLng(-33.762, 151.250),
- LatLng(-33.827, 151.250),
- LatLng(-33.833, 151.347),
- LatLng(-33.762, 151.250),
- ]
- ],
- fillColor: "#FF0000",
- ),
- FillOptions(geometry: [
- [
- LatLng(-33.719, 151.550),
- LatLng(-33.858, 151.550),
- LatLng(-33.866, 151.801),
- LatLng(-33.747, 151.728),
- LatLng(-33.719, 151.550),
- ],
- [
- LatLng(-33.762, 151.650),
- LatLng(-33.827, 151.650),
- LatLng(-33.833, 151.747),
- LatLng(-33.762, 151.650),
- ]
- ], fillColor: "#FF0000"),
-];
-
-class BatchAddPage extends ExamplePage {
- const BatchAddPage({super.key})
- : super(const Icon(Icons.check_circle), 'Batch add/remove');
-
- @override
- Widget build(BuildContext context) {
- return const BatchAddBody();
- }
-}
-
-class BatchAddBody extends StatefulWidget {
- const BatchAddBody({super.key});
-
- @override
- State createState() => BatchAddBodyState();
-}
-
-class BatchAddBodyState extends State {
- BatchAddBodyState();
-
- List _fills = [];
- List _circles = [];
- List _lines = [];
- List _symbols = [];
-
- static const LatLng center = LatLng(-33.86711, 151.1947171);
-
- late MapLibreMapController controller;
-
- void _onMapCreated(MapLibreMapController controller) {
- this.controller = controller;
- }
-
- List makeLinesOptionsForFillOptions(
- Iterable options) {
- final listOptions = [];
- for (final option in options) {
- for (final geom in option.geometry!) {
- listOptions.add(LineOptions(geometry: geom, lineColor: "#00FF00"));
- }
- }
- return listOptions;
- }
-
- List makeCircleOptionsForFillOptions(
- Iterable options) {
- final circleOptions = [];
- for (final option in options) {
- // put circles only on the outside
- for (final latLng in option.geometry!.first) {
- circleOptions
- .add(CircleOptions(geometry: latLng, circleColor: "#00FF00"));
- }
- }
- return circleOptions;
- }
-
- List makeSymbolOptionsForFillOptions(
- Iterable options) {
- final symbolOptions = [];
- for (final option in options) {
- // put symbols only on the inner most ring if it exists
- if (option.geometry!.length > 1) {
- for (final latLng in option.geometry!.last) {
- symbolOptions
- .add(SymbolOptions(iconImage: 'custom-marker', geometry: latLng));
- }
- }
- }
- return symbolOptions;
- }
-
- Future _add() async {
- if (_fills.isEmpty) {
- _fills = await controller.addFills(fillOptions);
- _lines = await controller
- .addLines(makeLinesOptionsForFillOptions(fillOptions));
- _circles = await controller
- .addCircles(makeCircleOptionsForFillOptions(fillOptions));
- _symbols = await controller
- .addSymbols(makeSymbolOptionsForFillOptions(fillOptions));
- }
- }
-
- Future _remove() async {
- await controller.removeFills(_fills);
- await controller.removeLines(_lines);
- await controller.removeCircles(_circles);
- await controller.removeSymbols(_symbols);
- _fills.clear();
- _lines.clear();
- _circles.clear();
- _symbols.clear();
- }
-
- @override
- Widget build(BuildContext context) {
- final width = MediaQuery.of(context).size.width;
- final height = MediaQuery.of(context).size.height;
-
- return Column(
- children: [
- SizedBox(
- width: width,
- height: height * 0.5,
- child: MapLibreMap(
- onMapCreated: _onMapCreated,
- onStyleLoadedCallback: () => addImageFromAsset(controller,
- "custom-marker", "assets/symbols/custom-marker.png"),
- initialCameraPosition: const CameraPosition(
- target: LatLng(-33.8, 151.511),
- zoom: 8.2,
- ),
- annotationOrder: const [
- AnnotationType.fill,
- AnnotationType.line,
- AnnotationType.circle,
- AnnotationType.symbol,
- ],
- ),
- ),
- Expanded(
- child: SingleChildScrollView(
- child: Padding(
- padding: const EdgeInsets.all(8.0),
- child: Wrap(
- spacing: 4.0,
- runSpacing: 4.0,
- alignment: WrapAlignment.center,
- children: [
- TextButton(onPressed: _add, child: const Text('batch add')),
- TextButton(
- onPressed: _remove, child: const Text('batch remove')),
- ],
- ),
- ),
- ),
- ),
- ],
- );
- }
-}
diff --git a/maplibre_gl_example/lib/place_circle.dart b/maplibre_gl_example/lib/place_circle.dart
deleted file mode 100644
index b82c1dc8d..000000000
--- a/maplibre_gl_example/lib/place_circle.dart
+++ /dev/null
@@ -1,284 +0,0 @@
-// Copyright 2018 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-import 'dart:async';
-import 'dart:math';
-
-import 'package:flutter/material.dart';
-import 'package:maplibre_gl/maplibre_gl.dart';
-
-import 'page.dart';
-
-class PlaceCirclePage extends ExamplePage {
- const PlaceCirclePage({super.key})
- : super(const Icon(Icons.check_circle), 'Place circle');
-
- @override
- Widget build(BuildContext context) {
- return const PlaceCircleBody();
- }
-}
-
-class PlaceCircleBody extends StatefulWidget {
- const PlaceCircleBody({super.key});
-
- @override
- State createState() => PlaceCircleBodyState();
-}
-
-class PlaceCircleBodyState extends State {
- PlaceCircleBodyState();
-
- static const LatLng center = LatLng(-33.86711, 151.1947171);
-
- MapLibreMapController? controller;
- int _circleCount = 0;
- Circle? _selectedCircle;
-
- void _onMapCreated(MapLibreMapController controller) {
- this.controller = controller;
- controller.onCircleTapped.add(_onCircleTapped);
- }
-
- @override
- void dispose() {
- controller?.onCircleTapped.remove(_onCircleTapped);
- super.dispose();
- }
-
- Future _onCircleTapped(Circle circle) async {
- if (_selectedCircle != null) {
- await _updateSelectedCircle(
- const CircleOptions(circleRadius: 60),
- );
- }
- setState(() {
- _selectedCircle = circle;
- });
- await _updateSelectedCircle(
- const CircleOptions(
- circleRadius: 30,
- ),
- );
- }
-
- Future _updateSelectedCircle(CircleOptions changes) async {
- await controller!.updateCircle(_selectedCircle!, changes);
- }
-
- Future _add() async {
- await controller!.addCircle(
- CircleOptions(
- geometry: LatLng(
- center.latitude + sin(_circleCount * pi / 6.0) / 20.0,
- center.longitude + cos(_circleCount * pi / 6.0) / 20.0,
- ),
- circleColor: "#FF0000"),
- );
- setState(() {
- _circleCount += 1;
- });
- }
-
- Future _remove() async {
- await controller!.removeCircle(_selectedCircle!);
- setState(() {
- _selectedCircle = null;
- _circleCount -= 1;
- });
- }
-
- Future _changePosition() async {
- final current = _selectedCircle!.options.geometry!;
- final offset = Offset(
- center.latitude - current.latitude,
- center.longitude - current.longitude,
- );
- await _updateSelectedCircle(
- CircleOptions(
- geometry: LatLng(
- center.latitude + offset.dy,
- center.longitude + offset.dx,
- ),
- ),
- );
- }
-
- Future _changeDraggable() async {
- var draggable = _selectedCircle!.options.draggable;
- draggable ??= false;
- await _updateSelectedCircle(
- CircleOptions(
- draggable: !draggable,
- ),
- );
- }
-
- void _getLatLng() {
- final latLng = controller!.getCircleLatLng(_selectedCircle!);
- if (!mounted) return;
-
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(
- content: Text(latLng.toString()),
- ),
- );
- }
-
- Future _changeCircleStrokeOpacity() async {
- var current = _selectedCircle!.options.circleStrokeOpacity;
- current ??= 1.0;
-
- await _updateSelectedCircle(
- CircleOptions(circleStrokeOpacity: current < 0.1 ? 1.0 : current * 0.75),
- );
- }
-
- Future _changeCircleStrokeWidth() async {
- var current = _selectedCircle!.options.circleStrokeWidth;
- current ??= 0;
- await _updateSelectedCircle(
- CircleOptions(circleStrokeWidth: current == 0 ? 5.0 : 0));
- }
-
- Future _changeCircleStrokeColor() async {
- var current = _selectedCircle!.options.circleStrokeColor;
- current ??= "#FFFFFF";
-
- await _updateSelectedCircle(
- CircleOptions(
- circleStrokeColor: current == "#FFFFFF" ? "#FF0000" : "#FFFFFF"),
- );
- }
-
- Future _changeCircleOpacity() async {
- var current = _selectedCircle!.options.circleOpacity;
- current ??= 1.0;
-
- await _updateSelectedCircle(
- CircleOptions(circleOpacity: current < 0.1 ? 1.0 : current * 0.75),
- );
- }
-
- Future _changeCircleRadius() async {
- var current = _selectedCircle!.options.circleRadius;
- current ??= 0;
- await _updateSelectedCircle(
- CircleOptions(circleRadius: current == 120.0 ? 30.0 : current + 30.0),
- );
- }
-
- Future _changeCircleColor() async {
- var current = _selectedCircle!.options.circleColor;
- current ??= "#FF0000";
-
- await _updateSelectedCircle(
- const CircleOptions(circleColor: "#FFFF00"),
- );
- }
-
- Future _changeCircleBlur() async {
- var current = _selectedCircle!.options.circleBlur;
- current ??= 0;
- await _updateSelectedCircle(
- CircleOptions(circleBlur: current == 0.75 ? 0 : 0.75),
- );
- }
-
- @override
- Widget build(BuildContext context) {
- final width = MediaQuery.of(context).size.width;
- final height = MediaQuery.of(context).size.height;
-
- return Column(
- children: [
- SizedBox(
- width: width,
- height: height * 0.5,
- child: MapLibreMap(
- onMapCreated: _onMapCreated,
- initialCameraPosition: const CameraPosition(
- target: LatLng(-33.852, 151.211),
- zoom: 11.0,
- ),
- ),
- ),
- Expanded(
- child: SingleChildScrollView(
- child: Padding(
- padding: const EdgeInsets.all(8.0),
- child: Wrap(
- spacing: 4.0,
- runSpacing: 4.0,
- alignment: WrapAlignment.center,
- children: [
- TextButton(
- onPressed: (_circleCount == 12) ? null : _add,
- child: const Text('add'),
- ),
- TextButton(
- onPressed: (_selectedCircle == null) ? null : _remove,
- child: const Text('remove'),
- ),
- TextButton(
- onPressed:
- (_selectedCircle == null) ? null : _changeCircleOpacity,
- child: const Text('change circle-opacity'),
- ),
- TextButton(
- onPressed:
- (_selectedCircle == null) ? null : _changeCircleRadius,
- child: const Text('change circle-radius'),
- ),
- TextButton(
- onPressed:
- (_selectedCircle == null) ? null : _changeCircleColor,
- child: const Text('change circle-color'),
- ),
- TextButton(
- onPressed:
- (_selectedCircle == null) ? null : _changeCircleBlur,
- child: const Text('change circle-blur'),
- ),
- TextButton(
- onPressed: (_selectedCircle == null)
- ? null
- : _changeCircleStrokeWidth,
- child: const Text('change circle-stroke-width'),
- ),
- TextButton(
- onPressed: (_selectedCircle == null)
- ? null
- : _changeCircleStrokeColor,
- child: const Text('change circle-stroke-color'),
- ),
- TextButton(
- onPressed: (_selectedCircle == null)
- ? null
- : _changeCircleStrokeOpacity,
- child: const Text('change circle-stroke-opacity'),
- ),
- TextButton(
- onPressed:
- (_selectedCircle == null) ? null : _changePosition,
- child: const Text('change position'),
- ),
- TextButton(
- onPressed:
- (_selectedCircle == null) ? null : _changeDraggable,
- child: const Text('toggle draggable'),
- ),
- TextButton(
- onPressed: (_selectedCircle == null) ? null : _getLatLng,
- child: const Text('get current LatLng'),
- ),
- ],
- ),
- ),
- ),
- ),
- ],
- );
- }
-}
diff --git a/maplibre_gl_example/lib/place_fill.dart b/maplibre_gl_example/lib/place_fill.dart
deleted file mode 100644
index 8712f59b1..000000000
--- a/maplibre_gl_example/lib/place_fill.dart
+++ /dev/null
@@ -1,240 +0,0 @@
-// Copyright 2018 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-import 'dart:async';
-
-import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
-import 'package:maplibre_gl/maplibre_gl.dart';
-
-import 'page.dart';
-
-class PlaceFillPage extends ExamplePage {
- const PlaceFillPage({super.key})
- : super(const Icon(Icons.check_circle), 'Place fill');
-
- @override
- Widget build(BuildContext context) {
- return const PlaceFillBody();
- }
-}
-
-class PlaceFillBody extends StatefulWidget {
- const PlaceFillBody({super.key});
-
- @override
- State createState() => PlaceFillBodyState();
-}
-
-class PlaceFillBodyState extends State {
- PlaceFillBodyState();
-
- static const LatLng center = LatLng(-33.86711, 151.1947171);
- final String _fillPatternImage = "assets/fill/cat_silhouette_pattern.png";
-
- final List