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. [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=com.android.application&package-manager=gradle&previous-version=8.12.0&new-version=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

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=org.maplibre.gl:android-sdk&package-manager=gradle&previous-version=11.13.5&new-version=12.3.0)](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

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/upload-artifact&package-manager=github_actions&previous-version=4&new-version=6)](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 [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=com.android.tools.build:gradle&package-manager=gradle&previous-version=8.13.0&new-version=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

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=5&new-version=6)](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

  • Fix: Don't delay triggering timeouts. In Okio 3.16.0 we introduced a regression that caused timeouts to fire later than they were supposed to.

  • Upgrade: [Okio 3.16.4][okio_3_16_4].

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

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=com.squareup.okhttp3:okhttp&package-manager=gradle&previous-version=4.12.0&new-version=5.3.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/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

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=org.jetbrains.kotlin:kotlin-gradle-plugin&package-manager=gradle&previous-version=2.1.0&new-version=2.2.20)](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

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=org.jetbrains.kotlin.android&package-manager=gradle&previous-version=2.1.0&new-version=2.2.21)](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

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=5&new-version=6)](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

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/upload-artifact&package-manager=github_actions&previous-version=4&new-version=6)](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 [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=com.android.tools.build:gradle&package-manager=gradle&previous-version=8.13.0&new-version=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. [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=com.android.application&package-manager=gradle&previous-version=8.12.0&new-version=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

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=org.jetbrains.kotlin:kotlin-gradle-plugin&package-manager=gradle&previous-version=2.1.0&new-version=2.3.0)](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

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=org.jetbrains.kotlin.android&package-manager=gradle&previous-version=2.1.0&new-version=2.3.0)](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

  • Fix: Don't delay triggering timeouts. In Okio 3.16.0 we introduced a regression that caused timeouts to fire later than they were supposed to.

  • Upgrade: [Okio 3.16.4][okio_3_16_4].

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

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=com.squareup.okhttp3:okhttp&package-manager=gradle&previous-version=4.12.0&new-version=5.3.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/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> _defaultGeometry = [ - [ - const LatLng(-33.719, 151.150), - const LatLng(-33.858, 151.150), - const LatLng(-33.866, 151.401), - const LatLng(-33.747, 151.328), - const LatLng(-33.719, 151.150), - ], - [ - const LatLng(-33.762, 151.250), - const LatLng(-33.827, 151.250), - const LatLng(-33.833, 151.347), - const LatLng(-33.762, 151.250), - ] - ]; - - MapLibreMapController? controller; - int _fillCount = 0; - Fill? _selectedFill; - - void _onMapCreated(MapLibreMapController controller) { - this.controller = controller; - controller.onFillTapped.add(_onFillTapped); - } - - Future _onStyleLoaded() async { - await addImageFromAsset("assetImage", _fillPatternImage); - } - - /// 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); - } - - @override - void dispose() { - controller?.onFillTapped.remove(_onFillTapped); - super.dispose(); - } - - void _onFillTapped(Fill fill) { - setState(() { - _selectedFill = fill; - }); - } - - Future _updateSelectedFill(FillOptions changes) async { - await controller!.updateFill(_selectedFill!, changes); - } - - Future _add() async { - await controller!.addFill( - FillOptions( - geometry: _defaultGeometry, - fillColor: "#FF0000", - fillOutlineColor: "#FF0000"), - ); - setState(() { - _fillCount += 1; - }); - } - - Future _remove() async { - await controller!.removeFill(_selectedFill!); - setState(() { - _selectedFill = null; - _fillCount -= 1; - }); - } - - Future _changePosition() async { - var geometry = _selectedFill!.options.geometry; - - geometry ??= _defaultGeometry; - - await _updateSelectedFill(FillOptions( - geometry: geometry - .map((list) => list - .map( - // Move to right with 0.1 degree on longitude - (latLng) => LatLng(latLng.latitude, latLng.longitude + 0.1)) - .toList()) - .toList())); - } - - Future _changeDraggable() async { - var draggable = _selectedFill!.options.draggable; - draggable ??= false; - await _updateSelectedFill( - FillOptions(draggable: !draggable), - ); - } - - Future _changeFillOpacity() async { - var current = _selectedFill!.options.fillOpacity; - current ??= 1.0; - - await _updateSelectedFill( - FillOptions(fillOpacity: current < 0.1 ? 1.0 : current * 0.75), - ); - } - - Future _changeFillColor() async { - var current = _selectedFill!.options.fillColor; - current ??= "#FF0000"; - - await _updateSelectedFill( - const FillOptions(fillColor: "#FFFF00"), - ); - } - - Future _changeFillOutlineColor() async { - var current = _selectedFill!.options.fillOutlineColor; - current ??= "#FF0000"; - - await _updateSelectedFill( - const FillOptions(fillOutlineColor: "#FFFF00"), - ); - } - - Future _changeFillPattern() async { - final current = - _selectedFill!.options.fillPattern == null ? "assetImage" : null; - await _updateSelectedFill( - FillOptions(fillPattern: current), - ); - } - - @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: _onStyleLoaded, - initialCameraPosition: const CameraPosition( - target: LatLng(-33.852, 151.211), - zoom: 7.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: (_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 deleted file mode 100644 index 7dc75a057..000000000 --- a/maplibre_gl_example/lib/place_source.dart +++ /dev/null @@ -1,191 +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 PlaceSourcePage extends ExamplePage { - const PlaceSourcePage({super.key}) - : super(const Icon(Icons.place), 'Place source'); - - @override - Widget build(BuildContext context) { - return const PlaceSymbolBody(); - } -} - -class PlaceSymbolBody extends StatefulWidget { - const PlaceSymbolBody({super.key}); - - @override - State createState() => PlaceSymbolBodyState(); -} - -class PlaceSymbolBodyState extends State { - PlaceSymbolBodyState(); - - static const sourceId = 'sydney_source'; - static const layerId = 'sydney_layer'; - - bool sourceAdded = false; - bool layerAdded = false; - bool imageFlag = false; - late MapLibreMapController controller; - - void _onMapCreated(MapLibreMapController controller) { - this.controller = controller; - } - - @override - void dispose() { - super.dispose(); - } - - /// Adds an asset image as a source to the currently displayed style - Future addImageSourceFromAsset( - String imageSourceId, String assetName) async { - final bytes = await rootBundle.load(assetName); - final list = bytes.buffer.asUint8List(); - return controller.addImageSource( - imageSourceId, - list, - const LatLngQuad( - bottomRight: LatLng(-33.86264728692581, 151.19916915893555), - bottomLeft: LatLng(-33.86264728692581, 151.2288236618042), - topLeft: LatLng(-33.84322353475214, 151.2288236618042), - topRight: LatLng(-33.84322353475214, 151.19916915893555), - ), - ); - } - - Future removeImageSource(String imageSourceId) { - return controller.removeSource(imageSourceId); - } - - Future addLayer(String imageLayerId, String imageSourceId) async { - if (layerAdded) { - await removeLayer(imageLayerId); - } - setState(() => layerAdded = true); - return controller.addImageLayer(imageLayerId, imageSourceId); - } - - Future addLayerBelow( - String imageLayerId, String imageSourceId, String belowLayerId) async { - if (layerAdded) { - await removeLayer(imageLayerId); - } - setState(() => layerAdded = true); - return controller.addImageLayerBelow( - imageLayerId, imageSourceId, belowLayerId); - } - - Future removeLayer(String imageLayerId) { - setState(() => layerAdded = false); - return controller.removeLayer(imageLayerId); - } - - Future updateImageSourceFromAsset( - String imageSourceId, String assetName) async { - final bytes = await rootBundle.load(assetName); - final list = bytes.buffer.asUint8List(); - return controller.updateImageSource(imageSourceId, list, null); - } - - String pickImage() { - return imageFlag ? 'assets/sydney0.png' : 'assets/sydney1.png'; - } - - @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: 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: 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 deleted file mode 100644 index f44e2c24e..000000000 --- a/maplibre_gl_example/lib/place_symbol.dart +++ /dev/null @@ -1,418 +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:core'; -import 'dart:developer' as dev; -import 'dart:math'; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:http/http.dart' as http; -import 'package:maplibre_gl/maplibre_gl.dart'; - -import 'page.dart'; - -class PlaceSymbolPage extends ExamplePage { - const PlaceSymbolPage({super.key}) - : super(const Icon(Icons.place), 'Place symbol'); - - @override - Widget build(BuildContext context) { - return const PlaceSymbolBody(); - } -} - -class PlaceSymbolBody extends StatefulWidget { - const PlaceSymbolBody({super.key}); - - @override - State createState() => PlaceSymbolBodyState(); -} - -class PlaceSymbolBodyState extends State { - PlaceSymbolBodyState(); - - static const LatLng center = LatLng(-33.86711, 151.1947171); - - MapLibreMapController? controller; - int _symbolCount = 0; - Symbol? _selectedSymbol; - bool _iconAllowOverlap = false; - - void _onMapCreated(MapLibreMapController controller) { - this.controller = controller; - controller.onSymbolTapped.add(_onSymbolTapped); - controller.onFeatureDrag.add(_onFeatureDrag); - } - - Future _onStyleLoaded() async { - await addImageFromAsset( - "custom-marker", "assets/symbols/custom-marker.png"); - await addImageFromAsset("assetImage", "assets/symbols/custom-icon.png"); - await addImageFromUrl( - "networkImage", Uri.parse("https://dummyimage.com/50x50")); - } - - @override - void dispose() { - controller?.onSymbolTapped.remove(_onSymbolTapped); - controller?.onFeatureDrag.remove(_onFeatureDrag); - 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); - } - - /// Adds a network image to the currently displayed style - Future addImageFromUrl(String name, Uri uri) async { - final response = await http.get(uri); - dev.log( - "response.statusCode: ${response.statusCode} for uri: $uri, bodyBytes length: ${response.bodyBytes.length}"); - return controller!.addImage(name, response.bodyBytes); - } - - Future _onSymbolTapped(Symbol symbol) async { - if (_selectedSymbol != null) { - await _updateSelectedSymbol( - const SymbolOptions(iconSize: 1.0), - ); - } - setState(() { - _selectedSymbol = symbol; - }); - await _updateSelectedSymbol( - const SymbolOptions(iconSize: 1.4), - ); - } - - void _onFeatureDrag( - Point point, - LatLng origin, - LatLng current, - LatLng delta, - String id, - Annotation? annotation, - DragEventType eventType, - ) { - if (annotation is! Symbol) return; - if (eventType == DragEventType.end) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'Symbol #${annotation.data?['count'] ?? ''} was dragged to ${annotation.options.geometry}'), - ), - ); - } - } - - Future _updateSelectedSymbol(SymbolOptions changes) async { - await controller!.updateSymbol(_selectedSymbol!, changes); - } - - Future _add(String iconImage) async { - final availableNumbers = Iterable.generate(12).toList(); - for (final s in controller!.symbols) { - availableNumbers.removeWhere((i) => i == s.data!['count']); - } - if (availableNumbers.isNotEmpty) { - await controller!.addSymbol( - _getSymbolOptions(iconImage, availableNumbers.first), - {'count': availableNumbers.first}); - setState(() { - _symbolCount += 1; - }); - } - } - - SymbolOptions _getSymbolOptions(String iconImage, int symbolCount) { - final geometry = LatLng( - center.latitude + sin(symbolCount * pi / 6.0) / 20.0, - center.longitude + cos(symbolCount * pi / 6.0) / 20.0, - ); - return iconImage == 'customFont' - ? SymbolOptions( - geometry: geometry, - iconImage: 'custom-marker', - //'airport-15', - fontNames: ['DIN Offc Pro Bold', 'Arial Unicode MS Regular'], - textField: 'Airport', - textSize: 12.5, - textOffset: const Offset(0, 0.8), - textAnchor: 'top', - textColor: '#000000', - textHaloBlur: 1, - textHaloColor: '#ffffff', - textHaloWidth: 0.8, - ) - : SymbolOptions( - geometry: geometry, - textField: 'Airport', - textOffset: const Offset(0, 0.8), - iconImage: iconImage, - ); - } - - Future _addAll(String iconImage) async { - final symbolsToAddNumbers = Iterable.generate(12).toList(); - for (final s in controller!.symbols) { - symbolsToAddNumbers.removeWhere((i) => i == s.data!['count']); - } - - if (symbolsToAddNumbers.isNotEmpty) { - final symbolOptionsList = symbolsToAddNumbers - .map((i) => _getSymbolOptions(iconImage, i)) - .toList(); - controller!.addSymbols(symbolOptionsList, - symbolsToAddNumbers.map((i) => {'count': i}).toList()); - - setState(() { - _symbolCount += symbolOptionsList.length; - }); - } - } - - Future _remove() async { - await controller!.removeSymbol(_selectedSymbol!); - setState(() { - _selectedSymbol = null; - _symbolCount -= 1; - }); - } - - Future _removeAll() async { - await controller!.removeSymbols(controller!.symbols); - setState(() { - _selectedSymbol = null; - _symbolCount = 0; - }); - } - - Future _changePosition() async { - final current = _selectedSymbol!.options.geometry!; - final offset = Offset( - center.latitude - current.latitude, - center.longitude - current.longitude, - ); - await _updateSelectedSymbol( - SymbolOptions( - geometry: LatLng( - center.latitude + offset.dy, - center.longitude + offset.dx, - ), - ), - ); - } - - Future _changeIconOffset() async { - var currentAnchor = _selectedSymbol!.options.iconOffset; - currentAnchor ??= Offset.zero; - final newAnchor = Offset(1.0 - currentAnchor.dy, currentAnchor.dx); - await _updateSelectedSymbol(SymbolOptions(iconOffset: newAnchor)); - } - - Future _changeIconAnchor() async { - var current = _selectedSymbol!.options.iconAnchor; - if (current == null || current == 'center') { - current = 'bottom'; - } else { - current = 'center'; - } - await _updateSelectedSymbol( - SymbolOptions(iconAnchor: current), - ); - } - - Future _toggleDraggable() async { - var draggable = _selectedSymbol!.options.draggable; - draggable ??= false; - - await _updateSelectedSymbol( - SymbolOptions(draggable: !draggable), - ); - } - - Future _changeAlpha() async { - var current = _selectedSymbol!.options.iconOpacity; - current ??= 1.0; - - await _updateSelectedSymbol( - SymbolOptions(iconOpacity: current < 0.1 ? 1.0 : current * 0.75), - ); - } - - Future _changeRotation() async { - var current = _selectedSymbol!.options.iconRotate; - current ??= 0; - await _updateSelectedSymbol( - SymbolOptions(iconRotate: current == 330.0 ? 0.0 : current + 30.0), - ); - } - - Future _toggleVisible() async { - var current = _selectedSymbol!.options.iconOpacity; - current ??= 1.0; - - await _updateSelectedSymbol( - SymbolOptions(iconOpacity: current == 0.0 ? 1.0 : 0.0), - ); - } - - Future _changeZIndex() async { - var current = _selectedSymbol!.options.zIndex; - current ??= 0; - await _updateSelectedSymbol( - SymbolOptions(zIndex: current == 12 ? 0 : current + 1), - ); - } - - void _getLatLng() { - final latLng = controller!.getSymbolLatLng(_selectedSymbol!); - if (!mounted) return; - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(latLng.toString()), - ), - ); - } - - Future _changeIconOverlap() async { - setState(() { - _iconAllowOverlap = !_iconAllowOverlap; - }); - await controller!.setSymbolIconAllowOverlap(_iconAllowOverlap); - await controller!.setSymbolTextAllowOverlap(_iconAllowOverlap); - } - - @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: _onStyleLoaded, - 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( - 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'), - ), - ], - ), - ), - ), - ), - ], - ); - } -} diff --git a/maplibre_gl_example/lib/pmtiles.dart b/maplibre_gl_example/lib/pmtiles.dart deleted file mode 100644 index 578cc6b62..000000000 --- a/maplibre_gl_example/lib/pmtiles.dart +++ /dev/null @@ -1,54 +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 PMTilesPage extends ExamplePage { - const PMTilesPage({super.key}) - : super(const Icon(Icons.map), 'PMTiles example'); - - @override - Widget build(BuildContext context) { - return const PMTilesMap(); - } -} - -class PMTilesMap extends StatefulWidget { - const PMTilesMap({super.key}); - - @override - State createState() => PMTilesMapState(); -} - -class PMTilesMapState 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), - styleString: "assets/pmtiles_style.json", - ), - ); - } - - void _moveCameraToNullIsland() => mapController.future.then( - (c) => c.animateCamera(CameraUpdate.newCameraPosition(_nullIsland))); -} diff --git a/maplibre_gl_example/lib/presentation/gps_location/gps_location_page.dart b/maplibre_gl_example/lib/presentation/gps_location/gps_location_page.dart deleted file mode 100644 index fecf07bef..000000000 --- a/maplibre_gl_example/lib/presentation/gps_location/gps_location_page.dart +++ /dev/null @@ -1,124 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:location/location.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; -import 'package:maplibre_gl_example/page.dart' show ExamplePage; - -class GpsLocationPage extends ExamplePage { - const GpsLocationPage({super.key}) - : super( - const Icon(Icons.gps_fixed), - 'GPS Location', - needsLocationPermission: false, - ); - - @override - Widget build(BuildContext context) { - return const GpsLocationMap(); - } -} - -class GpsLocationMap extends HookWidget { - const GpsLocationMap({super.key}); - - CameraPosition get initialLocation => const CameraPosition( - target: LatLng(37.3, -121.8), - zoom: 7, - ); - - @override - Widget build(BuildContext context) { - final mapController = useState(null); - final useDefaultLocationSettings = useState(true); - return Scaffold( - floatingActionButtonLocation: - FloatingActionButtonLocation.miniCenterFloat, - body: Stack( - children: [ - MapLibreMap( - onMapCreated: (controller) { - mapController.value = controller; - }, - styleString: "assets/osm_style.json", - compassEnabled: true, - myLocationEnabled: true, - trackCameraPosition: true, - locationEnginePlatforms: switch (useDefaultLocationSettings.value) { - false => const LocationEnginePlatforms( - androidPlatform: LocationEngineAndroidProperties( - interval: 1000, - displacement: 1, - priority: LocationPriority.highAccuracy, - ), - ), - true => LocationEnginePlatforms.defaultPlatform, - }, - initialCameraPosition: initialLocation, - ), - Column( - children: [ - GpsIcon(onTapAndPermissionGranted: () async { - final currentLocation = await Location().getLocation(); - print("Current location: $currentLocation"); - mapController.value?.animateCamera( - CameraUpdate.newLatLngZoom( - LatLng( - currentLocation.latitude!, - currentLocation.longitude!, - ), - 9), - ); - }), - FloatingActionButton( - heroTag: "accuracy", - onPressed: () { - useDefaultLocationSettings.value = - !useDefaultLocationSettings.value; - }, - child: Text(switch (useDefaultLocationSettings.value) { - true => "Default", - false => "high", - }), - ), - ], - ) - ], - ), - ); - } -} - -class GpsIcon extends HookWidget { - final Future Function() onTapAndPermissionGranted; - - const GpsIcon({super.key, required this.onTapAndPermissionGranted}); - - @override - Widget build(BuildContext context) { - final locationLoading = useState(false); - return FloatingActionButton.small( - heroTag: "gps", - onPressed: () async { - if (locationLoading.value) { - return; - } - locationLoading.value = true; - var hasPermissions = await Location().hasPermission(); - if (hasPermissions != PermissionStatus.granted) { - hasPermissions = await Location().requestPermission(); - } - if (hasPermissions == PermissionStatus.granted) { - await onTapAndPermissionGranted(); - } - locationLoading.value = false; - }, - child: switch (locationLoading.value) { - true => const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator(strokeWidth: 2)), - false => const Icon(Icons.gps_fixed), - }, - ); - } -} diff --git a/maplibre_gl_example/lib/scrolling_map.dart b/maplibre_gl_example/lib/scrolling_map.dart deleted file mode 100644 index 2a6e72890..000000000 --- a/maplibre_gl_example/lib/scrolling_map.dart +++ /dev/null @@ -1,143 +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/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; -import 'package:maplibre_gl_example/util.dart'; - -import 'page.dart'; - -class ScrollingMapPage extends ExamplePage { - const ScrollingMapPage({super.key}) - : super(const Icon(Icons.map), 'Scrolling map'); - - @override - Widget build(BuildContext context) { - return const ScrollingMapBody(); - } -} - -class ScrollingMapBody extends StatefulWidget { - const ScrollingMapBody({super.key}); - - @override - State createState() => _ScrollingMapBodyState(); -} - -class _ScrollingMapBodyState extends State { - late MapLibreMapController controllerOne; - late MapLibreMapController controllerTwo; - - final LatLng center = const LatLng(32.080664, 34.9563837); - - @override - Widget build(BuildContext context) { - return ListView( - children: [ - Card( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 30.0), - child: Column( - children: [ - const Padding( - padding: EdgeInsets.only(bottom: 12.0), - child: Text('This map consumes all touch events.'), - ), - Center( - child: SizedBox( - width: 300.0, - height: 300.0, - child: MapLibreMap( - onMapCreated: onMapCreatedOne, - onStyleLoadedCallback: () => onStyleLoaded(controllerOne), - initialCameraPosition: CameraPosition( - target: center, - zoom: 11.0, - ), - gestureRecognizers: >{ - Factory( - () => EagerGestureRecognizer(), - ), - }, - ), - ), - ), - ], - ), - ), - ), - Card( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 30.0), - child: Column( - children: [ - const Text("This map doesn't consume the vertical drags."), - const Padding( - padding: EdgeInsets.only(bottom: 12.0), - child: - Text('It still gets other gestures (e.g scale or tap).'), - ), - Center( - child: SizedBox( - width: 300.0, - height: 300.0, - child: MapLibreMap( - onMapCreated: onMapCreatedTwo, - onStyleLoadedCallback: () => onStyleLoaded(controllerTwo), - initialCameraPosition: CameraPosition( - target: center, - zoom: 11.0, - ), - gestureRecognizers: >{ - Factory( - () => ScaleGestureRecognizer(), - ), - }, - ), - ), - ), - ], - ), - ), - ), - ], - ); - } - - 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")); - 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: 7.0, - lineOpacity: 0.5, - ), - ); - } -} diff --git a/maplibre_gl_example/lib/shared/constants.dart b/maplibre_gl_example/lib/shared/constants.dart new file mode 100644 index 000000000..ec7301066 --- /dev/null +++ b/maplibre_gl_example/lib/shared/constants.dart @@ -0,0 +1,146 @@ +import 'package:flutter/material.dart'; +import 'package:maplibre_gl/maplibre_gl.dart'; + +/// Common constants used across examples +class ExampleConstants { + ExampleConstants._(); + + // ============================================================================ + // Map Coordinates + // ============================================================================ + + /// Default map center (Sydney, Australia) + static const LatLng sydneyCenter = LatLng(-33.86711, 151.1947171); + + /// Alternative coordinates for variety + static const LatLng sanFrancisco = LatLng(37.7749, -122.4194); + static const LatLng london = LatLng(51.5074, -0.1278); + static const LatLng tokyo = LatLng(35.6762, 139.6503); + + // ============================================================================ + // Map Configuration + // ============================================================================ + + /// Default zoom level + static const double defaultZoom = 4.0; + + /// Default map bounds (Australia region) + static final LatLngBounds defaultBounds = LatLngBounds( + southwest: const LatLng(-34.0, 150.5), + northeast: const LatLng(-33.5, 151.5), + ); + + // ============================================================================ + // Spacing & Layout + // ============================================================================ + + /// Standard padding for content + static const double paddingStandard = 16.0; + + /// Small padding + static const double paddingSmall = 8.0; + + /// Tiny padding for tight spaces + static const double paddingTiny = 4.0; + + /// Spacing between buttons in control panels + static const double buttonSpacing = 8.0; + + /// Run spacing for wrapped button groups + static const double buttonRunSpacing = 8.0; + + /// Map height ratio (portion of screen height) + static const double mapHeightRatio = 0.5; + + /// Border radius for cards and containers + static const double borderRadius = 12.0; + + // ============================================================================ + // Map Styles + // ============================================================================ + + /// Demo map style URL (default) + static const String demoMapStyle = + 'https://demotiles.maplibre.org/style.json'; + + /// Style asset paths + static const String osmStyleAsset = 'assets/osm_style.json'; + static const String pmtilesStyleAsset = 'assets/pmtiles_style.json'; + static const String translucenStyleAsset = 'assets/translucent_style.json'; + static const String localStyleAsset = 'assets/style.json'; + + // ============================================================================ + // Colors + // ============================================================================ + + /// Primary accent color for UI elements + static const Color primaryColor = Color(0xFF1976D2); + + /// Secondary accent color + static const Color secondaryColor = Color(0xFF388E3C); + + /// Error/destructive action color + static const Color errorColor = Color(0xFFD32F2F); + + /// Warning color + static const Color warningColor = Color(0xFFFFA726); + + // ============================================================================ + // Animation Durations + // ============================================================================ + + /// Standard animation duration + static const Duration animationDuration = Duration(milliseconds: 300); + + /// Camera animation duration + static const Duration cameraAnimationDuration = Duration(milliseconds: 1000); + + /// Long animation duration + static const Duration longAnimationDuration = Duration(milliseconds: 2000); + + // ============================================================================ + // Camera Positions + // ============================================================================ + + /// Default camera position + static CameraPosition defaultCameraPosition = toCameraPosition( + const LatLng(0.0, 0.0), + ); + + /// Camera position for Sydney + static CameraPosition sydneyCameraPosition = toCameraPosition(sydneyCenter); + + /// Camera position for San Francisco + static CameraPosition sanFranciscoCameraPosition = + toCameraPosition(sanFrancisco); + + /// Camera position for London + static CameraPosition londonCameraPosition = toCameraPosition(london); + + /// Camera position for Tokyo + static CameraPosition tokyoCameraPosition = toCameraPosition(tokyo); + + /// Helper to create a CameraPosition with default zoom. + static CameraPosition toCameraPosition( + LatLng target, [ + double zoom = defaultZoom, + ]) { + return CameraPosition(target: target, zoom: zoom); + } + + // ============================================================================ + // Map Settings + // ============================================================================ + + /// Default tilt angle + static const double defaultTilt = 0.0; + + /// Default bearing (rotation) + static const double defaultBearing = 0.0; + + /// Minimum zoom level + static const double minZoom = 0.0; + + /// Maximum zoom level + static const double maxZoom = 22.0; +} diff --git a/maplibre_gl_example/lib/shared/extensions.dart b/maplibre_gl_example/lib/shared/extensions.dart new file mode 100644 index 000000000..45c3a4882 --- /dev/null +++ b/maplibre_gl_example/lib/shared/extensions.dart @@ -0,0 +1,8 @@ +/// String extensions for common operations +extension StringExtensions on String { + /// Capitalizes the first letter of the string + String capitalize() { + if (isEmpty) return this; + return '${this[0].toUpperCase()}${substring(1)}'; + } +} diff --git a/maplibre_gl_example/lib/shared/shared.dart b/maplibre_gl_example/lib/shared/shared.dart new file mode 100644 index 000000000..74c2d166c --- /dev/null +++ b/maplibre_gl_example/lib/shared/shared.dart @@ -0,0 +1,10 @@ +// Shared widgets +export 'widgets/map_example_scaffold.dart'; +export 'widgets/example_button.dart'; +export 'widgets/color_picker.dart'; + +// Constants +export 'constants.dart'; + +// Extensions +export 'extensions.dart'; diff --git a/maplibre_gl_example/lib/shared/widgets/color_picker.dart b/maplibre_gl_example/lib/shared/widgets/color_picker.dart new file mode 100644 index 000000000..964f8c6c0 --- /dev/null +++ b/maplibre_gl_example/lib/shared/widgets/color_picker.dart @@ -0,0 +1,177 @@ +import 'package:flutter/material.dart'; + +/// A reusable color picker widget that displays a modal bottom sheet +/// with a grid of color options +class ColorPickerModal { + /// Shows a color picker modal and returns the selected color + /// + /// [context] - The build context + /// [title] - Optional title for the picker (defaults to "Select Color") + /// [currentColor] - The currently selected color (for highlighting) + /// [colorFormat] - Whether to return hex string or Color object + static Future show({ + required BuildContext context, + String title = 'Select Color', + Color? currentColor, + ColorFormat colorFormat = ColorFormat.color, + }) async { + final colorOptions = _getDefaultColors(); + + return showModalBottomSheet( + context: context, + builder: (context) => Container( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + title, + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 16), + Wrap( + spacing: 8, + runSpacing: 8, + children: colorOptions.entries.map((entry) { + final color = entry.value; + final isSelected = currentColor == color; + + return InkWell( + onTap: () { + final result = colorFormat == ColorFormat.hex + ? _colorToHex(color) + : color; + Navigator.pop(context, result); + }, + child: Container( + width: 64, + height: 64, + decoration: BoxDecoration( + color: color, + border: Border.all( + color: isSelected ? Colors.blue : Colors.grey, + width: isSelected ? 3 : 2, + ), + borderRadius: BorderRadius.circular(8), + ), + child: entry.key.isNotEmpty + ? Center( + child: Text( + entry.key, + textAlign: TextAlign.center, + style: TextStyle( + color: _getContrastColor(color), + fontSize: 10, + fontWeight: FontWeight.bold, + shadows: const [ + Shadow( + color: Colors.black45, + blurRadius: 2, + ), + ], + ), + ), + ) + : null, + ), + ); + }).toList(), + ), + ], + ), + ), + ); + } + + /// Shows a color picker and returns a hex color string + static Future showForHex({ + required BuildContext context, + String title = 'Select Color', + String? currentHexColor, + }) async { + final currentColor = currentHexColor != null + ? _hexToColor(currentHexColor) + : null; + + return await show( + context: context, + title: title, + currentColor: currentColor, + colorFormat: ColorFormat.hex, + ); + } + + /// Default color palette + static Map _getDefaultColors() { + return { + 'Red': const Color(0xFFE74C3C), + 'Blue': const Color(0xFF3498DB), + 'Green': const Color(0xFF2ECC71), + 'Yellow': const Color(0xFFF1C40F), + 'Purple': const Color(0xFF9B59B6), + 'Orange': const Color(0xFFE67E22), + 'Pink': const Color(0xFFEC7063), + 'Teal': const Color(0xFF1ABC9C), + 'Cyan': const Color(0xFF00BCD4), + 'Indigo': const Color(0xFF5C6BC0), + 'White': Colors.white, + 'Black': Colors.black, + }; + } + + /// Converts a Color to hex string format (#RRGGBB) + static String _colorToHex(Color color) { + return '#${color.toARGB32().toRadixString(16).substring(2).toUpperCase()}'; + } + + /// Converts a hex string to Color + static Color _hexToColor(String hex) { + final hexCode = hex.replaceAll('#', ''); + return Color(int.parse(hexCode, radix: 16) + 0xFF000000); + } + + /// Get contrasting color for text (white or black) + static Color _getContrastColor(Color color) { + final luminance = (0.299 * color.r + 0.587 * color.g + 0.114 * color.b) / 255; + return luminance > 0.5 ? Colors.black : Colors.white; + } +} + +/// Format for the color picker return value +enum ColorFormat { + /// Returns a Color object + color, + + /// Returns a hex string (#RRGGBB) + hex, +} + +/// A simple widget that displays a color swatch with an optional border +class ColorSwatch extends StatelessWidget { + final Color color; + final double size; + final bool isSelected; + + const ColorSwatch({ + super.key, + required this.color, + this.size = 48, + this.isSelected = false, + }); + + @override + Widget build(BuildContext context) { + return Container( + width: size, + height: size, + decoration: BoxDecoration( + color: color, + border: Border.all( + color: isSelected ? Colors.blue : Colors.grey, + width: isSelected ? 3 : 1, + ), + borderRadius: BorderRadius.circular(4), + ), + ); + } +} diff --git a/maplibre_gl_example/lib/shared/widgets/example_button.dart b/maplibre_gl_example/lib/shared/widgets/example_button.dart new file mode 100644 index 000000000..eea54c8e3 --- /dev/null +++ b/maplibre_gl_example/lib/shared/widgets/example_button.dart @@ -0,0 +1,328 @@ +import 'package:flutter/material.dart'; + +/// A styled button component for map examples with Material 3 design +class ExampleButton extends StatelessWidget { + /// Button label text + final String label; + + /// Callback when button is pressed + final VoidCallback? onPressed; + + /// Button style variant + final ExampleButtonStyle style; + + /// Optional icon + final IconData? icon; + + const ExampleButton({ + super.key, + required this.label, + this.onPressed, + this.style = ExampleButtonStyle.filled, + this.icon, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + switch (style) { + case ExampleButtonStyle.filled: + return icon != null + ? FilledButton.icon( + onPressed: onPressed, + icon: Icon(icon), + label: Text(label), + ) + : FilledButton( + onPressed: onPressed, + child: Text(label), + ); + + case ExampleButtonStyle.tonal: + return icon != null + ? FilledButton.tonalIcon( + onPressed: onPressed, + icon: Icon(icon), + label: Text(label), + ) + : FilledButton.tonal( + onPressed: onPressed, + child: Text(label), + ); + + case ExampleButtonStyle.outlined: + return icon != null + ? OutlinedButton.icon( + onPressed: onPressed, + icon: Icon(icon), + label: Text(label), + ) + : OutlinedButton( + onPressed: onPressed, + child: Text(label), + ); + + case ExampleButtonStyle.text: + return icon != null + ? TextButton.icon( + onPressed: onPressed, + icon: Icon(icon), + label: Text(label), + ) + : TextButton( + onPressed: onPressed, + child: Text(label), + ); + + case ExampleButtonStyle.destructive: + return icon != null + ? FilledButton.tonalIcon( + onPressed: onPressed, + icon: Icon(icon), + label: Text(label), + style: FilledButton.styleFrom( + backgroundColor: theme.colorScheme.errorContainer, + foregroundColor: theme.colorScheme.onErrorContainer, + ), + ) + : FilledButton.tonal( + onPressed: onPressed, + style: FilledButton.styleFrom( + backgroundColor: theme.colorScheme.errorContainer, + foregroundColor: theme.colorScheme.onErrorContainer, + ), + child: Text(label), + ); + } + } +} + +/// Button style variants +enum ExampleButtonStyle { + /// Filled button (primary action) + filled, + + /// Tonal button (secondary action) + tonal, + + /// Outlined button (tertiary action) + outlined, + + /// Text button (low emphasis) + text, + + /// Destructive action (remove, delete, clear) + destructive, +} + +/// A group of buttons with a label +class ControlGroup extends StatelessWidget { + /// Group title/label + final String title; + + /// Buttons in this group + final List children; + + /// Whether to show as a vertical list. Defaults to false (horizontal wrap). + final bool vertical; + + /// Whether to wrap the group in a Card. Defaults to true. + final bool wrapInCard; + + const ControlGroup({ + super.key, + required this.title, + required this.children, + this.vertical = false, + this.wrapInCard = true, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + if (wrapInCard) { + return Card( + child: Padding( + padding: const EdgeInsets.all(12.0), + child: SizedBox(width: double.infinity, child: _buildContent(theme)), + ), + ); + } + return _buildContent(theme); + } + + Widget _buildContent(ThemeData theme) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(left: 4, bottom: 8), + child: Text( + title, + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + color: theme.colorScheme.primary, + ), + ), + ), + if (vertical) + Padding( + padding: const EdgeInsets.only(left: 4.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: children, + ), + ) + else + Wrap( + spacing: 8.0, + runSpacing: 8.0, + children: children, + ), + ], + ); + } +} + +/// A segmented button group for mutually exclusive options +class ExampleSegmentedButton extends StatelessWidget { + /// Available options + final List> segments; + + /// Currently selected value + final T selected; + + /// Callback when selection changes + final ValueChanged onSelectionChanged; + + const ExampleSegmentedButton({ + super.key, + required this.segments, + required this.selected, + required this.onSelectionChanged, + }); + + @override + Widget build(BuildContext context) { + return SegmentedButton( + segments: segments + .map((segment) => ButtonSegment( + value: segment.value, + label: Text(segment.label), + icon: segment.icon != null ? Icon(segment.icon) : null, + )) + .toList(), + selected: {selected}, + onSelectionChanged: (Set newSelection) { + if (newSelection.isNotEmpty) { + onSelectionChanged(newSelection.first); + } + }, + ); + } +} + +/// A segment for ExampleSegmentedButton +class ExampleSegment { + final T value; + final String label; + final IconData? icon; + + const ExampleSegment({ + required this.value, + required this.label, + this.icon, + }); +} + +/// A toggle switch with label +class ExampleSwitch extends StatelessWidget { + final String label; + final bool value; + final ValueChanged onChanged; + + const ExampleSwitch({ + super.key, + required this.label, + required this.value, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + return SwitchListTile( + title: Text(label), + value: value, + onChanged: onChanged, + contentPadding: EdgeInsets.zero, + ); + } +} + +/// An info card to display status or information +class InfoCard extends StatelessWidget { + final String title; + final String? subtitle; + final IconData? icon; + final Color? color; + + const InfoCard({ + super.key, + required this.title, + this.subtitle, + this.icon, + this.color, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final cardColor = color ?? theme.colorScheme.secondaryContainer; + final textColor = color != null + ? ThemeData.estimateBrightnessForColor(color!) == Brightness.light + ? Colors.black87 + : Colors.white + : theme.colorScheme.onSecondaryContainer; + + return Card( + color: cardColor, + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + children: [ + if (icon != null) ...[ + Icon(icon, color: textColor), + const SizedBox(width: 12), + ], + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + title, + style: theme.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w600, + color: textColor, + ), + ), + if (subtitle != null) + Text( + subtitle!, + style: theme.textTheme.bodySmall?.copyWith( + color: textColor.withValues(alpha: 0.8), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/maplibre_gl_example/lib/shared/widgets/map_example_scaffold.dart b/maplibre_gl_example/lib/shared/widgets/map_example_scaffold.dart new file mode 100644 index 000000000..9cb51729c --- /dev/null +++ b/maplibre_gl_example/lib/shared/widgets/map_example_scaffold.dart @@ -0,0 +1,179 @@ +import 'package:flutter/material.dart'; +import 'package:maplibre_gl/maplibre_gl.dart'; +import '../constants.dart'; + +/// A reusable scaffold for map examples that provides a consistent layout: +/// - Map on top (50% of screen by default) +/// - Scrollable control panel below +/// - Automatic handling of responsive sizing +class MapExampleScaffold extends StatelessWidget { + /// The MapLibre map widget + final MapLibreMap map; + + /// Control widgets to display below the map + final List controls; + + /// Optional title for the example (shown in app bar if provided) + final String? title; + + /// Map height ratio (0.0 to 1.0). Defaults to 0.5 (50% of screen) + final double mapHeightRatio; + + /// Whether to show the app bar. Defaults to false. + final bool showAppBar; + + /// Optional floating action button + final Widget? floatingActionButton; + + /// Alignment of control buttons. Defaults to center. + final WrapAlignment controlsAlignment; + + /// Padding around controls. Defaults to standard padding. + final EdgeInsets? controlsPadding; + + /// Whether to wrap controls in a Card. Defaults to false. + final bool wrapInCard; + + const MapExampleScaffold({ + super.key, + required this.map, + required this.controls, + this.title, + this.mapHeightRatio = ExampleConstants.mapHeightRatio, + this.showAppBar = false, + this.floatingActionButton, + this.controlsAlignment = WrapAlignment.start, + this.controlsPadding, + this.wrapInCard = false, + }) : assert(mapHeightRatio > 0.0 && mapHeightRatio <= 1.0, + 'mapHeightRatio must be between 0.0 and 1.0'); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final screenHeight = MediaQuery.of(context).size.height; + final mapHeight = screenHeight * mapHeightRatio; + + final controlsWidget = _buildControls(theme); + + final body = Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: mapHeight, + child: map, + ), + Expanded( + child: SingleChildScrollView( + child: controlsWidget, + ), + ), + ], + ); + + return Scaffold( + appBar: showAppBar && title != null + ? AppBar( + title: Text(title!), + elevation: 0, + ) + : null, + body: body, + floatingActionButton: floatingActionButton, + ); + } + + Widget _buildControls(ThemeData theme) { + final padding = controlsPadding ?? + const EdgeInsets.all(ExampleConstants.paddingStandard); + + final wrappedControls = Wrap( + runSpacing: ExampleConstants.buttonRunSpacing, + alignment: controlsAlignment, + children: controls, + ); + + if (wrapInCard) { + return Padding( + padding: padding, + child: Card( + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(ExampleConstants.borderRadius), + ), + child: Padding( + padding: const EdgeInsets.all(ExampleConstants.paddingStandard), + child: wrappedControls, + ), + ), + ); + } + + return Padding( + padding: padding, + child: wrappedControls, + ); + } +} + +/// A builder variant that accepts a child builder for more complex layouts +class MapExampleScaffoldBuilder extends StatelessWidget { + /// The MapLibre map widget + final MapLibreMap map; + + /// Builder for the control panel content + final Widget Function(BuildContext context) controlsBuilder; + + /// Optional title for the example + final String? title; + + /// Map height ratio (0.0 to 1.0). Defaults to 0.5 (50% of screen) + final double mapHeightRatio; + + /// Whether to show the app bar. Defaults to false. + final bool showAppBar; + + /// Optional floating action button + final Widget? floatingActionButton; + + const MapExampleScaffoldBuilder({ + super.key, + required this.map, + required this.controlsBuilder, + this.title, + this.mapHeightRatio = ExampleConstants.mapHeightRatio, + this.showAppBar = false, + this.floatingActionButton, + }); + + @override + Widget build(BuildContext context) { + final screenHeight = MediaQuery.of(context).size.height; + final mapHeight = screenHeight * mapHeightRatio; + + final body = Column( + children: [ + SizedBox( + height: mapHeight, + child: map, + ), + Expanded( + child: SingleChildScrollView( + child: controlsBuilder(context), + ), + ), + ], + ); + + return Scaffold( + appBar: showAppBar && title != null + ? AppBar( + title: Text(title!), + elevation: 0, + ) + : null, + body: body, + floatingActionButton: floatingActionButton, + ); + } +} diff --git a/maplibre_gl_example/lib/translucent_full_map.dart b/maplibre_gl_example/lib/translucent_full_map.dart index 9aa61b1b5..d43a2b93d 100644 --- a/maplibre_gl_example/lib/translucent_full_map.dart +++ b/maplibre_gl_example/lib/translucent_full_map.dart @@ -1,45 +1,61 @@ import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; - -import 'page.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.map), 'Translucent full screen map'); + : super( + const Icon(Icons.layers), + 'Translucent map', + category: ExampleCategory.advanced, + ); @override - Widget build(BuildContext context) { - return const TranslucentFullMap(); - } + Widget build(BuildContext context) => const _TranslucentMapBody(); } -class TranslucentFullMap extends StatefulWidget { - const TranslucentFullMap({super.key}); +class _TranslucentMapBody extends StatefulWidget { + const _TranslucentMapBody(); @override - State createState() => TranslucentFullMapState(); + State<_TranslucentMapBody> createState() => _TranslucentMapBodyState(); } -class TranslucentFullMapState extends State { - final Completer mapController = Completer(); - bool canInteractWithMap = false; +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 _moveCameraToNullIsland() async { + await _mapController?.animateCamera( + CameraUpdate.newCameraPosition(_nullIsland), + ); + setState(() => _canReset = true); + } + + Future _resetCamera() async { + await _mapController?.animateCamera( + CameraUpdate.newCameraPosition(ExampleConstants.defaultCameraPosition), + ); + setState(() => _canReset = 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: Stack( children: [ const ColoredBox( @@ -47,17 +63,22 @@ class TranslucentFullMapState extends State { child: Center( child: Text( 'Any widget can be here', - style: TextStyle(fontSize: 20), + style: TextStyle( + fontSize: 20, + color: Colors.white, + fontWeight: FontWeight.bold, + ), ), ), ), MapLibreMap( - styleString: _styleString, - onMapCreated: (controller) => mapController.complete(controller), - initialCameraPosition: _nullIsland, - onStyleLoadedCallback: () => setState( - () => canInteractWithMap = true, - ), + 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. @@ -65,11 +86,23 @@ class TranslucentFullMapState extends State { ), ], ), + floatingActionButton: ExampleButton( + label: _canReset ? 'Reset camera' : 'Go to Null Island', + icon: _canReset ? Icons.refresh : Icons.flight_takeoff, + onPressed: _canInteractWithMap + ? _canReset + ? _resetCamera + : _moveCameraToNullIsland + : null, + style: ExampleButtonStyle.tonal, + ), + floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, ); } - String get _styleString => 'assets/translucent_style.json'; - - void _moveCameraToNullIsland() => mapController.future.then( - (c) => c.animateCamera(CameraUpdate.newCameraPosition(_nullIsland))); + @override + void dispose() { + _mapController?.dispose(); + super.dispose(); + } } diff --git a/maplibre_gl_platform_interface/lib/src/ui.dart b/maplibre_gl_platform_interface/lib/src/ui.dart index abeebfc98..a4064f237 100644 --- a/maplibre_gl_platform_interface/lib/src/ui.dart +++ b/maplibre_gl_platform_interface/lib/src/ui.dart @@ -40,6 +40,14 @@ enum AttributionButtonPosition { bottomRight, } +/// Logo View Position +enum LogoViewPosition { + topLeft, + topRight, + bottomLeft, + bottomRight, +} + /// Bounds for the map camera target. /// Used with [_MapLibreMapOptions] to wrap a [LatLngBounds] value. This allows /// distinguishing between specifying an unbounded target (null `LatLngBounds`) diff --git a/maplibre_gl_web/lib/src/convert.dart b/maplibre_gl_web/lib/src/convert.dart index b890aa9b4..9a93d4b36 100644 --- a/maplibre_gl_web/lib/src/convert.dart +++ b/maplibre_gl_web/lib/src/convert.dart @@ -63,6 +63,10 @@ class Convert { if (options.containsKey('myLocationRenderMode')) { sink.setMyLocationRenderMode(options['myLocationRenderMode']); } + if (options.containsKey('logoViewPosition')) { + final position = LogoViewPosition.values[options['logoViewPosition']]; + sink.setLogoViewAlignment(position); + } if (options.containsKey('logoViewMargins')) { sink.setLogoViewMargins( options['logoViewMargins'][0], options['logoViewMargins'][1]); diff --git a/maplibre_gl_web/lib/src/maplibre_web_gl_platform.dart b/maplibre_gl_web/lib/src/maplibre_web_gl_platform.dart index c067a15b6..57f9b25ad 100644 --- a/maplibre_gl_web/lib/src/maplibre_web_gl_platform.dart +++ b/maplibre_gl_web/lib/src/maplibre_web_gl_platform.dart @@ -803,6 +803,11 @@ class MapLibreMapController extends MapLibrePlatform print('setCompassViewMargins not available in web'); } + @override + void setLogoViewAlignment(LogoViewPosition position) { + print('setLogoViewAlignment not available in web'); + } + @override void setLogoViewMargins(int x, int y) { print('setLogoViewMargins not available in web'); diff --git a/maplibre_gl_web/lib/src/options_sink.dart b/maplibre_gl_web/lib/src/options_sink.dart index d3afcee80..527848b91 100644 --- a/maplibre_gl_web/lib/src/options_sink.dart +++ b/maplibre_gl_web/lib/src/options_sink.dart @@ -27,6 +27,8 @@ abstract class MapLibreMapOptionsSink { void setMyLocationRenderMode(int myLocationRenderMode); + void setLogoViewAlignment(LogoViewPosition position); + void setLogoViewMargins(int x, int y); void setCompassAlignment(CompassViewPosition position); From 12dfad23a56d12163cc218712782d557b9a38ba6 Mon Sep 17 00:00:00 2001 From: Gabriel Palmisano Date: Tue, 6 Jan 2026 21:26:54 +0100 Subject: [PATCH 22/38] fix: update setPaintProperty and setLayoutProperty to handle nullable JSAny values. Improve jsify function to create JS arrays correctly --- .../lib/src/interop/ui/map_interop.dart | 4 +- .../lib/src/maplibre_web_gl_platform.dart | 56 ++++++++++--------- maplibre_gl_web/lib/src/ui/map.dart | 15 +++-- maplibre_gl_web/lib/src/utils.dart | 3 +- 4 files changed, 45 insertions(+), 33 deletions(-) diff --git a/maplibre_gl_web/lib/src/interop/ui/map_interop.dart b/maplibre_gl_web/lib/src/interop/ui/map_interop.dart index 33e5b28f8..cc7c7bd2d 100644 --- a/maplibre_gl_web/lib/src/interop/ui/map_interop.dart +++ b/maplibre_gl_web/lib/src/interop/ui/map_interop.dart @@ -760,7 +760,7 @@ extension MapLibreMapJsImplExtension on MapLibreMapJsImpl { /// @see [Change a layer's color with buttons](https://maplibre.org/maplibre-gl-js/docs/examples/color-switcher/) /// @see [Adjust a layer's opacity](https://maplibre.org/maplibre-gl-js/docs/examples/adjust-layer-opacity/) /// @see [Create a draggable point](https://maplibre.org/maplibre-gl-js/docs/examples/drag-a-point/) - external void setPaintProperty(String layerId, String name, JSAny value, + external void setPaintProperty(String layerId, String name, JSAny? value, [StyleSetterOptionsJsImpl? options]); /// Returns the value of a paint property in the specified style layer. @@ -781,7 +781,7 @@ extension MapLibreMapJsImplExtension on MapLibreMapJsImpl { /// @example /// map.setLayoutProperty('my-layer', 'visibility', 'none'); external MapLibreMapJsImpl setLayoutProperty( - String layerId, String name, JSAny value, + String layerId, String name, JSAny? value, [StyleSetterOptionsJsImpl? options]); /// Returns the value of a layout property in the specified style layer. diff --git a/maplibre_gl_web/lib/src/maplibre_web_gl_platform.dart b/maplibre_gl_web/lib/src/maplibre_web_gl_platform.dart index 57f9b25ad..13fe994fd 100644 --- a/maplibre_gl_web/lib/src/maplibre_web_gl_platform.dart +++ b/maplibre_gl_web/lib/src/maplibre_web_gl_platform.dart @@ -385,10 +385,10 @@ class MapLibreMapController extends MapLibrePlatform final pointAsList = [point.x, point.y]; return _map .queryRenderedFeatures([pointAsList, pointAsList], options) - .map((feature) => { + .map((feature) => { 'type': 'Feature', 'id': feature.id, - 'geometry': { + 'geometry': { 'type': feature.geometry.type, 'coordinates': feature.geometry.coordinates, }, @@ -421,10 +421,10 @@ class MapLibreMapController extends MapLibrePlatform [rect.left, rect.bottom], [rect.right, rect.top], ], options) - .map((feature) => { + .map((feature) => { 'type': 'Feature', 'id': feature.id, - 'geometry': { + 'geometry': { 'type': feature.geometry.type, 'coordinates': feature.geometry.coordinates, }, @@ -457,10 +457,10 @@ class MapLibreMapController extends MapLibrePlatform return _map .querySourceFeatures(sourceId, parameters) - .map((feature) => { + .map((feature) => { 'type': 'Feature', 'id': feature.id, - 'geometry': { + 'geometry': { 'type': feature.geometry.type, 'coordinates': feature.geometry.coordinates, }, @@ -948,7 +948,7 @@ class MapLibreMapController extends MapLibrePlatform {String? promoteId}) async { final data = _makeFeatureCollection(geojson); _addedFeaturesByLayer[sourceId] = data; - _map.addSource(sourceId, { + _map.addSource(sourceId, { "type": 'geojson', "data": geojson, // pass the raw string here to avoid errors if (promoteId != null) "promoteId": promoteId @@ -956,12 +956,19 @@ class MapLibreMapController extends MapLibrePlatform } Feature _makeFeature(Map geojsonFeature) { + final geometry = + Map.from(geojsonFeature["geometry"] as Map); + final propertiesRaw = geojsonFeature["properties"]; + final properties = propertiesRaw != null + ? Map.from(propertiesRaw as Map) + : null; + return Feature( - geometry: Geometry( - type: geojsonFeature["geometry"]["type"], - coordinates: geojsonFeature["geometry"]["coordinates"]), - properties: geojsonFeature["properties"], - id: geojsonFeature["properties"]?["id"] ?? geojsonFeature["id"]); + geometry: Geometry( + type: geometry["type"], coordinates: geometry["coordinates"]), + properties: properties, + id: properties?["id"] ?? geojsonFeature["id"], + ); } FeatureCollection _makeFeatureCollection(Map geojson) { @@ -1066,19 +1073,18 @@ class MapLibreMapController extends MapLibrePlatform Future setLayerProperties( String layerId, Map properties) async { for (final entry in properties.entries) { - // Very hacky: because we don't know if the property is a layout - // or paint property, we try to set it as both. - try { - _map.setLayoutProperty(layerId, entry.key, entry.value); - } catch (e) { - print( - 'Caught exception (usually safe to ignore): $e.\nLayerId: $layerId, Property: ${entry.key}, Value: ${entry.value}'); - } + // Try paint property first (most common), then layout property try { _map.setPaintProperty(layerId, entry.key, entry.value); } catch (e) { - print( - 'Caught exception (usually safe to ignore): $e.\nLayerId: $layerId, Property: ${entry.key}, Value: ${entry.value}'); + // If setPaintProperty fails, try setLayoutProperty + try { + _map.setLayoutProperty(layerId, entry.key, entry.value); + } catch (e) { + // If both fail, the property doesn't exist on this layer type + print( + 'Warning: Could not set property "${entry.key}" on layer "$layerId" for value "${entry.value}": $e'); + } } } } @@ -1154,12 +1160,12 @@ class MapLibreMapController extends MapLibrePlatform double? maxzoom, dynamic filter, required bool enableInteraction}) async { - final layout = Map.fromEntries( + final layout = Map.fromEntries( properties.entries.where((entry) => isLayoutProperty(entry.key))); - final paint = Map.fromEntries( + final paint = Map.fromEntries( properties.entries.where((entry) => !isLayoutProperty(entry.key))); - _map.addLayer({ + _map.addLayer({ 'id': layerId, 'type': layerType, 'source': sourceId, diff --git a/maplibre_gl_web/lib/src/ui/map.dart b/maplibre_gl_web/lib/src/ui/map.dart index 629162fee..a4518d505 100644 --- a/maplibre_gl_web/lib/src/ui/map.dart +++ b/maplibre_gl_web/lib/src/ui/map.dart @@ -865,8 +865,10 @@ class MapLibreMap extends Camera { /// @see [Adjust a layer's opacity](https://maplibre.org/maplibre-gl-js/docs/examples/adjust-layer-opacity/) /// @see [Create a draggable point](https://maplibre.org/maplibre-gl-js/docs/examples/drag-a-point/) setPaintProperty(String layerId, String name, dynamic value, - [StyleSetterOptions? options]) => - jsObject.setPaintProperty(layerId, name, utils.jsify(value)!); + [StyleSetterOptions? options]) { + final jsValue = utils.jsify(value); + jsObject.setPaintProperty(layerId, name, jsValue); + } /// Returns the value of a paint property in the specified style layer. /// @@ -887,9 +889,12 @@ class MapLibreMap extends Camera { /// @example /// map.setLayoutProperty('my-layer', 'visibility', 'none'); MapLibreMap setLayoutProperty(String layerId, String name, dynamic value, - [StyleSetterOptions? options]) => - MapLibreMap.fromJsObject( - jsObject.setLayoutProperty(layerId, name, utils.jsify(value)!)); + [StyleSetterOptions? options]) { + final jsValue = utils.jsify(value); + return MapLibreMap.fromJsObject( + jsObject.setLayoutProperty(layerId, name, jsValue), + ); + } /// Returns the value of a layout property in the specified style layer. /// diff --git a/maplibre_gl_web/lib/src/utils.dart b/maplibre_gl_web/lib/src/utils.dart index b1be72593..ac43dfe04 100644 --- a/maplibre_gl_web/lib/src/utils.dart +++ b/maplibre_gl_web/lib/src/utils.dart @@ -45,7 +45,8 @@ JSAny? jsify(Object? dartObject) { if (dartObject is num) return dartObject.toJS; if (dartObject is bool) return dartObject.toJS; if (dartObject is List) { - return dartObject.map((e) => jsify(e)).toList().toJS as JSAny; + final jsArray = dartObject.map((e) => jsify(e)).toList(); + return jsArray.toJS; } if (dartObject is Map) { return jsifyMap(dartObject); From 2b550ed1107d6c91a983bb9efbbcf9c716a66146 Mon Sep 17 00:00:00 2001 From: Gabriel Palmisano Date: Tue, 6 Jan 2026 21:32:33 +0100 Subject: [PATCH 23/38] refactor: Improved layers examples. Improved MapLibreMapController disposing. Fixed lineDasharray and patterns reset to null in layer_properties.dart --- maplibre_gl/lib/src/layer_properties.dart | 8 +- maplibre_gl/lib/src/maplibre_map.dart | 10 +- .../advanced/translucent_full_map.dart | 2 - .../examples/basics/gps_location_page.dart | 1 - .../camera/camera_bounds_example.dart | 2 - .../examples/layers/circle_layer_example.dart | 5 +- .../examples/layers/fill_layer_example.dart | 62 ++++++- .../examples/layers/line_layer_example.dart | 160 +++++++++++++++++- .../examples/layers/symbol_layer_example.dart | 5 +- .../lib/shared/widgets/color_picker.dart | 10 +- 10 files changed, 236 insertions(+), 29 deletions(-) diff --git a/maplibre_gl/lib/src/layer_properties.dart b/maplibre_gl/lib/src/layer_properties.dart index f4e5735aa..0fd142c78 100644 --- a/maplibre_gl/lib/src/layer_properties.dart +++ b/maplibre_gl/lib/src/layer_properties.dart @@ -1549,8 +1549,8 @@ class LineLayerProperties implements LayerProperties { addIfPresent('line-gap-width', lineGapWidth); addIfPresent('line-offset', lineOffset); addIfPresent('line-blur', lineBlur); - addIfPresent('line-dasharray', lineDasharray); - addIfPresent('line-pattern', linePattern); + json['line-dasharray'] = lineDasharray; + json['line-pattern'] = linePattern; addIfPresent('line-gradient', lineGradient); addIfPresent('line-cap', lineCap); addIfPresent('line-join', lineJoin); @@ -1734,7 +1734,7 @@ class FillLayerProperties implements LayerProperties { addIfPresent('fill-outline-color', fillOutlineColor); addIfPresent('fill-translate', fillTranslate); addIfPresent('fill-translate-anchor', fillTranslateAnchor); - addIfPresent('fill-pattern', fillPattern); + json['fill-pattern'] = fillPattern; addIfPresent('fill-sort-key', fillSortKey); addIfPresent('visibility', visibility); return json; @@ -1914,7 +1914,7 @@ class FillExtrusionLayerProperties implements LayerProperties { addIfPresent('fill-extrusion-translate', fillExtrusionTranslate); addIfPresent( 'fill-extrusion-translate-anchor', fillExtrusionTranslateAnchor); - addIfPresent('fill-extrusion-pattern', fillExtrusionPattern); + json['fill-extrusion-pattern'] = fillExtrusionPattern; addIfPresent('fill-extrusion-height', fillExtrusionHeight); addIfPresent('fill-extrusion-base', fillExtrusionBase); addIfPresent( diff --git a/maplibre_gl/lib/src/maplibre_map.dart b/maplibre_gl/lib/src/maplibre_map.dart index 8ce2f03c0..87f4ba509 100644 --- a/maplibre_gl/lib/src/maplibre_map.dart +++ b/maplibre_gl/lib/src/maplibre_map.dart @@ -291,6 +291,7 @@ class MapLibreMap extends StatefulWidget { class _MapLibreMapState extends State { final Completer _controller = Completer(); + MapLibreMapController? _mapController; late _MapLibreMapOptions _maplibreMapOptions; final MapLibrePlatform _maplibrePlatform = MapLibrePlatform.createInstance(); @@ -322,12 +323,12 @@ class _MapLibreMapState extends State { } @override - Future dispose() async { - super.dispose(); + void dispose() { if (_controller.isCompleted) { - final controller = await _controller.future; - controller.dispose(); + _mapController?.dispose(); } + + super.dispose(); } @override @@ -375,6 +376,7 @@ class _MapLibreMapState extends State { annotationConsumeTapEvents: widget.annotationConsumeTapEvents, ); await _maplibrePlatform.initPlatform(id); + _mapController = controller; _controller.complete(controller); widget.onMapCreated?.call(controller); } diff --git a/maplibre_gl_example/lib/examples/advanced/translucent_full_map.dart b/maplibre_gl_example/lib/examples/advanced/translucent_full_map.dart index da62be019..cd4d2fb68 100644 --- a/maplibre_gl_example/lib/examples/advanced/translucent_full_map.dart +++ b/maplibre_gl_example/lib/examples/advanced/translucent_full_map.dart @@ -4,8 +4,6 @@ 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}) diff --git a/maplibre_gl_example/lib/examples/basics/gps_location_page.dart b/maplibre_gl_example/lib/examples/basics/gps_location_page.dart index c19860b73..611440055 100644 --- a/maplibre_gl_example/lib/examples/basics/gps_location_page.dart +++ b/maplibre_gl_example/lib/examples/basics/gps_location_page.dart @@ -31,7 +31,6 @@ class _GpsLocationBody extends StatefulWidget { class _GpsLocationBodyState extends State<_GpsLocationBody> { MapLibreMapController? _controller; LocationData? _currentLocation; - bool _isLoadingLocation = false; bool _useHighAccuracy = false; PermissionStatus? _permissionStatus; MyLocationTrackingMode _trackingMode = MyLocationTrackingMode.none; diff --git a/maplibre_gl_example/lib/examples/camera/camera_bounds_example.dart b/maplibre_gl_example/lib/examples/camera/camera_bounds_example.dart index 401375c87..8df1b9944 100644 --- a/maplibre_gl_example/lib/examples/camera/camera_bounds_example.dart +++ b/maplibre_gl_example/lib/examples/camera/camera_bounds_example.dart @@ -119,7 +119,6 @@ class _CameraBoundsBodyState extends State<_CameraBoundsBody> { initialCameraPosition: ExampleConstants.defaultCameraPosition, minMaxZoomPreference: MinMaxZoomPreference(_minZoom, _maxZoom), ), - controls: [ InfoCard( title: 'Visible Bounds', @@ -136,7 +135,6 @@ class _CameraBoundsBodyState extends State<_CameraBoundsBody> { ), const SizedBox(height: 8), ControlGroup( - title: 'Move to Bounds', children: [ ExampleButton( diff --git a/maplibre_gl_example/lib/examples/layers/circle_layer_example.dart b/maplibre_gl_example/lib/examples/layers/circle_layer_example.dart index 102032e5c..39d10cb1d 100644 --- a/maplibre_gl_example/lib/examples/layers/circle_layer_example.dart +++ b/maplibre_gl_example/lib/examples/layers/circle_layer_example.dart @@ -1,3 +1,4 @@ +import 'dart:developer' as dev; import 'dart:math'; import 'package:flutter/material.dart'; @@ -90,6 +91,7 @@ class _CircleLayerBodyState extends State<_CircleLayerBody> { setState(() {}); } catch (e) { + dev.log('Error adding circle layer: $e'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error adding circle layer: $e')), @@ -140,9 +142,10 @@ class _CircleLayerBodyState extends State<_CircleLayerBody> { ), ); } catch (e) { + dev.log('Error updating circle layer: $e'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Error updating layer: $e')), + SnackBar(content: Text('Error updating circle layer: $e')), ); } } diff --git a/maplibre_gl_example/lib/examples/layers/fill_layer_example.dart b/maplibre_gl_example/lib/examples/layers/fill_layer_example.dart index 5578905ec..a9f60739c 100644 --- a/maplibre_gl_example/lib/examples/layers/fill_layer_example.dart +++ b/maplibre_gl_example/lib/examples/layers/fill_layer_example.dart @@ -1,7 +1,9 @@ +import 'dart:developer' as dev; 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'; @@ -37,7 +39,9 @@ class _FillLayerBodyState extends State<_FillLayerBody> { Color _fillOutlineColor = const Color(0xFF2C3E50); double _fillTranslateX = 0.0; double _fillTranslateY = 0.0; + String _fillTranslateAnchor = 'map'; bool _fillAntialias = true; + String? _fillPattern; @override void initState() { @@ -49,9 +53,25 @@ class _FillLayerBodyState extends State<_FillLayerBody> { } Future _onStyleLoaded() async { + await _loadPatternImages(); await _addFillLayer(); } + Future _loadPatternImages() async { + if (_controller == null) return; + + try { + // Load cat silhouette pattern from assets + await addImageFromAsset( + _controller!, + 'pattern-cat', + 'assets/fill/cat_silhouette_pattern.png', + ); + } catch (e) { + dev.log('Error loading pattern images: $e'); + } + } + Future _addFillLayer() async { if (_controller == null) return; @@ -75,12 +95,15 @@ class _FillLayerBodyState extends State<_FillLayerBody> { fillOutlineColor: '#${_fillOutlineColor.toARGB32().toRadixString(16).substring(2)}', fillTranslate: [_fillTranslateX, _fillTranslateY], + fillTranslateAnchor: _fillTranslateAnchor, fillAntialias: _fillAntialias, + fillPattern: _fillPattern, ), ); setState(() {}); } catch (e) { + dev.log('Error adding fill layer: $e'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error adding fill layer: $e')), @@ -131,13 +154,16 @@ class _FillLayerBodyState extends State<_FillLayerBody> { fillOutlineColor: '#${_fillOutlineColor.toARGB32().toRadixString(16).substring(2)}', fillTranslate: [_fillTranslateX, _fillTranslateY], + fillTranslateAnchor: _fillTranslateAnchor, fillAntialias: _fillAntialias, + fillPattern: _fillPattern, ), ); } catch (e) { + dev.log('Error updating fill layer: $e'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Error updating layer: $e')), + SnackBar(content: Text('Error updating fill layer: $e')), ); } } @@ -249,6 +275,38 @@ class _FillLayerBodyState extends State<_FillLayerBody> { }, ), ), + ListTile( + title: const Text('Translate Anchor'), + subtitle: const Text('Reference frame for translation'), + trailing: SegmentedButton( + segments: const [ + ButtonSegment(value: 'map', label: Text('Map')), + ButtonSegment( + value: 'viewport', label: Text('Viewport')), + ], + selected: {_fillTranslateAnchor}, + onSelectionChanged: (Set selected) async { + setState(() => _fillTranslateAnchor = selected.first); + await _updateLayer(); + }, + ), + ), + ], + ), + ControlGroup( + title: 'Pattern', + children: [ + SwitchListTile( + value: _fillPattern != null, + title: const Text('Fill Pattern'), + subtitle: Text(_fillPattern ?? 'None'), + onChanged: (bool value) async { + setState(() { + _fillPattern = value ? 'pattern-cat' : null; + }); + await _updateLayer(); + }, + ), ], ), ControlGroup( @@ -263,7 +321,9 @@ class _FillLayerBodyState extends State<_FillLayerBody> { _fillOutlineColor = const Color(0xFF2C3E50); _fillTranslateX = 0.0; _fillTranslateY = 0.0; + _fillTranslateAnchor = 'map'; _fillAntialias = true; + _fillPattern = null; }); 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 index 382bcde68..9adef8eb8 100644 --- a/maplibre_gl_example/lib/examples/layers/line_layer_example.dart +++ b/maplibre_gl_example/lib/examples/layers/line_layer_example.dart @@ -1,7 +1,9 @@ +import 'dart:developer' as dev; 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'; @@ -44,6 +46,8 @@ class _LineLayerBodyState extends State<_LineLayerBody> { String _lineJoin = 'round'; // bevel, round, miter double _lineMiterLimit = 2.0; double _lineRoundLimit = 1.05; + String? _linePattern; + _LineDashStyle _lineDasharray = _LineDashStyle.solid; @override void initState() { @@ -55,22 +59,38 @@ class _LineLayerBodyState extends State<_LineLayerBody> { } Future _onStyleLoaded() async { + await _loadPatternImages(); + + // Add GeoJSON source with multiple lines + await _controller!.addGeoJsonSource( + _sourceId, + { + 'type': 'FeatureCollection', + 'features': _generateRandomLines(5), + }, + ); await _addLineLayer(); } - Future _addLineLayer() async { + Future _loadPatternImages() async { if (_controller == null) return; try { - // Add GeoJSON source with multiple lines - await _controller!.addGeoJsonSource( - _sourceId, - { - 'type': 'FeatureCollection', - 'features': _generateRandomLines(5), - }, + // Load cat silhouette pattern from assets + await addImageFromAsset( + _controller!, + 'pattern-cat', + 'assets/fill/cat_silhouette_pattern.png', ); + } catch (e) { + dev.log('Error loading pattern images: $e'); + } + } + + Future _addLineLayer() async { + if (_controller == null) return; + try { // Add line layer await _controller!.addLineLayer( _sourceId, @@ -87,11 +107,14 @@ class _LineLayerBodyState extends State<_LineLayerBody> { lineJoin: _lineJoin, lineMiterLimit: _lineMiterLimit, lineRoundLimit: _lineRoundLimit, + linePattern: _linePattern, + lineDasharray: _lineDasharray.dashArray, ), ); setState(() {}); } catch (e) { + dev.log('Error adding line layer: $e'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error adding line layer: $e')), @@ -148,9 +171,12 @@ class _LineLayerBodyState extends State<_LineLayerBody> { lineJoin: _lineJoin, lineMiterLimit: _lineMiterLimit, lineRoundLimit: _lineRoundLimit, + linePattern: _linePattern, + lineDasharray: _lineDasharray.dashArray, ), ); } catch (e) { + dev.log('Error updating line layer: $e'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error updating layer: $e')), @@ -411,6 +437,33 @@ class _LineLayerBodyState extends State<_LineLayerBody> { ), ], ), + ControlGroup( + title: 'Pattern', + children: [ + SwitchListTile( + value: _linePattern != null, + title: const Text('Line Pattern'), + subtitle: Text(_linePattern ?? 'None'), + onChanged: (bool value) async { + setState(() { + _linePattern = value ? 'pattern-cat' : null; + }); + await _updateLayer(); + }, + ), + ], + ), + ControlGroup( + title: 'Dash Array', + children: [ + ListTile( + title: const Text('Dash Style'), + subtitle: Text(_lineDasharray.label), + trailing: const Icon(Icons.arrow_forward_ios, size: 16), + onTap: () => _selectDashArray(), + ), + ], + ), ControlGroup( title: 'Actions', children: [ @@ -430,6 +483,8 @@ class _LineLayerBodyState extends State<_LineLayerBody> { _lineJoin = 'round'; _lineMiterLimit = 2.0; _lineRoundLimit = 1.05; + _linePattern = null; + _lineDasharray = _LineDashStyle.solid; }); await _updateLayer(); }, @@ -452,4 +507,93 @@ class _LineLayerBodyState extends State<_LineLayerBody> { await _updateLayer(); } } + + Future _selectDashArray() async { + final result = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Select Dash Style'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text(_LineDashStyle.solid.label), + subtitle: const Text('No dashes'), + leading: Icon( + _lineDasharray == _LineDashStyle.solid + ? Icons.radio_button_checked + : Icons.radio_button_unchecked, + ), + onTap: () => Navigator.pop(context, _LineDashStyle.solid), + ), + ListTile( + title: Text(_LineDashStyle.dotted.label), + subtitle: Text(_LineDashStyle.dotted.dashArray.toString()), + leading: Icon( + _lineDasharray == _LineDashStyle.dotted + ? Icons.radio_button_checked + : Icons.radio_button_unchecked, + ), + onTap: () => Navigator.pop(context, _LineDashStyle.dotted), + ), + ListTile( + title: Text(_LineDashStyle.dashed.label), + subtitle: Text(_LineDashStyle.dashed.dashArray.toString()), + leading: Icon( + _lineDasharray == _LineDashStyle.dashed + ? Icons.radio_button_checked + : Icons.radio_button_unchecked, + ), + onTap: () => Navigator.pop(context, _LineDashStyle.dashed), + ), + ListTile( + title: Text(_LineDashStyle.dashDot.label), + subtitle: Text(_LineDashStyle.dashDot.dashArray.toString()), + leading: Icon( + _lineDasharray == _LineDashStyle.dashDot + ? Icons.radio_button_checked + : Icons.radio_button_unchecked, + ), + onTap: () => Navigator.pop(context, _LineDashStyle.dashDot), + ), + ListTile( + title: Text(_LineDashStyle.custom.label), + subtitle: Text(_LineDashStyle.custom.dashArray.toString()), + leading: Icon( + _lineDasharray == _LineDashStyle.custom + ? Icons.radio_button_checked + : Icons.radio_button_unchecked, + ), + onTap: () => Navigator.pop(context, _LineDashStyle.custom), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancel'), + ), + ], + ), + ); + + // Only update if a selection was made (not cancelled) + if (result != null && result is _LineDashStyle) { + dev.log('Selected dash style: ${result.label}'); + setState(() => _lineDasharray = result); + await _updateLayer(); + } + } +} + +enum _LineDashStyle { + solid(label: 'Solid', dashArray: null), + dotted(label: 'Dotted', dashArray: [0.1, 2]), + dashed(label: 'Dashed', dashArray: [6, 3]), + dashDot(label: 'Dash Dot', dashArray: [6, 2, 0.1, 2]), + custom(label: 'Custom', dashArray: [10.0, 5.0, 2.0, 5.0]); + + final String label; + final List? dashArray; + const _LineDashStyle({required this.label, required this.dashArray}); } diff --git a/maplibre_gl_example/lib/examples/layers/symbol_layer_example.dart b/maplibre_gl_example/lib/examples/layers/symbol_layer_example.dart index dac2e825a..45cb02723 100644 --- a/maplibre_gl_example/lib/examples/layers/symbol_layer_example.dart +++ b/maplibre_gl_example/lib/examples/layers/symbol_layer_example.dart @@ -1,3 +1,4 @@ +import 'dart:developer' as dev; import 'dart:math'; import 'package:flutter/material.dart'; @@ -128,6 +129,7 @@ class _SymbolLayerBodyState extends State<_SymbolLayerBody> { setState(() {}); } catch (e) { + dev.log('Error adding symbol layer: $e'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error adding symbol layer: $e')), @@ -206,9 +208,10 @@ class _SymbolLayerBodyState extends State<_SymbolLayerBody> { ), ); } catch (e) { + dev.log('Error updating symbol layer: $e'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Error updating layer: $e')), + SnackBar(content: Text('Error updating symbol layer: $e')), ); } } diff --git a/maplibre_gl_example/lib/shared/widgets/color_picker.dart b/maplibre_gl_example/lib/shared/widgets/color_picker.dart index 964f8c6c0..e9593afc2 100644 --- a/maplibre_gl_example/lib/shared/widgets/color_picker.dart +++ b/maplibre_gl_example/lib/shared/widgets/color_picker.dart @@ -89,9 +89,8 @@ class ColorPickerModal { String title = 'Select Color', String? currentHexColor, }) async { - final currentColor = currentHexColor != null - ? _hexToColor(currentHexColor) - : null; + final currentColor = + currentHexColor != null ? _hexToColor(currentHexColor) : null; return await show( context: context, @@ -132,7 +131,8 @@ class ColorPickerModal { /// Get contrasting color for text (white or black) static Color _getContrastColor(Color color) { - final luminance = (0.299 * color.r + 0.587 * color.g + 0.114 * color.b) / 255; + final luminance = + (0.299 * color.r + 0.587 * color.g + 0.114 * color.b) / 255; return luminance > 0.5 ? Colors.black : Colors.white; } } @@ -141,7 +141,7 @@ class ColorPickerModal { enum ColorFormat { /// Returns a Color object color, - + /// Returns a hex string (#RRGGBB) hex, } From 9ce52a62941792304ebf6eefa90fb178c042fec1 Mon Sep 17 00:00:00 2001 From: Gabriel Palmisano Date: Tue, 6 Jan 2026 23:32:38 +0100 Subject: [PATCH 24/38] refactor: Fixed setLayerProperties and pattern images on web and android. Refactor upload images on web, now all are converted to RGBA format. --- .../maplibregl/LayerPropertyConverter.java | 18 ++++- .../maplibregl/MapLibreMapController.java | 14 +++- maplibre_gl/lib/src/controller.dart | 11 +++- maplibre_gl/lib/src/layer_properties.dart | 66 ++++++++----------- .../lib/src/maplibre_web_gl_platform.dart | 8 ++- scripts/lib/generate.dart | 1 + .../LayerPropertyConverter.java.template | 10 +++ .../templates/layer_properties.dart.template | 9 ++- 8 files changed, 87 insertions(+), 50 deletions(-) diff --git a/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/LayerPropertyConverter.java b/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/LayerPropertyConverter.java index b4b08ddce..2531e908e 100644 --- a/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/LayerPropertyConverter.java +++ b/maplibre_gl/android/src/main/java/org/maplibre/maplibregl/LayerPropertyConverter.java @@ -373,7 +373,11 @@ static PropertyValue[] interpretLineLayerProperties(Object o) { } break; case "line-pattern": - properties.add(PropertyFactory.linePattern(expression)); + if (jsonElement.isJsonPrimitive() && jsonElement.getAsJsonPrimitive().isString()) { + properties.add(PropertyFactory.linePattern(jsonElement.getAsString())); + } else { + properties.add(PropertyFactory.linePattern(expression)); + } break; case "line-gradient": properties.add(PropertyFactory.lineGradient(expression)); @@ -441,7 +445,11 @@ static PropertyValue[] interpretFillLayerProperties(Object o) { properties.add(PropertyFactory.fillTranslateAnchor(expression)); break; case "fill-pattern": - properties.add(PropertyFactory.fillPattern(expression)); + if (jsonElement.isJsonPrimitive() && jsonElement.getAsJsonPrimitive().isString()) { + properties.add(PropertyFactory.fillPattern(jsonElement.getAsString())); + } else { + properties.add(PropertyFactory.fillPattern(expression)); + } break; case "fill-sort-key": properties.add(PropertyFactory.fillSortKey(expression)); @@ -488,7 +496,11 @@ static PropertyValue[] interpretFillExtrusionLayerProperties(Object o) { properties.add(PropertyFactory.fillExtrusionTranslateAnchor(expression)); break; case "fill-extrusion-pattern": - properties.add(PropertyFactory.fillExtrusionPattern(expression)); + if (jsonElement.isJsonPrimitive() && jsonElement.getAsJsonPrimitive().isString()) { + properties.add(PropertyFactory.fillExtrusionPattern(jsonElement.getAsString())); + } else { + properties.add(PropertyFactory.fillExtrusionPattern(expression)); + } break; case "fill-extrusion-height": properties.add(PropertyFactory.fillExtrusionHeight(expression)); 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 05e4b4fcd..873a4d8be 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 @@ -1486,9 +1486,21 @@ public void onFailure(@NonNull Exception exception) { "The style is null. Has onStyleLoaded() already been invoked?", null); } + // Configure bitmap options to prevent density-based scaling + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inScaled = false; // Disable automatic scaling + options.inDensity = 0; // No source density + options.inTargetDensity = 0; // No target density + + Bitmap bitmap = BitmapFactory.decodeByteArray( + call.argument("bytes"), + 0, + call.argument("length"), + options); + style.addImage( call.argument("name"), - BitmapFactory.decodeByteArray(call.argument("bytes"), 0, call.argument("length")), + bitmap, call.argument("sdf")); result.success(null); break; diff --git a/maplibre_gl/lib/src/controller.dart b/maplibre_gl/lib/src/controller.dart index bd8248fe0..158dfd599 100644 --- a/maplibre_gl/lib/src/controller.dart +++ b/maplibre_gl/lib/src/controller.dart @@ -530,9 +530,16 @@ class MapLibreMapController extends ChangeNotifier { /// /// The returned [Future] completes after the change has been made on the /// platform side. + /// + /// NOTE: The [properties] will not skip null values, so setting a property to null will potentially reset it to default. Future setLayerProperties( - String layerId, LayerProperties properties) async { - await _maplibrePlatform.setLayerProperties(layerId, properties.toJson()); + String layerId, + LayerProperties properties, + ) async { + await _maplibrePlatform.setLayerProperties( + layerId, + properties.toJson(skipNulls: false), + ); } /// Add a fill layer to the map with the given properties diff --git a/maplibre_gl/lib/src/layer_properties.dart b/maplibre_gl/lib/src/layer_properties.dart index 0fd142c78..7e0e38d28 100644 --- a/maplibre_gl/lib/src/layer_properties.dart +++ b/maplibre_gl/lib/src/layer_properties.dart @@ -4,7 +4,7 @@ part of '../maplibre_gl.dart'; abstract class LayerProperties { - Map toJson(); + Map toJson({bool skipNulls = true}); } class SymbolLayerProperties implements LayerProperties { @@ -903,13 +903,12 @@ class SymbolLayerProperties implements LayerProperties { } @override - Map toJson() { + Map toJson({bool skipNulls = true}) { final json = {}; void addIfPresent(String fieldName, dynamic value) { - if (value != null) { - json[fieldName] = value; - } + if (value == null && skipNulls) return; + json[fieldName] = value; } addIfPresent('icon-opacity', iconOpacity); @@ -1228,13 +1227,12 @@ class CircleLayerProperties implements LayerProperties { } @override - Map toJson() { + Map toJson({bool skipNulls = true}) { final json = {}; void addIfPresent(String fieldName, dynamic value) { - if (value != null) { - json[fieldName] = value; - } + if (value == null && skipNulls) return; + json[fieldName] = value; } addIfPresent('circle-radius', circleRadius); @@ -1532,13 +1530,12 @@ class LineLayerProperties implements LayerProperties { } @override - Map toJson() { + Map toJson({bool skipNulls = true}) { final json = {}; void addIfPresent(String fieldName, dynamic value) { - if (value != null) { - json[fieldName] = value; - } + if (value == null && skipNulls) return; + json[fieldName] = value; } addIfPresent('line-opacity', lineOpacity); @@ -1549,8 +1546,8 @@ class LineLayerProperties implements LayerProperties { addIfPresent('line-gap-width', lineGapWidth); addIfPresent('line-offset', lineOffset); addIfPresent('line-blur', lineBlur); - json['line-dasharray'] = lineDasharray; - json['line-pattern'] = linePattern; + addIfPresent('line-dasharray', lineDasharray); + addIfPresent('line-pattern', linePattern); addIfPresent('line-gradient', lineGradient); addIfPresent('line-cap', lineCap); addIfPresent('line-join', lineJoin); @@ -1719,13 +1716,12 @@ class FillLayerProperties implements LayerProperties { } @override - Map toJson() { + Map toJson({bool skipNulls = true}) { final json = {}; void addIfPresent(String fieldName, dynamic value) { - if (value != null) { - json[fieldName] = value; - } + if (value == null && skipNulls) return; + json[fieldName] = value; } addIfPresent('fill-antialias', fillAntialias); @@ -1734,7 +1730,7 @@ class FillLayerProperties implements LayerProperties { addIfPresent('fill-outline-color', fillOutlineColor); addIfPresent('fill-translate', fillTranslate); addIfPresent('fill-translate-anchor', fillTranslateAnchor); - json['fill-pattern'] = fillPattern; + addIfPresent('fill-pattern', fillPattern); addIfPresent('fill-sort-key', fillSortKey); addIfPresent('visibility', visibility); return json; @@ -1900,13 +1896,12 @@ class FillExtrusionLayerProperties implements LayerProperties { } @override - Map toJson() { + Map toJson({bool skipNulls = true}) { final json = {}; void addIfPresent(String fieldName, dynamic value) { - if (value != null) { - json[fieldName] = value; - } + if (value == null && skipNulls) return; + json[fieldName] = value; } addIfPresent('fill-extrusion-opacity', fillExtrusionOpacity); @@ -1914,7 +1909,7 @@ class FillExtrusionLayerProperties implements LayerProperties { addIfPresent('fill-extrusion-translate', fillExtrusionTranslate); addIfPresent( 'fill-extrusion-translate-anchor', fillExtrusionTranslateAnchor); - json['fill-extrusion-pattern'] = fillExtrusionPattern; + addIfPresent('fill-extrusion-pattern', fillExtrusionPattern); addIfPresent('fill-extrusion-height', fillExtrusionHeight); addIfPresent('fill-extrusion-base', fillExtrusionBase); addIfPresent( @@ -2077,13 +2072,12 @@ class RasterLayerProperties implements LayerProperties { } @override - Map toJson() { + Map toJson({bool skipNulls = true}) { final json = {}; void addIfPresent(String fieldName, dynamic value) { - if (value != null) { - json[fieldName] = value; - } + if (value == null && skipNulls) return; + json[fieldName] = value; } addIfPresent('raster-opacity', rasterOpacity); @@ -2227,13 +2221,12 @@ class HillshadeLayerProperties implements LayerProperties { } @override - Map toJson() { + Map toJson({bool skipNulls = true}) { final json = {}; void addIfPresent(String fieldName, dynamic value) { - if (value != null) { - json[fieldName] = value; - } + if (value == null && skipNulls) return; + json[fieldName] = value; } addIfPresent( @@ -2357,13 +2350,12 @@ class HeatmapLayerProperties implements LayerProperties { } @override - Map toJson() { + Map toJson({bool skipNulls = true}) { final json = {}; void addIfPresent(String fieldName, dynamic value) { - if (value != null) { - json[fieldName] = value; - } + if (value == null && skipNulls) return; + json[fieldName] = value; } addIfPresent('heatmap-radius', heatmapRadius); diff --git a/maplibre_gl_web/lib/src/maplibre_web_gl_platform.dart b/maplibre_gl_web/lib/src/maplibre_web_gl_platform.dart index 13fe994fd..e52b8a20f 100644 --- a/maplibre_gl_web/lib/src/maplibre_web_gl_platform.dart +++ b/maplibre_gl_web/lib/src/maplibre_web_gl_platform.dart @@ -505,14 +505,18 @@ class MapLibreMapController extends MapLibrePlatform [bool sdf = false]) async { final photo = decodeImage(bytes)!; if (!_map.hasImage(name)) { + // Convert image to RGBA format with proper byte ordering + final rgbaBytes = photo.convert(numChannels: 4).getBytes(); + final data = Uint8List.fromList(rgbaBytes); + await _map.addImage( name, { 'width': photo.width, 'height': photo.height, - 'data': photo.getBytes(), + 'data': data, }, - {'sdf': sdf}, + {'sdf': sdf, 'pixelRatio': 1}, ); } else { dev.log('Image already exists on map: $name', diff --git a/scripts/lib/generate.dart b/scripts/lib/generate.dart index d0df94f13..f74e87e17 100644 --- a/scripts/lib/generate.dart +++ b/scripts/lib/generate.dart @@ -171,6 +171,7 @@ Map buildStyleProperty( 'value': key, 'isFloatArrayProperty': typeDart == "List" && nestedTypeDart == "double", 'isVisibilityProperty': key == "visibility", + 'isPatternProperty': key.endsWith("-pattern"), 'requiresLiteral': key == "icon-image", 'isIosAsCamelCase': renamedIosProperties.containsKey(camelCase), 'iosAsCamelCase': renamedIosProperties[camelCase], diff --git a/scripts/templates/LayerPropertyConverter.java.template b/scripts/templates/LayerPropertyConverter.java.template index 8b0c1236d..94479b32d 100644 --- a/scripts/templates/LayerPropertyConverter.java.template +++ b/scripts/templates/LayerPropertyConverter.java.template @@ -31,8 +31,18 @@ class LayerPropertyConverter { switch (entry.getKey()) { {{#paint_properties}} {{^isFloatArrayProperty}} + {{^isPatternProperty}} case "{{value}}": properties.add(PropertyFactory.{{valueAsCamelCase}}(expression)); + {{/isPatternProperty}} + {{#isPatternProperty}} + case "{{value}}": + if (jsonElement.isJsonPrimitive() && jsonElement.getAsJsonPrimitive().isString()) { + properties.add(PropertyFactory.{{valueAsCamelCase}}(jsonElement.getAsString())); + } else { + properties.add(PropertyFactory.{{valueAsCamelCase}}(expression)); + } + {{/isPatternProperty}} {{/isFloatArrayProperty}} {{#isFloatArrayProperty}} case "{{value}}": diff --git a/scripts/templates/layer_properties.dart.template b/scripts/templates/layer_properties.dart.template index bef01c502..d909e5fd3 100644 --- a/scripts/templates/layer_properties.dart.template +++ b/scripts/templates/layer_properties.dart.template @@ -4,7 +4,7 @@ part of '../maplibre_gl.dart'; abstract class LayerProperties { - Map toJson(); + Map toJson({bool skipNulls = true}); } {{#layerTypes}} @@ -46,13 +46,12 @@ class {{typePascal}}LayerProperties implements LayerProperties { } @override - Map toJson() { + Map toJson({bool skipNulls = true}) { final json = {}; void addIfPresent(String fieldName, dynamic value) { - if (value != null) { - json[fieldName] = value; - } + if (value == null && skipNulls) return; + json[fieldName] = value; } {{#paint_properties}} From 8fe20784f735a76aae73a0c82977b983ffeb928c Mon Sep 17 00:00:00 2001 From: Gabriel Palmisano Date: Tue, 6 Jan 2026 23:33:15 +0100 Subject: [PATCH 25/38] chore: changed cat icon to marker icon for pattern examples --- .../{fill => pattern}/cat_silhouette_pattern.png | Bin .../assets/pattern/marker_pattern.png | Bin 0 -> 1172 bytes .../lib/examples/layers/fill_layer_example.dart | 9 +++++---- .../lib/examples/layers/line_layer_example.dart | 8 ++++---- maplibre_gl_example/lib/shared/constants.dart | 11 +++++++++++ maplibre_gl_example/pubspec.yaml | 3 ++- 6 files changed, 22 insertions(+), 9 deletions(-) rename maplibre_gl_example/assets/{fill => pattern}/cat_silhouette_pattern.png (100%) create mode 100644 maplibre_gl_example/assets/pattern/marker_pattern.png diff --git a/maplibre_gl_example/assets/fill/cat_silhouette_pattern.png b/maplibre_gl_example/assets/pattern/cat_silhouette_pattern.png similarity index 100% rename from maplibre_gl_example/assets/fill/cat_silhouette_pattern.png rename to maplibre_gl_example/assets/pattern/cat_silhouette_pattern.png diff --git a/maplibre_gl_example/assets/pattern/marker_pattern.png b/maplibre_gl_example/assets/pattern/marker_pattern.png new file mode 100644 index 0000000000000000000000000000000000000000..aa069df60afd586cf69fc5c30e1f3298b6ac6214 GIT binary patch literal 1172 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I0wfs{c7_5;g=CK)Uj~LMH3o);76yi2K%s^g z3=E|P3=FRl7#OT(FffQ0%-I!a1C(G&@^*J&_}|`tWQ-z_ICwl2r)@uj}`u9T_DWEuz9yE9F@z<0+rp8H=| zws~;=etX{Hk)Y_Ey-(>UpaK;+7sT+x+ZXy5Z?dI#x4`!Z~6Bh{N>-os8 zCi8XT`9|Ml>2}!v3hSkMf z&O^@kxQzy@oYqV+E}!5?DaH&@nb7hKT~WCQFYB57y*t~Nd6ysNoaLp>!2h$d@&C!g zL5~-YdK$9Fl--&9{K_;6{O=c1V`oatBZA5=GeFhNKAjP=Y% z=AoU(gVtU9dJvc`RZCnWN>UO_QmvAUQh^kMk%5t!u7RPhfmw)=krfb`SZW&>SQ!|Y zrMLY>(U6;;l9^VCTZ6lmog+|#2Hb{{%-q!ClEmBsOg*N?5KAh)I`{$gFnGH9xvX { if (_controller == null) return; try { - // Load cat silhouette pattern from assets + // Load marker pattern from assets await addImageFromAsset( _controller!, - 'pattern-cat', - 'assets/fill/cat_silhouette_pattern.png', + 'marker-pattern', + ExampleConstants.markerPatternPath, ); + dev.log('Pattern images loaded successfully', name: 'FillLayerExample'); } catch (e) { dev.log('Error loading pattern images: $e'); } @@ -302,7 +303,7 @@ class _FillLayerBodyState extends State<_FillLayerBody> { subtitle: Text(_fillPattern ?? 'None'), onChanged: (bool value) async { setState(() { - _fillPattern = value ? 'pattern-cat' : null; + _fillPattern = value ? 'marker-pattern' : null; }); 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 index 9adef8eb8..db5b144ff 100644 --- a/maplibre_gl_example/lib/examples/layers/line_layer_example.dart +++ b/maplibre_gl_example/lib/examples/layers/line_layer_example.dart @@ -76,11 +76,11 @@ class _LineLayerBodyState extends State<_LineLayerBody> { if (_controller == null) return; try { - // Load cat silhouette pattern from assets + // Load marker pattern from assets await addImageFromAsset( _controller!, - 'pattern-cat', - 'assets/fill/cat_silhouette_pattern.png', + 'marker-pattern', + ExampleConstants.markerPatternPath, ); } catch (e) { dev.log('Error loading pattern images: $e'); @@ -446,7 +446,7 @@ class _LineLayerBodyState extends State<_LineLayerBody> { subtitle: Text(_linePattern ?? 'None'), onChanged: (bool value) async { setState(() { - _linePattern = value ? 'pattern-cat' : null; + _linePattern = value ? 'marker-pattern' : null; }); await _updateLayer(); }, diff --git a/maplibre_gl_example/lib/shared/constants.dart b/maplibre_gl_example/lib/shared/constants.dart index ec7301066..c83d5499d 100644 --- a/maplibre_gl_example/lib/shared/constants.dart +++ b/maplibre_gl_example/lib/shared/constants.dart @@ -143,4 +143,15 @@ class ExampleConstants { /// Maximum zoom level static const double maxZoom = 22.0; + + // =========================================================================== + // Pattern images paths + // =========================================================================== + + /// Pattern image for fill layer example + static const String catPatternPath = + 'assets/pattern/cat_silhouette_pattern.png'; + + /// Pattern image for line layer example + static const String markerPatternPath = 'assets/pattern/marker_pattern.png'; } diff --git a/maplibre_gl_example/pubspec.yaml b/maplibre_gl_example/pubspec.yaml index 1787d2288..5a96022fd 100644 --- a/maplibre_gl_example/pubspec.yaml +++ b/maplibre_gl_example/pubspec.yaml @@ -30,7 +30,8 @@ dev_dependencies: flutter: uses-material-design: true assets: - - assets/fill/cat_silhouette_pattern.png + - assets/pattern/cat_silhouette_pattern.png + - assets/pattern/marker_pattern.png - assets/symbols/custom-icon.png - assets/symbols/2.0x/custom-icon.png - assets/symbols/3.0x/custom-icon.png From 7ee3a08f8952c6c5dd5cf265bb20b31590ac7222 Mon Sep 17 00:00:00 2001 From: Gabriel Palmisano Date: Wed, 7 Jan 2026 00:00:00 +0100 Subject: [PATCH 26/38] chore: update changelogs, release, contributing .md and minor fix for WASM compilation --- CHANGELOG.md | 51 ++++++++++ CONTRIBUTING.md | 7 +- RELEASE.md | 93 ++++++++++++------- maplibre_gl/CHANGELOG.md | 34 +++++++ maplibre_gl_platform_interface/CHANGELOG.md | 14 +++ maplibre_gl_web/CHANGELOG.md | 44 +++++++++ .../lib/src/interop/style/style_interop.dart | 3 + .../lib/src/maplibre_web_gl_platform.dart | 23 +++-- 8 files changed, 223 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e33c34f4..0a06ef7ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,57 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.0.0](https://github.com/maplibre/flutter-maplibre-gl/compare/v0.24.1...v1.0.0) - 2026-01-06 + +🎉 **First stable release!** This major version marks the maturity and stability of the Flutter MapLibre GL package. We now follow standard semantic versioning. + +### Added +* Logo customization options including visibility and position settings (#b4fb174). +* Explicit annotation manager initialization with clear error handling (#668). +* Web: Implemented `getStyle()` to return map style as JSON string. +* Web: Implemented `getSourceIds()` to return list of source IDs. +* Web: Improved `getLayers()` with safe null handling. + +### Changed +* **BREAKING**: Web implementation migrated from deprecated `dart:js_util` to modern `dart:js_interop` API (#687). + - **WASM compatible**: Now supports Flutter's upcoming WASM compilation target + - Required for Flutter 3.38.4+ compatibility + - All JS interop classes updated to `@staticInterop` pattern with extension methods + - Improved type safety for JS ↔ Dart conversions + - No public API changes for users +* MapLibre Android SDK upgraded from `11.13.5` to `12.3.0` (#690). + - Includes synchronous GeoJSON source updates + - Support for MLT-format vector tile sources + - Better frustum offset support + - See [MapLibre Native Android 12.3.0 release notes](https://github.com/maplibre/maplibre-native/releases/tag/android-v12.3.0) +* OkHttp updated from `4.12.0` to `5.3.2` for Node.js 24 compatibility (#676, #700). +* Kotlin updated to `2.3.0` (#697, #698). +* Android Gradle Plugin updated to `8.13.2` (#695, #674). +* Android Application Plugin updated to `8.13.2` (#696, #689). +* GitHub Actions: `actions/checkout` updated from v5 to v6 (#672, #693). +* GitHub Actions: `actions/upload-artifact` updated from v4 to v6 (#688, #694). + +### Fixed +* Min/max zoom preference on iOS (#5230fab). +* `queryRenderedFeatures` now returns all targets when supplying empty layers list on iOS, aligning behavior with Android (#680). +* Web: Fixed `setPaintProperty` and `setLayoutProperty` to handle nullable `JSAny` values correctly (#12dfad2). +* Web: Improved `jsify` function to create JS arrays correctly (#2b550ed). +* Fixed `lineDasharray` and patterns reset to null in layer properties (#2b550ed). +* Improved MapLibreMapController disposing to prevent memory leaks (#2b550ed). +* Fixed `setLayerProperties` and pattern images on web and Android (#9ce52a6). + - Pattern images now correctly converted to RGBA format on web + - Fixed mismatched image size error when loading pattern images + +### Refactor +* Complete refactor of example app with new UI and improved user experience (#ac877a4). + - Improved map sizing with responsive layouts (50-60% of screen height) + - Better button and control layouts across different screen sizes + - Enhanced visual design and usability +* Refactored `cameraTargetBounds` implementation on Android and iOS for consistent behavior (#8bcd74a). +* Refactored image upload on web - all images now converted to RGBA format for consistency (#9ce52a6). + +**Full Changelog**: [v0.24.1...v1.0.0](https://github.com/maplibre/flutter-maplibre-gl/compare/v0.24.1...v1.0.0) + ## [0.24.1](https://github.com/maplibre/flutter-maplibre-gl/compare/v0.24.0...v0.24.1) ### Fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a65300d8b..45d4509db 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -163,12 +163,15 @@ Found a vulnerability or sensitive exposure vector? ## Release process (Maintainers) A high-level outline (subject to change): 1. Ensure `main` (or release branch) is green (CI, tests, analyzer). -2. Update package versions & root/individual `CHANGELOG.md` sections. +2. Update package versions & root/individual `CHANGELOG.md` sections following [Semantic Versioning 2.0.0](https://semver.org/): + - **MAJOR** version for incompatible API changes + - **MINOR** version for new backward-compatible functionality + - **PATCH** version for backward-compatible bug fixes 3. Tag the release (`vX.Y.Z`) and publish packages to pub.dev in dependency order. 4. Merge back any release branch into `main`. 5. Announce in discussions (optional). -For more information, see [RELEASE](RELEASE.md) instructions. +For more information, see [RELEASE.md](RELEASE.md) instructions. ## Community epectations Be respectful and constructive. We follow the project's `CODE_OF_CONDUCT.md`. Harassment, discrimination, or unprofessional behavior is not tolerated. diff --git a/RELEASE.md b/RELEASE.md index dff452be3..0d0f91754 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -11,40 +11,42 @@ All packages share the same version number. A release = bump versions + update c ## Versioning Policy -Format: `MAJOR.MINOR.PATCH` (currently MAJOR fixed at 0 until 1.0.0). +Format: `MAJOR.MINOR.PATCH` following [Semantic Versioning 2.0.0](https://semver.org/). + +As of v1.0.0, this project follows **standard semantic versioning**: | Change Type | Bump | Effect | Example (old → new) | Notes | |-------------|------|--------|---------------------|-------| -| Breaking / significant feature | MINOR (2nd) & reset PATCH | Public API or behavior change (may be breaking pre‑1.0) | 0.23.4 → 0.24.0 | Mark **BREAKING** in changelog if present | -| Compatible feature / enhancement | MINOR (2nd) & reset PATCH | Adds capability, no removal | 0.24.1 → 0.25.0 | Combine small features where possible | -| Bug fix / doc / internal only | PATCH (3rd) | No API change | 0.24.0 → 0.24.1 | Batch multiple fixes if close in time | +| Breaking change | MAJOR (1st) | Incompatible API changes | 1.2.3 → 2.0.0 | Mark **BREAKING** in changelog with migration guide | +| New feature / enhancement | MINOR (2nd) & reset PATCH | Adds functionality in a backward-compatible manner | 1.2.3 → 1.3.0 | Combine related features where possible | +| Bug fix / doc / internal only | PATCH (3rd) | Backward-compatible bug fixes | 1.2.3 → 1.2.4 | Batch multiple fixes if close in time | Rules: -1. Pre‑1.0 any incompatible change still uses a MINOR bump (second segment). -2. Always reset PATCH to 0 when you bump MINOR. -3. Avoid publishing multiple PATCH releases within minutes—group fixes. -4. No build metadata (`+...`) for normal releases. +1. **MAJOR version** (X.y.z) increments for incompatible API changes. +2. **MINOR version** (x.Y.z) increments for new backward-compatible functionality (reset PATCH to 0). +3. **PATCH version** (x.y.Z) increments for backward-compatible bug fixes. +4. Avoid publishing multiple PATCH releases within minutes—group fixes. +5. No build metadata (`+...`) for normal releases. Changelog tags: -- Use headings: Added / Changed / Fixed / Removed / **BREAKING**. -- Provide a one‑line migration hint for each breaking change. - -Tag format: `v0.X.Y` (e.g. `v0.24.0`). +- Use headings: Added / Changed / Fixed / Removed / Deprecated / Security / **BREAKING**. +- Provide migration hints for each breaking change. -After 1.0.0 the table semantics align with standard SemVer (MINOR no longer contains breaking changes; those move to MAJOR). +Tag format: `vX.Y.Z` (e.g. `v1.0.0`, `v1.2.3`). -### Examples (Pre-1.0) +### Examples (Post-1.0) | Scenario | Old | New | Notes | |----------|-----|-----|-------| -| Add new public controller API | 0.23.1 | 0.24.0 | MINOR bump (document new API) | -| Fix crash in symbol update logic | 0.24.0 | 0.24.1 | PATCH fix | -| Introduce breaking rename of parameter | 0.24.1 | 0.25.0 | MINOR bump (flag **BREAKING**) | -| Documentation typo only (optional) | 0.25.0 | 0.25.1 | Only if you want it on pub.dev | +| Remove deprecated API or change signature | 1.2.3 | 2.0.0 | MAJOR bump (breaking change) | +| Add new public controller method | 1.2.3 | 1.3.0 | MINOR bump (backward-compatible feature) | +| Fix crash in symbol update logic | 1.2.3 | 1.2.4 | PATCH fix | +| Documentation typo only | 1.2.3 | 1.2.4 | PATCH (or skip if trivial) | ### Deciding the bump -1. Is the change user-visible (new feature or breaking)? -> bump MINOR, reset PATCH to 0. -2. Pure bug fix with no API surface change? -> bump PATCH. -3. Multiple fixes queued? Prefer batching into one PATCH rather than multiple rapid releases. +1. Does it break existing code or change behavior? -> bump MAJOR. +2. Does it add new functionality without breaking changes? -> bump MINOR, reset PATCH to 0. +3. Is it a pure bug fix with no API surface change? -> bump PATCH. +4. Multiple fixes queued? Prefer batching into one PATCH rather than multiple rapid releases. --- ## Pre‑Release Checklist @@ -74,19 +76,22 @@ Edit `pubspec.yaml` in each package: Ensure internal dependencies point to the new version (same number across all three). Example snippet: ```yaml dependencies: - maplibre_gl_platform_interface: ^0.23.0 + maplibre_gl_platform_interface: ^1.0.0 ``` ## Update the Changelog In root `CHANGELOG.md`: -1. Move items from `Unreleased` (if present) under a new heading: `## 0.23.0 - YYYY-MM-DD`. +1. Move items from `Unreleased` (if present) under a new heading: `## [X.Y.Z] - YYYY-MM-DD`. 2. Group by sections (Suggested): - Added - Changed - Fixed - Removed / Deprecated + - Security + - **BREAKING** (for major versions) - Internal (optional) -3. Ensure breaking changes are clearly marked with **BREAKING** and (if needed) short migration hints. +3. For **MAJOR** version bumps, ensure breaking changes are clearly marked with **BREAKING** and include migration guides. +4. Link to full changelog comparison: `[vX.Y.Z...vA.B.C]` Sub-package changelogs may link to root; keep duplication minimal. @@ -116,8 +121,8 @@ After the release PR has been merged, create & push the version tag (format `vX. ```bash git checkout main git pull origin main -git tag v0.23.0 -git push origin v0.23.0 +git tag v1.2.3 +git push origin v1.2.3 ``` No further manual publish commands are required unless the pipeline fails. In that case, resolve the issue and re-push (delete & recreate tag only if absolutely necessary and before broad adoption). @@ -140,15 +145,39 @@ If a critical issue is discovered shortly after release: - If native SDK version bumps are included, link upstream release notes in the PR description. - Run `flutter clean` in example only if weird build artifacts appear (avoid unnecessary noise in instructions). -## Example Flow (Patch Fix) -``` +## Example Flows + +### Patch Fix (Bug Fix) +```bash # After merging a simple fix -# decide version 0.23.1 -edit pubspecs -> 0.23.1 +# decide version 1.2.4 (was 1.2.3) +edit pubspecs -> 1.2.4 +update CHANGELOG +commit & PR -> chore: release 1.2.4 +merge +git tag v1.2.4 && git push origin v1.2.4 +``` + +### Minor Release (New Feature) +```bash +# After merging a new backward-compatible feature +# decide version 1.3.0 (was 1.2.3) +edit pubspecs -> 1.3.0 update CHANGELOG -commit & PR -> chore: release 0.23.1 +commit & PR -> chore: release 1.3.0 +merge +git tag v1.3.0 && git push origin v1.3.0 +``` + +### Major Release (Breaking Change) +```bash +# After merging breaking API changes +# decide version 2.0.0 (was 1.2.3) +edit pubspecs -> 2.0.0 +update CHANGELOG with BREAKING section and migration guide +commit & PR -> chore: release 2.0.0 merge -git tag v0.23.1 && push tag +git tag v2.0.0 && git push origin v2.0.0 ``` --- diff --git a/maplibre_gl/CHANGELOG.md b/maplibre_gl/CHANGELOG.md index 45582986c..c3876b595 100644 --- a/maplibre_gl/CHANGELOG.md +++ b/maplibre_gl/CHANGELOG.md @@ -1,3 +1,37 @@ +## [1.0.0](https://github.com/maplibre/flutter-maplibre-gl/compare/v0.24.1...v1.0.0) - 2026-01-06 + +🎉 **First stable release!** This major version marks the maturity and stability of the Flutter MapLibre GL package. We now follow standard semantic versioning. + +### Added +* Logo customization options including visibility and position settings (#b4fb174). +* Explicit annotation manager initialization with clear error handling (#668). + +### Changed +* MapLibre Android SDK upgraded from `11.13.5` to `12.3.0` (#690). + - Includes synchronous GeoJSON source updates + - Support for MLT-format vector tile sources + - Better frustum offset support + - See [MapLibre Native Android 12.3.0 release notes](https://github.com/maplibre/maplibre-native/releases/tag/android-v12.3.0) +* OkHttp updated from `4.12.0` to `5.3.2` for Node.js 24 compatibility (#676, #700). +* Kotlin updated to `2.3.0` (#697, #698). +* Android Gradle Plugin updated to `8.13.2` (#695, #674). +* Android Application Plugin updated to `8.13.2` (#696, #689). +* GitHub Actions: `actions/checkout` updated from v5 to v6 (#672, #693). +* GitHub Actions: `actions/upload-artifact` updated from v4 to v6 (#688, #694). + +### Fixed +* Min/max zoom preference on iOS (#5230fab). +* `queryRenderedFeatures` now returns all targets when supplying empty layers list on iOS, aligning behavior with Android (#680). +* Fixed `lineDasharray` and patterns reset to null in layer properties (#2b550ed). +* Improved MapLibreMapController disposing to prevent memory leaks.* Fixed `setLayerProperties` and pattern images on web and Android (#9ce52a6). + - Pattern images now correctly converted to RGBA format on web + - Fixed mismatched image size error when loading pattern images +### Refactor +* Complete refactor of example app with new UI and improved user experience (#ac877a4). +* Refactored `cameraTargetBounds` implementation on Android and iOS for consistent behavior (#8bcd74a). + +**Full Changelog**: [v0.24.1...v1.0.0](https://github.com/maplibre/flutter-maplibre-gl/compare/v0.24.1...v1.0.0) + ## [0.24.1](https://github.com/maplibre/flutter-maplibre-gl/compare/v0.24.0...v0.24.1) ### Fixed diff --git a/maplibre_gl_platform_interface/CHANGELOG.md b/maplibre_gl_platform_interface/CHANGELOG.md index f04ca6408..e2966f1e2 100644 --- a/maplibre_gl_platform_interface/CHANGELOG.md +++ b/maplibre_gl_platform_interface/CHANGELOG.md @@ -1,3 +1,17 @@ +## [1.0.0](https://github.com/maplibre/flutter-maplibre-gl/compare/v0.24.1...v1.0.0) - 2026-01-06 + +🎉 **First stable release!** This package now follows standard semantic versioning. + +See top-level [CHANGELOG.md](../CHANGELOG.md) for full details. + +### Changed +* Updated to align with main package v1.0.0. +* No breaking changes to the platform interface in this release. + +## [0.24.1](https://github.com/maplibre/flutter-maplibre-gl/compare/v0.24.0...v0.24.1) + +See top-level CHANGELOG.md + ## 0.23.0 > Note: This release has breaking changes. diff --git a/maplibre_gl_web/CHANGELOG.md b/maplibre_gl_web/CHANGELOG.md index eccac2bf1..29a2b6fa3 100644 --- a/maplibre_gl_web/CHANGELOG.md +++ b/maplibre_gl_web/CHANGELOG.md @@ -1,5 +1,49 @@ See top-level [CHANGELOG.md](../CHANGELOG.md) for full details. +## [1.0.0](https://github.com/maplibre/flutter-maplibre-gl/compare/v0.24.1...v1.0.0) - 2026-01-06 + +🎉 **First stable release!** This package now follows standard semantic versioning. + +### Major Changes + +#### **BREAKING**: Migration to Modern JS Interop (#687) +* **WASM Compatible**: Migrated from deprecated `dart:js_util` to modern `dart:js_interop` API +* Required for Flutter 3.38.4+ compatibility +* Now fully compatible with Flutter's WASM compilation target +* **No public API changes** - this is an internal implementation update + +#### Technical Details of JS Interop Migration: +* Replaced `dart:js_util` with `dart:js_interop` and `dart:js_interop_unsafe` +* Updated all JS interop classes to use `@staticInterop` + extension methods pattern +* Migrated from `@JS()` factory constructors to new interop model +* Converted `allowInterop()` callbacks to `.toJS` +* Updated property access from `getProperty()`/`setProperty()` to native JS property access +* Replaced `jsify()`/`dartify()` utilities to work with `JSAny`/`JSObject` types +* Fixed primitive type conversions: `JSString.toDart`, `JSNumber.toDartDouble`, `JSArray.toDart` +* Converted static methods to top-level functions (e.g., `LngLat.convert()` → `lngLatConvert()`) + +### Added +* Implemented `getStyle()` - returns map style as JSON string (previously threw `UnimplementedError`) +* Implemented `getSourceIds()` - returns list of source IDs from current style +* Improved `getLayers()` - safely handles null styles and returns empty list instead of crashing + +### Fixed +* Fixed `setPaintProperty` and `setLayoutProperty` to handle nullable `JSAny` values correctly (#12dfad2) +* Improved `jsify` function to create JS arrays correctly +* Enhanced error handling in `getLayer()`, `getFilter()`, and `isStyleLoaded()` with null-safety checks +* Fixed pattern images loading - all images now correctly converted to RGBA format (#9ce52a6) + - Resolves mismatched image size errors when loading pattern images + - Ensures consistent image format across all image uploads + +### Refactor +* Improved null safety across the web platform +* Enhanced type safety for JS ↔ Dart conversions +* More descriptive error messages in the web implementation +* Example app improvements: + - Maps now use responsive sizing (50-60% of screen height) + - Removed fixed width constraints for full-screen responsiveness + - Better button and control layouts + ## [0.24.1](https://github.com/maplibre/flutter-maplibre-gl/compare/v0.24.0...v0.24.1) * Rollback maplibre-gl to `4.7.1` version. (#660) diff --git a/maplibre_gl_web/lib/src/interop/style/style_interop.dart b/maplibre_gl_web/lib/src/interop/style/style_interop.dart index 15f7fe79d..52056da73 100644 --- a/maplibre_gl_web/lib/src/interop/style/style_interop.dart +++ b/maplibre_gl_web/lib/src/interop/style/style_interop.dart @@ -149,6 +149,9 @@ extension StyleJsImplExtension on StyleJsImpl { String mapId, RequestParametersJsImpl params, JSFunction callback); external JSArray layers; + + /// Map of source IDs to source objects + external JSAny? get sources; } @JS() diff --git a/maplibre_gl_web/lib/src/maplibre_web_gl_platform.dart b/maplibre_gl_web/lib/src/maplibre_web_gl_platform.dart index e52b8a20f..e8e1c5da2 100644 --- a/maplibre_gl_web/lib/src/maplibre_web_gl_platform.dart +++ b/maplibre_gl_web/lib/src/maplibre_web_gl_platform.dart @@ -193,19 +193,19 @@ class MapLibreMapController extends MapLibrePlatform @override Future animateCamera(CameraUpdate cameraUpdate, {Duration? duration}) async { - final cameraOptions = Convert.toCameraOptions(cameraUpdate, _map).jsObject; - final jsObj = cameraOptions as JSObject; + final cameraOptions = Convert.toCameraOptions(cameraUpdate, _map); - final around = jsObj.getProperty('around'.toJS); - final bearing = jsObj.getProperty('bearing'.toJS); - final center = jsObj.getProperty('center'.toJS); - final pitch = jsObj.getProperty('pitch'.toJS); - final zoom = jsObj.getProperty('zoom'.toJS); + // Use the existing CameraOptions wrapper which has proper WASM-compatible accessors + final around = cameraOptions.around; + final bearing = cameraOptions.bearing; + final center = cameraOptions.center; + final pitch = cameraOptions.pitch; + final zoom = cameraOptions.zoom; _map.flyTo({ - if (around != null) 'around': around, + if (around != null) 'around': around.jsObject, if (bearing != null) 'bearing': bearing, - if (center != null) 'center': center, + if (center != null) 'center': center.jsObject, if (pitch != null) 'pitch': pitch, if (zoom != null) 'zoom': zoom, if (duration != null) 'duration': duration.inMilliseconds, @@ -1377,9 +1377,8 @@ class MapLibreMapController extends MapLibrePlatform final style = _map.getStyle(); if (style == null) return []; - // The style is a JavaScript object with a 'sources' property - final jsStyle = style as JSObject; - final jsSources = jsStyle.getProperty('sources'.toJS); + // Get the sources from the style object + final jsSources = style.sources; if (jsSources == null) return []; From 2b3b21ae2cd9401e2642f26bcc41645c8cee081a Mon Sep 17 00:00:00 2001 From: Gabriel Palmisano Date: Wed, 7 Jan 2026 00:06:55 +0100 Subject: [PATCH 27/38] chore: updated pubspec versions to `1.0.0` --- maplibre_gl/ios/maplibre_gl.podspec | 2 +- maplibre_gl/pubspec.yaml | 6 +++--- maplibre_gl_example/pubspec.yaml | 2 +- maplibre_gl_platform_interface/pubspec.yaml | 2 +- maplibre_gl_web/lib/maplibre_gl_web.dart | 1 - maplibre_gl_web/pubspec.yaml | 4 ++-- scripts/pubspec.yaml | 2 +- 7 files changed, 9 insertions(+), 10 deletions(-) diff --git a/maplibre_gl/ios/maplibre_gl.podspec b/maplibre_gl/ios/maplibre_gl.podspec index 00a7aba7e..cd06f580c 100644 --- a/maplibre_gl/ios/maplibre_gl.podspec +++ b/maplibre_gl/ios/maplibre_gl.podspec @@ -3,7 +3,7 @@ # Pod::Spec.new do |s| s.name = 'maplibre_gl' - s.version = '0.24.1' + s.version = '1.0.0' s.summary = 'MapLibre GL Flutter plugin' s.description = <<-DESC MapLibre GL Flutter plugin. diff --git a/maplibre_gl/pubspec.yaml b/maplibre_gl/pubspec.yaml index 4a0b77950..fed1e2f15 100644 --- a/maplibre_gl/pubspec.yaml +++ b/maplibre_gl/pubspec.yaml @@ -1,6 +1,6 @@ name: maplibre_gl description: A Flutter plugin for integrating MapLibre Maps inside a Flutter application on Android, iOS and web platforms. -version: 0.24.1 +version: 1.0.0 repository: https://github.com/maplibre/flutter-maplibre-gl issue_tracker: https://github.com/maplibre/flutter-maplibre-gl/issues resolution: workspace @@ -12,8 +12,8 @@ environment: dependencies: flutter: sdk: flutter - maplibre_gl_platform_interface: ^0.24.1 - maplibre_gl_web: ^0.24.1 + maplibre_gl_platform_interface: ^1.0.0 + maplibre_gl_web: ^1.0.0 dev_dependencies: very_good_analysis: ^10.0.0 diff --git a/maplibre_gl_example/pubspec.yaml b/maplibre_gl_example/pubspec.yaml index 5a96022fd..cd73417af 100644 --- a/maplibre_gl_example/pubspec.yaml +++ b/maplibre_gl_example/pubspec.yaml @@ -1,7 +1,7 @@ name: maplibre_gl_example description: Demonstrates how to use the maplibre_gl plugin. publish_to: 'none' -version: 0.24.1 +version: 1.0.0 repository: https://github.com/maplibre/flutter-maplibre-gl issue_tracker: https://github.com/maplibre/flutter-maplibre-gl/issues resolution: workspace diff --git a/maplibre_gl_platform_interface/pubspec.yaml b/maplibre_gl_platform_interface/pubspec.yaml index cdf710952..3fb1d61b5 100644 --- a/maplibre_gl_platform_interface/pubspec.yaml +++ b/maplibre_gl_platform_interface/pubspec.yaml @@ -1,6 +1,6 @@ name: maplibre_gl_platform_interface description: A common platform interface for the maplibre_gl plugin. This package is only intended to be used by the maplibre_gl package. -version: 0.24.1 +version: 1.0.0 repository: https://github.com/maplibre/flutter-maplibre-gl issue_tracker: https://github.com/maplibre/flutter-maplibre-gl/issues resolution: workspace diff --git a/maplibre_gl_web/lib/maplibre_gl_web.dart b/maplibre_gl_web/lib/maplibre_gl_web.dart index dff296381..3766c9a7d 100644 --- a/maplibre_gl_web/lib/maplibre_gl_web.dart +++ b/maplibre_gl_web/lib/maplibre_gl_web.dart @@ -5,7 +5,6 @@ import 'dart:convert'; import 'dart:developer' as dev; import 'dart:js_interop'; -import 'dart:js_interop_unsafe'; import 'dart:math'; import 'dart:ui' as ui; diff --git a/maplibre_gl_web/pubspec.yaml b/maplibre_gl_web/pubspec.yaml index 04bc1b675..3de12bc02 100644 --- a/maplibre_gl_web/pubspec.yaml +++ b/maplibre_gl_web/pubspec.yaml @@ -1,6 +1,6 @@ name: maplibre_gl_web description: Web platform implementation of maplibre_gl. This package is only intended to be used by the maplibre_gl package. -version: 0.24.1 +version: 1.0.0 repository: https://github.com/maplibre/flutter-maplibre-gl issue_tracker: https://github.com/maplibre/flutter-maplibre-gl/issues resolution: workspace @@ -22,7 +22,7 @@ dependencies: flutter_web_plugins: sdk: flutter image: ^4.0.17 - maplibre_gl_platform_interface: ^0.24.1 + maplibre_gl_platform_interface: ^1.0.0 meta: ^1.3.0 web: ^1.1.1 diff --git a/scripts/pubspec.yaml b/scripts/pubspec.yaml index 55d66fa00..88a76fc32 100644 --- a/scripts/pubspec.yaml +++ b/scripts/pubspec.yaml @@ -3,7 +3,7 @@ description: code generation for flutter-maplibre-gl publish_to: 'none' resolution: workspace -version: 0.24.1 +version: 1.0.0 environment: sdk: ">=3.5.0 <4.0.0" From 07ecf40f885f7665e007a5cf4ff3cd555cd14bbc Mon Sep 17 00:00:00 2001 From: Gabriel Palmisano Date: Wed, 7 Jan 2026 12:27:53 +0100 Subject: [PATCH 28/38] chore: rollback to 0.25.0 release version --- CHANGELOG.md | 6 +- CONTRIBUTING.md | 7 +- RELEASE.md | 86 +++++++++------------ maplibre_gl/CHANGELOG.md | 6 +- maplibre_gl/ios/maplibre_gl.podspec | 2 +- maplibre_gl/pubspec.yaml | 4 +- maplibre_gl_platform_interface/CHANGELOG.md | 6 +- maplibre_gl_platform_interface/pubspec.yaml | 2 +- maplibre_gl_web/CHANGELOG.md | 4 +- maplibre_gl_web/pubspec.yaml | 4 +- scripts/pubspec.yaml | 2 +- 11 files changed, 52 insertions(+), 77 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a06ef7ba..07f4f09a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,7 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. -## [1.0.0](https://github.com/maplibre/flutter-maplibre-gl/compare/v0.24.1...v1.0.0) - 2026-01-06 - -🎉 **First stable release!** This major version marks the maturity and stability of the Flutter MapLibre GL package. We now follow standard semantic versioning. +## [0.25.0](https://github.com/maplibre/flutter-maplibre-gl/compare/v0.24.1...v0.25.0) - 2026-01-07 ### Added * Logo customization options including visibility and position settings (#b4fb174). @@ -52,7 +50,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline * Refactored `cameraTargetBounds` implementation on Android and iOS for consistent behavior (#8bcd74a). * Refactored image upload on web - all images now converted to RGBA format for consistency (#9ce52a6). -**Full Changelog**: [v0.24.1...v1.0.0](https://github.com/maplibre/flutter-maplibre-gl/compare/v0.24.1...v1.0.0) +**Full Changelog**: [v0.24.1...v0.25.0](https://github.com/maplibre/flutter-maplibre-gl/compare/v0.24.1...v0.25.0) ## [0.24.1](https://github.com/maplibre/flutter-maplibre-gl/compare/v0.24.0...v0.24.1) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 45d4509db..66667b698 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -163,11 +163,10 @@ Found a vulnerability or sensitive exposure vector? ## Release process (Maintainers) A high-level outline (subject to change): 1. Ensure `main` (or release branch) is green (CI, tests, analyzer). -2. Update package versions & root/individual `CHANGELOG.md` sections following [Semantic Versioning 2.0.0](https://semver.org/): - - **MAJOR** version for incompatible API changes - - **MINOR** version for new backward-compatible functionality +2. Update package versions & root/individual `CHANGELOG.md` sections following the pre-1.0 versioning policy in [RELEASE.md](RELEASE.md): + - **MINOR** version for breaking changes, significant features, or new functionality - **PATCH** version for backward-compatible bug fixes -3. Tag the release (`vX.Y.Z`) and publish packages to pub.dev in dependency order. +3. Tag the release (`v0.X.Y`) and publish packages to pub.dev in dependency order. 4. Merge back any release branch into `main`. 5. Announce in discussions (optional). diff --git a/RELEASE.md b/RELEASE.md index 0d0f91754..39cb91fd6 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -11,42 +11,40 @@ All packages share the same version number. A release = bump versions + update c ## Versioning Policy -Format: `MAJOR.MINOR.PATCH` following [Semantic Versioning 2.0.0](https://semver.org/). - -As of v1.0.0, this project follows **standard semantic versioning**: +Format: `MAJOR.MINOR.PATCH` (currently MAJOR fixed at 0 until 1.0.0). | Change Type | Bump | Effect | Example (old → new) | Notes | |-------------|------|--------|---------------------|-------| -| Breaking change | MAJOR (1st) | Incompatible API changes | 1.2.3 → 2.0.0 | Mark **BREAKING** in changelog with migration guide | -| New feature / enhancement | MINOR (2nd) & reset PATCH | Adds functionality in a backward-compatible manner | 1.2.3 → 1.3.0 | Combine related features where possible | -| Bug fix / doc / internal only | PATCH (3rd) | Backward-compatible bug fixes | 1.2.3 → 1.2.4 | Batch multiple fixes if close in time | +| Breaking / significant feature | MINOR (2nd) & reset PATCH | Public API or behavior change (may be breaking pre‑1.0) | 0.23.4 → 0.24.0 | Mark **BREAKING** in changelog if present | +| Compatible feature / enhancement | MINOR (2nd) & reset PATCH | Adds capability, no removal | 0.24.1 → 0.25.0 | Combine small features where possible | +| Bug fix / doc / internal only | PATCH (3rd) | No API change | 0.24.0 → 0.24.1 | Batch multiple fixes if close in time | Rules: -1. **MAJOR version** (X.y.z) increments for incompatible API changes. -2. **MINOR version** (x.Y.z) increments for new backward-compatible functionality (reset PATCH to 0). -3. **PATCH version** (x.y.Z) increments for backward-compatible bug fixes. -4. Avoid publishing multiple PATCH releases within minutes—group fixes. -5. No build metadata (`+...`) for normal releases. +1. Pre‑1.0 any incompatible change still uses a MINOR bump (second segment). +2. Always reset PATCH to 0 when you bump MINOR. +3. Avoid publishing multiple PATCH releases within minutes—group fixes. +4. No build metadata (`+...`) for normal releases. Changelog tags: -- Use headings: Added / Changed / Fixed / Removed / Deprecated / Security / **BREAKING**. -- Provide migration hints for each breaking change. +- Use headings: Added / Changed / Fixed / Removed / **BREAKING**. +- Provide a one‑line migration hint for each breaking change. + +Tag format: `v0.X.Y` (e.g. `v0.24.0`). -Tag format: `vX.Y.Z` (e.g. `v1.0.0`, `v1.2.3`). +After 1.0.0 the table semantics align with standard SemVer (MINOR no longer contains breaking changes; those move to MAJOR). -### Examples (Post-1.0) +### Examples (Pre-1.0) | Scenario | Old | New | Notes | |----------|-----|-----|-------| -| Remove deprecated API or change signature | 1.2.3 | 2.0.0 | MAJOR bump (breaking change) | -| Add new public controller method | 1.2.3 | 1.3.0 | MINOR bump (backward-compatible feature) | -| Fix crash in symbol update logic | 1.2.3 | 1.2.4 | PATCH fix | -| Documentation typo only | 1.2.3 | 1.2.4 | PATCH (or skip if trivial) | +| Add new public controller API | 0.23.1 | 0.24.0 | MINOR bump (document new API) | +| Fix crash in symbol update logic | 0.24.0 | 0.24.1 | PATCH fix | +| Introduce breaking rename of parameter | 0.24.1 | 0.25.0 | MINOR bump (flag **BREAKING**) | +| Documentation typo only (optional) | 0.25.0 | 0.25.1 | Only if you want it on pub.dev | ### Deciding the bump -1. Does it break existing code or change behavior? -> bump MAJOR. -2. Does it add new functionality without breaking changes? -> bump MINOR, reset PATCH to 0. -3. Is it a pure bug fix with no API surface change? -> bump PATCH. -4. Multiple fixes queued? Prefer batching into one PATCH rather than multiple rapid releases. +1. Is the change user-visible (new feature or breaking)? -> bump MINOR, reset PATCH to 0. +2. Pure bug fix with no API surface change? -> bump PATCH. +3. Multiple fixes queued? Prefer batching into one PATCH rather than multiple rapid releases. --- ## Pre‑Release Checklist @@ -76,22 +74,19 @@ Edit `pubspec.yaml` in each package: Ensure internal dependencies point to the new version (same number across all three). Example snippet: ```yaml dependencies: - maplibre_gl_platform_interface: ^1.0.0 + maplibre_gl_platform_interface: ^0.25.0 ``` ## Update the Changelog In root `CHANGELOG.md`: -1. Move items from `Unreleased` (if present) under a new heading: `## [X.Y.Z] - YYYY-MM-DD`. +1. Move items from `Unreleased` (if present) under a new heading: `## 0.25.0 - YYYY-MM-DD`. 2. Group by sections (Suggested): - Added - Changed - Fixed - Removed / Deprecated - - Security - - **BREAKING** (for major versions) - Internal (optional) -3. For **MAJOR** version bumps, ensure breaking changes are clearly marked with **BREAKING** and include migration guides. -4. Link to full changelog comparison: `[vX.Y.Z...vA.B.C]` +3. Ensure breaking changes are clearly marked with **BREAKING** and (if needed) short migration hints. Sub-package changelogs may link to root; keep duplication minimal. @@ -121,8 +116,8 @@ After the release PR has been merged, create & push the version tag (format `vX. ```bash git checkout main git pull origin main -git tag v1.2.3 -git push origin v1.2.3 +git tag v0.24.0 +git push origin v0.24.0 ``` No further manual publish commands are required unless the pipeline fails. In that case, resolve the issue and re-push (delete & recreate tag only if absolutely necessary and before broad adoption). @@ -150,34 +145,23 @@ If a critical issue is discovered shortly after release: ### Patch Fix (Bug Fix) ```bash # After merging a simple fix -# decide version 1.2.4 (was 1.2.3) -edit pubspecs -> 1.2.4 +# decide version 0.25.1 (was 0.25.0) +edit pubspecs -> 0.25.1 update CHANGELOG -commit & PR -> chore: release 1.2.4 +commit & PR -> chore: release 0.25.1 merge -git tag v1.2.4 && git push origin v1.2.4 +git tag v0.25.1 && git push origin v0.25.1 ``` ### Minor Release (New Feature) ```bash -# After merging a new backward-compatible feature -# decide version 1.3.0 (was 1.2.3) -edit pubspecs -> 1.3.0 +# After merging a new feature +# decide version 0.25.0 (was 0.24.1) +edit pubspecs -> 0.25.0 update CHANGELOG -commit & PR -> chore: release 1.3.0 -merge -git tag v1.3.0 && git push origin v1.3.0 -``` - -### Major Release (Breaking Change) -```bash -# After merging breaking API changes -# decide version 2.0.0 (was 1.2.3) -edit pubspecs -> 2.0.0 -update CHANGELOG with BREAKING section and migration guide -commit & PR -> chore: release 2.0.0 +commit & PR -> chore: release 0.25.0 merge -git tag v2.0.0 && git push origin v2.0.0 +git tag v0.25.0 && git push origin v0.25.0 ``` --- diff --git a/maplibre_gl/CHANGELOG.md b/maplibre_gl/CHANGELOG.md index c3876b595..dd98316f9 100644 --- a/maplibre_gl/CHANGELOG.md +++ b/maplibre_gl/CHANGELOG.md @@ -1,6 +1,4 @@ -## [1.0.0](https://github.com/maplibre/flutter-maplibre-gl/compare/v0.24.1...v1.0.0) - 2026-01-06 - -🎉 **First stable release!** This major version marks the maturity and stability of the Flutter MapLibre GL package. We now follow standard semantic versioning. +## [0.25.0](https://github.com/maplibre/flutter-maplibre-gl/compare/v0.24.1...v0.25.0) - 2026-01-07 ### Added * Logo customization options including visibility and position settings (#b4fb174). @@ -30,7 +28,7 @@ * Complete refactor of example app with new UI and improved user experience (#ac877a4). * Refactored `cameraTargetBounds` implementation on Android and iOS for consistent behavior (#8bcd74a). -**Full Changelog**: [v0.24.1...v1.0.0](https://github.com/maplibre/flutter-maplibre-gl/compare/v0.24.1...v1.0.0) +**Full Changelog**: [v0.24.1...v0.25.0](https://github.com/maplibre/flutter-maplibre-gl/compare/v0.24.1...v0.25.0) ## [0.24.1](https://github.com/maplibre/flutter-maplibre-gl/compare/v0.24.0...v0.24.1) diff --git a/maplibre_gl/ios/maplibre_gl.podspec b/maplibre_gl/ios/maplibre_gl.podspec index cd06f580c..a322e7900 100644 --- a/maplibre_gl/ios/maplibre_gl.podspec +++ b/maplibre_gl/ios/maplibre_gl.podspec @@ -3,7 +3,7 @@ # Pod::Spec.new do |s| s.name = 'maplibre_gl' - s.version = '1.0.0' + s.version = '0.25.0' s.summary = 'MapLibre GL Flutter plugin' s.description = <<-DESC MapLibre GL Flutter plugin. diff --git a/maplibre_gl/pubspec.yaml b/maplibre_gl/pubspec.yaml index fed1e2f15..35d49020f 100644 --- a/maplibre_gl/pubspec.yaml +++ b/maplibre_gl/pubspec.yaml @@ -1,6 +1,6 @@ name: maplibre_gl description: A Flutter plugin for integrating MapLibre Maps inside a Flutter application on Android, iOS and web platforms. -version: 1.0.0 +version: 0.25.0 repository: https://github.com/maplibre/flutter-maplibre-gl issue_tracker: https://github.com/maplibre/flutter-maplibre-gl/issues resolution: workspace @@ -12,7 +12,7 @@ environment: dependencies: flutter: sdk: flutter - maplibre_gl_platform_interface: ^1.0.0 + maplibre_gl_platform_interface: ^0.25.0 maplibre_gl_web: ^1.0.0 dev_dependencies: diff --git a/maplibre_gl_platform_interface/CHANGELOG.md b/maplibre_gl_platform_interface/CHANGELOG.md index e2966f1e2..bebedb1a1 100644 --- a/maplibre_gl_platform_interface/CHANGELOG.md +++ b/maplibre_gl_platform_interface/CHANGELOG.md @@ -1,11 +1,9 @@ -## [1.0.0](https://github.com/maplibre/flutter-maplibre-gl/compare/v0.24.1...v1.0.0) - 2026-01-06 - -🎉 **First stable release!** This package now follows standard semantic versioning. +## [0.25.0](https://github.com/maplibre/flutter-maplibre-gl/compare/v0.24.1...v0.25.0) - 2026-01-07 See top-level [CHANGELOG.md](../CHANGELOG.md) for full details. ### Changed -* Updated to align with main package v1.0.0. +* Updated to align with main package v0.25.0. * No breaking changes to the platform interface in this release. ## [0.24.1](https://github.com/maplibre/flutter-maplibre-gl/compare/v0.24.0...v0.24.1) diff --git a/maplibre_gl_platform_interface/pubspec.yaml b/maplibre_gl_platform_interface/pubspec.yaml index 3fb1d61b5..b1cbe2850 100644 --- a/maplibre_gl_platform_interface/pubspec.yaml +++ b/maplibre_gl_platform_interface/pubspec.yaml @@ -1,6 +1,6 @@ name: maplibre_gl_platform_interface description: A common platform interface for the maplibre_gl plugin. This package is only intended to be used by the maplibre_gl package. -version: 1.0.0 +version: 0.25.0 repository: https://github.com/maplibre/flutter-maplibre-gl issue_tracker: https://github.com/maplibre/flutter-maplibre-gl/issues resolution: workspace diff --git a/maplibre_gl_web/CHANGELOG.md b/maplibre_gl_web/CHANGELOG.md index 29a2b6fa3..aa36bf3cb 100644 --- a/maplibre_gl_web/CHANGELOG.md +++ b/maplibre_gl_web/CHANGELOG.md @@ -1,8 +1,6 @@ See top-level [CHANGELOG.md](../CHANGELOG.md) for full details. -## [1.0.0](https://github.com/maplibre/flutter-maplibre-gl/compare/v0.24.1...v1.0.0) - 2026-01-06 - -🎉 **First stable release!** This package now follows standard semantic versioning. +## [0.25.0](https://github.com/maplibre/flutter-maplibre-gl/compare/v0.24.1...v0.25.0) - 2026-01-07 ### Major Changes diff --git a/maplibre_gl_web/pubspec.yaml b/maplibre_gl_web/pubspec.yaml index 3de12bc02..1c12f91b6 100644 --- a/maplibre_gl_web/pubspec.yaml +++ b/maplibre_gl_web/pubspec.yaml @@ -1,6 +1,6 @@ name: maplibre_gl_web description: Web platform implementation of maplibre_gl. This package is only intended to be used by the maplibre_gl package. -version: 1.0.0 +version: 0.25.0 repository: https://github.com/maplibre/flutter-maplibre-gl issue_tracker: https://github.com/maplibre/flutter-maplibre-gl/issues resolution: workspace @@ -22,7 +22,7 @@ dependencies: flutter_web_plugins: sdk: flutter image: ^4.0.17 - maplibre_gl_platform_interface: ^1.0.0 + maplibre_gl_platform_interface: ^0.25.0 meta: ^1.3.0 web: ^1.1.1 diff --git a/scripts/pubspec.yaml b/scripts/pubspec.yaml index 88a76fc32..afdf778a7 100644 --- a/scripts/pubspec.yaml +++ b/scripts/pubspec.yaml @@ -3,7 +3,7 @@ description: code generation for flutter-maplibre-gl publish_to: 'none' resolution: workspace -version: 1.0.0 +version: 0.25.0 environment: sdk: ">=3.5.0 <4.0.0" From 97fabb17a26aaee999d9f54ec0c32229a2b827cb Mon Sep 17 00:00:00 2001 From: Gabriel Palmisano Date: Wed, 7 Jan 2026 12:47:59 +0100 Subject: [PATCH 29/38] chore: rollback to 0.25.0 --- maplibre_gl/pubspec.yaml | 2 +- maplibre_gl_example/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/maplibre_gl/pubspec.yaml b/maplibre_gl/pubspec.yaml index 35d49020f..f5d99da71 100644 --- a/maplibre_gl/pubspec.yaml +++ b/maplibre_gl/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: flutter: sdk: flutter maplibre_gl_platform_interface: ^0.25.0 - maplibre_gl_web: ^1.0.0 + maplibre_gl_web: ^0.25.0 dev_dependencies: very_good_analysis: ^10.0.0 diff --git a/maplibre_gl_example/pubspec.yaml b/maplibre_gl_example/pubspec.yaml index cd73417af..5cdb6a3de 100644 --- a/maplibre_gl_example/pubspec.yaml +++ b/maplibre_gl_example/pubspec.yaml @@ -1,7 +1,7 @@ name: maplibre_gl_example description: Demonstrates how to use the maplibre_gl plugin. publish_to: 'none' -version: 1.0.0 +version: 0.25.0 repository: https://github.com/maplibre/flutter-maplibre-gl issue_tracker: https://github.com/maplibre/flutter-maplibre-gl/issues resolution: workspace From f989797db9c50f8be8912bcffb20afe185b3c7bd Mon Sep 17 00:00:00 2001 From: Gabriel Palmisano Date: Wed, 7 Jan 2026 17:31:23 +0100 Subject: [PATCH 30/38] chore: remove disposing of mapController in examples. Minor fixes. --- maplibre_gl/lib/src/controller.dart | 6 +- .../lib/examples/advanced/pmtiles.dart | 6 -- .../advanced/translucent_full_map.dart | 6 -- .../annotation_properties_example.dart | 6 -- .../examples/annotations/custom_marker.dart | 68 ++----------------- .../lib/examples/basics/full_map_example.dart | 6 -- .../examples/basics/gps_location_page.dart | 6 -- .../interaction/map_controls_example.dart | 1 - .../interaction/map_gestures_example.dart | 1 - .../lib/translucent_full_map.dart | 6 -- .../lib/src/method_channel_maplibre_gl.dart | 2 +- .../lib/src/maplibre_web_gl_platform.dart | 2 +- 12 files changed, 11 insertions(+), 105 deletions(-) diff --git a/maplibre_gl/lib/src/controller.dart b/maplibre_gl/lib/src/controller.dart index 158dfd599..f511b3cd0 100644 --- a/maplibre_gl/lib/src/controller.dart +++ b/maplibre_gl/lib/src/controller.dart @@ -176,13 +176,13 @@ class MapLibreMapController extends ChangeNotifier { _maplibrePlatform.onCameraMoveStartedPlatform.add((_) { _isCameraMoving = true; - notifyListeners(); + if (!isDisposed) notifyListeners(); }); _maplibrePlatform.onCameraMovePlatform.add((cameraPosition) { _cameraPosition = cameraPosition; onCameraMove?.call(cameraPosition); - notifyListeners(); + if (!isDisposed) notifyListeners(); }); _maplibrePlatform.onCameraIdlePlatform.add((cameraPosition) { @@ -191,7 +191,7 @@ class MapLibreMapController extends ChangeNotifier { _cameraPosition = cameraPosition; } onCameraIdle?.call(); - notifyListeners(); + if (!isDisposed) notifyListeners(); }); _maplibrePlatform.onMapStyleLoadedPlatform.add((_) async { diff --git a/maplibre_gl_example/lib/examples/advanced/pmtiles.dart b/maplibre_gl_example/lib/examples/advanced/pmtiles.dart index 68a3bb2c2..adb337153 100644 --- a/maplibre_gl_example/lib/examples/advanced/pmtiles.dart +++ b/maplibre_gl_example/lib/examples/advanced/pmtiles.dart @@ -78,10 +78,4 @@ class _PMTilesBodyState extends State<_PMTilesBody> { 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 index cd4d2fb68..fe6f6d18f 100644 --- a/maplibre_gl_example/lib/examples/advanced/translucent_full_map.dart +++ b/maplibre_gl_example/lib/examples/advanced/translucent_full_map.dart @@ -97,10 +97,4 @@ class _TranslucentMapBodyState extends State<_TranslucentMapBody> { floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, ); } - - @override - void dispose() { - _mapController?.dispose(); - super.dispose(); - } } diff --git a/maplibre_gl_example/lib/examples/annotations/annotation_properties_example.dart b/maplibre_gl_example/lib/examples/annotations/annotation_properties_example.dart index bf30eb4b7..c3b81469d 100644 --- a/maplibre_gl_example/lib/examples/annotations/annotation_properties_example.dart +++ b/maplibre_gl_example/lib/examples/annotations/annotation_properties_example.dart @@ -681,10 +681,4 @@ class _AnnotationPropertiesBodyState extends State<_AnnotationPropertiesBody> { } } } - - @override - void dispose() { - _controller?.dispose(); - super.dispose(); - } } diff --git a/maplibre_gl_example/lib/examples/annotations/custom_marker.dart b/maplibre_gl_example/lib/examples/annotations/custom_marker.dart index 2160bd010..cd870e14b 100644 --- a/maplibre_gl_example/lib/examples/annotations/custom_marker.dart +++ b/maplibre_gl_example/lib/examples/annotations/custom_marker.dart @@ -29,7 +29,7 @@ class CustomMarker extends StatefulWidget { class CustomMarkerState extends State { final _rnd = Random(); - late MapLibreMapController _mapController; + MapLibreMapController? _mapController; final _markers = []; final _markerStates = []; @@ -38,7 +38,8 @@ class CustomMarkerState extends State { } void _onMapCreated(MapLibreMapController controller) { - _mapController = controller; + setState(() => _mapController = controller); + controller.addListener(() async { if (controller.isCameraMoving) { await _updateMarkerPosition(); @@ -65,7 +66,7 @@ class CustomMarkerState extends State { coordinates.add(markerState.getCoordinate()); } - await _mapController.toScreenLocationBatch(coordinates).then((points) { + await _mapController?.toScreenLocationBatch(coordinates).then((points) { _markerStates.asMap().forEach((i, value) { _markerStates[i].updatePosition(points[i]); }); @@ -117,7 +118,7 @@ class CustomMarkerState extends State { param.add(LatLng(lat, lng)); } - await _mapController.toScreenLocationBatch(param).then((value) { + await _mapController?.toScreenLocationBatch(param).then((value) { for (var i = 0; i < randomMarkerNum; i++) { final point = Point(value[i].x as double, value[i].y as double); @@ -129,57 +130,6 @@ class CustomMarkerState extends State { ), ); } - - // ignore: unused_element --- IGNORE --- - Future _measurePerformance() async { - const trial = 10; - final batches = [500, 1000, 1500, 2000, 2500, 3000]; - final results = >{}; - for (final batch in batches) { - results[batch] = [0.0, 0.0]; - } - - await _mapController.toScreenLocation(const LatLng(0, 0)); - final sw = Stopwatch(); - - for (final batch in batches) { - // - // primitive - // - for (var i = 0; i < trial; i++) { - sw.start(); - final list = >>[]; - for (var j = 0; j < batch; j++) { - final p = _mapController - .toScreenLocation(LatLng(j.toDouble() % 80, j.toDouble() % 300)); - list.add(p); - } - Future.wait(list); - sw.stop(); - results[batch]![0] += sw.elapsedMilliseconds; - sw.reset(); - } - - // - // batch - // - for (var i = 0; i < trial; i++) { - sw.start(); - final param = []; - for (var j = 0; j < batch; j++) { - param.add(LatLng(j.toDouble() % 80, j.toDouble() % 300)); - } - Future.wait([_mapController.toScreenLocationBatch(param)]); - sw.stop(); - results[batch]![1] += sw.elapsedMilliseconds; - sw.reset(); - } - - debugPrint( - 'batch=$batch,primitive=${results[batch]![0] / trial}ms, batch=${results[batch]![1] / trial}ms', - ); - } - } } class Marker extends StatefulWidget { @@ -226,13 +176,7 @@ class MarkerState extends State with TickerProviderStateMixin { curve: Curves.elasticOut, ); } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - + @override Widget build(BuildContext context) { var ratio = 1.0; diff --git a/maplibre_gl_example/lib/examples/basics/full_map_example.dart b/maplibre_gl_example/lib/examples/basics/full_map_example.dart index e9a9dc09e..e12c669fd 100644 --- a/maplibre_gl_example/lib/examples/basics/full_map_example.dart +++ b/maplibre_gl_example/lib/examples/basics/full_map_example.dart @@ -77,10 +77,4 @@ class _FullMapBodyState extends State<_FullMapBody> { floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, ); } - - @override - void dispose() { - _mapController?.dispose(); - super.dispose(); - } } diff --git a/maplibre_gl_example/lib/examples/basics/gps_location_page.dart b/maplibre_gl_example/lib/examples/basics/gps_location_page.dart index 611440055..acf9eac5c 100644 --- a/maplibre_gl_example/lib/examples/basics/gps_location_page.dart +++ b/maplibre_gl_example/lib/examples/basics/gps_location_page.dart @@ -245,10 +245,4 @@ class _GpsLocationBodyState extends State<_GpsLocationBody> { contentPadding: EdgeInsets.zero, ); } - - @override - void dispose() { - _controller?.dispose(); - super.dispose(); - } } diff --git a/maplibre_gl_example/lib/examples/interaction/map_controls_example.dart b/maplibre_gl_example/lib/examples/interaction/map_controls_example.dart index d92e4f04e..f9af8a58d 100644 --- a/maplibre_gl_example/lib/examples/interaction/map_controls_example.dart +++ b/maplibre_gl_example/lib/examples/interaction/map_controls_example.dart @@ -223,7 +223,6 @@ class _MapControlsBodyState extends State<_MapControlsBody> { @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 index 0fea6997b..7327dd6b2 100644 --- a/maplibre_gl_example/lib/examples/interaction/map_gestures_example.dart +++ b/maplibre_gl_example/lib/examples/interaction/map_gestures_example.dart @@ -199,7 +199,6 @@ class _MapGesturesBodyState extends State<_MapGesturesBody> { @override void dispose() { _controller?.removeListener(_onMapChanged); - _controller?.dispose(); super.dispose(); } } diff --git a/maplibre_gl_example/lib/translucent_full_map.dart b/maplibre_gl_example/lib/translucent_full_map.dart index d43a2b93d..b6f33d6d5 100644 --- a/maplibre_gl_example/lib/translucent_full_map.dart +++ b/maplibre_gl_example/lib/translucent_full_map.dart @@ -99,10 +99,4 @@ class _TranslucentMapBodyState extends State<_TranslucentMapBody> { floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, ); } - - @override - void dispose() { - _mapController?.dispose(); - super.dispose(); - } } diff --git a/maplibre_gl_platform_interface/lib/src/method_channel_maplibre_gl.dart b/maplibre_gl_platform_interface/lib/src/method_channel_maplibre_gl.dart index 2c124038b..78ca3d6ed 100644 --- a/maplibre_gl_platform_interface/lib/src/method_channel_maplibre_gl.dart +++ b/maplibre_gl_platform_interface/lib/src/method_channel_maplibre_gl.dart @@ -819,8 +819,8 @@ class MapLibreMethodChannel extends MapLibrePlatform { @override void dispose() { - super.dispose(); _channel.setMethodCallHandler(null); + super.dispose(); } @override diff --git a/maplibre_gl_web/lib/src/maplibre_web_gl_platform.dart b/maplibre_gl_web/lib/src/maplibre_web_gl_platform.dart index e8e1c5da2..c48dea840 100644 --- a/maplibre_gl_web/lib/src/maplibre_web_gl_platform.dart +++ b/maplibre_gl_web/lib/src/maplibre_web_gl_platform.dart @@ -37,8 +37,8 @@ class MapLibreMapController extends MapLibrePlatform @override void dispose() { - super.dispose(); _map.remove(); + super.dispose(); } void _registerViewFactory(Function(int) callback, int identifier) { From 98660dca370e4b72d911d63f8480e6527651433d Mon Sep 17 00:00:00 2001 From: Gabriel Palmisano Date: Wed, 7 Jan 2026 17:33:23 +0100 Subject: [PATCH 31/38] feat: enhance iOS LayerPropertyConverter to handle null values and improve expression parsing --- .../maplibre_gl/LayerPropertyConverter.swift | 181 +++++++++++++++--- .../LayerPropertyConverter.swift.template | 34 +++- 2 files changed, 188 insertions(+), 27 deletions(-) diff --git a/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/LayerPropertyConverter.swift b/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/LayerPropertyConverter.swift index 992ba230d..e80a7272d 100644 --- a/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/LayerPropertyConverter.swift +++ b/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/LayerPropertyConverter.swift @@ -6,7 +6,20 @@ import MapLibre class LayerPropertyConverter { class func addSymbolProperties(symbolLayer: MLNSymbolStyleLayer, properties: [String: String]) { for (propertyName, propertyValue) in properties { - let expression = interpretExpression(propertyName: propertyName, expression: propertyValue) + // Check if the value is explicitly null to clear the property + let trimmedValue = propertyValue.trimmingCharacters(in: .whitespaces) + + // Prepare expression: nil for "null", parsed for valid values, skip for invalid + var expression: NSExpression? + if trimmedValue == "null" { + expression = nil + } else { + guard let expr = interpretExpression(propertyName: propertyName, expression: propertyValue) else { + continue + } + expression = expr + } + switch propertyName { case "icon-opacity": symbolLayer.iconOpacity = expression @@ -119,8 +132,10 @@ class LayerPropertyConverter { case "text-optional": symbolLayer.textOptional = expression case "visibility": - let trimmedPropertyValue = propertyValue.trimmingCharacters(in: .init(charactersIn: "\"")) - symbolLayer.isVisible = trimmedPropertyValue == "visible" + if trimmedValue != "null" { + let trimmedPropertyValue = propertyValue.trimmingCharacters(in: .init(charactersIn: "\"")) + symbolLayer.isVisible = trimmedPropertyValue == "visible" + } default: break @@ -130,7 +145,20 @@ class LayerPropertyConverter { class func addCircleProperties(circleLayer: MLNCircleStyleLayer, properties: [String: String]) { for (propertyName, propertyValue) in properties { - let expression = interpretExpression(propertyName: propertyName, expression: propertyValue) + // Check if the value is explicitly null to clear the property + let trimmedValue = propertyValue.trimmingCharacters(in: .whitespaces) + + // Prepare expression: nil for "null", parsed for valid values, skip for invalid + var expression: NSExpression? + if trimmedValue == "null" { + expression = nil + } else { + guard let expr = interpretExpression(propertyName: propertyName, expression: propertyValue) else { + continue + } + expression = expr + } + switch propertyName { case "circle-radius": circleLayer.circleRadius = expression @@ -157,8 +185,10 @@ class LayerPropertyConverter { case "circle-sort-key": circleLayer.circleSortKey = expression case "visibility": - let trimmedPropertyValue = propertyValue.trimmingCharacters(in: .init(charactersIn: "\"")) - circleLayer.isVisible = trimmedPropertyValue == "visible" + if trimmedValue != "null" { + let trimmedPropertyValue = propertyValue.trimmingCharacters(in: .init(charactersIn: "\"")) + circleLayer.isVisible = trimmedPropertyValue == "visible" + } default: break @@ -168,7 +198,20 @@ class LayerPropertyConverter { class func addLineProperties(lineLayer: MLNLineStyleLayer, properties: [String: String]) { for (propertyName, propertyValue) in properties { - let expression = interpretExpression(propertyName: propertyName, expression: propertyValue) + // Check if the value is explicitly null to clear the property + let trimmedValue = propertyValue.trimmingCharacters(in: .whitespaces) + + // Prepare expression: nil for "null", parsed for valid values, skip for invalid + var expression: NSExpression? + if trimmedValue == "null" { + expression = nil + } else { + guard let expr = interpretExpression(propertyName: propertyName, expression: propertyValue) else { + continue + } + expression = expr + } + switch propertyName { case "line-opacity": lineLayer.lineOpacity = expression @@ -203,8 +246,10 @@ class LayerPropertyConverter { case "line-sort-key": lineLayer.lineSortKey = expression case "visibility": - let trimmedPropertyValue = propertyValue.trimmingCharacters(in: .init(charactersIn: "\"")) - lineLayer.isVisible = trimmedPropertyValue == "visible" + if trimmedValue != "null" { + let trimmedPropertyValue = propertyValue.trimmingCharacters(in: .init(charactersIn: "\"")) + lineLayer.isVisible = trimmedPropertyValue == "visible" + } default: break @@ -214,7 +259,20 @@ class LayerPropertyConverter { class func addFillProperties(fillLayer: MLNFillStyleLayer, properties: [String: String]) { for (propertyName, propertyValue) in properties { - let expression = interpretExpression(propertyName: propertyName, expression: propertyValue) + // Check if the value is explicitly null to clear the property + let trimmedValue = propertyValue.trimmingCharacters(in: .whitespaces) + + // Prepare expression: nil for "null", parsed for valid values, skip for invalid + var expression: NSExpression? + if trimmedValue == "null" { + expression = nil + } else { + guard let expr = interpretExpression(propertyName: propertyName, expression: propertyValue) else { + continue + } + expression = expr + } + switch propertyName { case "fill-antialias": fillLayer.fillAntialiased = expression @@ -233,8 +291,10 @@ class LayerPropertyConverter { case "fill-sort-key": fillLayer.fillSortKey = expression case "visibility": - let trimmedPropertyValue = propertyValue.trimmingCharacters(in: .init(charactersIn: "\"")) - fillLayer.isVisible = trimmedPropertyValue == "visible" + if trimmedValue != "null" { + let trimmedPropertyValue = propertyValue.trimmingCharacters(in: .init(charactersIn: "\"")) + fillLayer.isVisible = trimmedPropertyValue == "visible" + } default: break @@ -244,7 +304,20 @@ class LayerPropertyConverter { class func addFillExtrusionProperties(fillExtrusionLayer: MLNFillExtrusionStyleLayer, properties: [String: String]) { for (propertyName, propertyValue) in properties { - let expression = interpretExpression(propertyName: propertyName, expression: propertyValue) + // Check if the value is explicitly null to clear the property + let trimmedValue = propertyValue.trimmingCharacters(in: .whitespaces) + + // Prepare expression: nil for "null", parsed for valid values, skip for invalid + var expression: NSExpression? + if trimmedValue == "null" { + expression = nil + } else { + guard let expr = interpretExpression(propertyName: propertyName, expression: propertyValue) else { + continue + } + expression = expr + } + switch propertyName { case "fill-extrusion-opacity": fillExtrusionLayer.fillExtrusionOpacity = expression @@ -263,8 +336,10 @@ class LayerPropertyConverter { case "fill-extrusion-vertical-gradient": fillExtrusionLayer.fillExtrusionHasVerticalGradient = expression case "visibility": - let trimmedPropertyValue = propertyValue.trimmingCharacters(in: .init(charactersIn: "\"")) - fillExtrusionLayer.isVisible = trimmedPropertyValue == "visible" + if trimmedValue != "null" { + let trimmedPropertyValue = propertyValue.trimmingCharacters(in: .init(charactersIn: "\"")) + fillExtrusionLayer.isVisible = trimmedPropertyValue == "visible" + } default: break @@ -274,7 +349,20 @@ class LayerPropertyConverter { class func addRasterProperties(rasterLayer: MLNRasterStyleLayer, properties: [String: String]) { for (propertyName, propertyValue) in properties { - let expression = interpretExpression(propertyName: propertyName, expression: propertyValue) + // Check if the value is explicitly null to clear the property + let trimmedValue = propertyValue.trimmingCharacters(in: .whitespaces) + + // Prepare expression: nil for "null", parsed for valid values, skip for invalid + var expression: NSExpression? + if trimmedValue == "null" { + expression = nil + } else { + guard let expr = interpretExpression(propertyName: propertyName, expression: propertyValue) else { + continue + } + expression = expr + } + switch propertyName { case "raster-opacity": rasterLayer.rasterOpacity = expression @@ -293,8 +381,10 @@ class LayerPropertyConverter { case "raster-fade-duration": rasterLayer.rasterFadeDuration = expression case "visibility": - let trimmedPropertyValue = propertyValue.trimmingCharacters(in: .init(charactersIn: "\"")) - rasterLayer.isVisible = trimmedPropertyValue == "visible" + if trimmedValue != "null" { + let trimmedPropertyValue = propertyValue.trimmingCharacters(in: .init(charactersIn: "\"")) + rasterLayer.isVisible = trimmedPropertyValue == "visible" + } default: break @@ -304,7 +394,20 @@ class LayerPropertyConverter { class func addHillshadeProperties(hillshadeLayer: MLNHillshadeStyleLayer, properties: [String: String]) { for (propertyName, propertyValue) in properties { - let expression = interpretExpression(propertyName: propertyName, expression: propertyValue) + // Check if the value is explicitly null to clear the property + let trimmedValue = propertyValue.trimmingCharacters(in: .whitespaces) + + // Prepare expression: nil for "null", parsed for valid values, skip for invalid + var expression: NSExpression? + if trimmedValue == "null" { + expression = nil + } else { + guard let expr = interpretExpression(propertyName: propertyName, expression: propertyValue) else { + continue + } + expression = expr + } + switch propertyName { case "hillshade-illumination-direction": hillshadeLayer.hillshadeIlluminationDirection = expression @@ -319,8 +422,10 @@ class LayerPropertyConverter { case "hillshade-accent-color": hillshadeLayer.hillshadeAccentColor = expression case "visibility": - let trimmedPropertyValue = propertyValue.trimmingCharacters(in: .init(charactersIn: "\"")) - hillshadeLayer.isVisible = trimmedPropertyValue == "visible" + if trimmedValue != "null" { + let trimmedPropertyValue = propertyValue.trimmingCharacters(in: .init(charactersIn: "\"")) + hillshadeLayer.isVisible = trimmedPropertyValue == "visible" + } default: break @@ -330,7 +435,20 @@ class LayerPropertyConverter { class func addHeatmapProperties(heatmapLayer: MLNHeatmapStyleLayer, properties: [String: String]) { for (propertyName, propertyValue) in properties { - let expression = interpretExpression(propertyName: propertyName, expression: propertyValue) + // Check if the value is explicitly null to clear the property + let trimmedValue = propertyValue.trimmingCharacters(in: .whitespaces) + + // Prepare expression: nil for "null", parsed for valid values, skip for invalid + var expression: NSExpression? + if trimmedValue == "null" { + expression = nil + } else { + guard let expr = interpretExpression(propertyName: propertyName, expression: propertyValue) else { + continue + } + expression = expr + } + switch propertyName { case "heatmap-radius": heatmapLayer.heatmapRadius = expression @@ -343,8 +461,10 @@ class LayerPropertyConverter { case "heatmap-opacity": heatmapLayer.heatmapOpacity = expression case "visibility": - let trimmedPropertyValue = propertyValue.trimmingCharacters(in: .init(charactersIn: "\"")) - heatmapLayer.isVisible = trimmedPropertyValue == "visible" + if trimmedValue != "null" { + let trimmedPropertyValue = propertyValue.trimmingCharacters(in: .init(charactersIn: "\"")) + heatmapLayer.isVisible = trimmedPropertyValue == "visible" + } default: break @@ -359,6 +479,12 @@ class LayerPropertyConverter { do { let json = try JSONSerialization.jsonObject(with: expression.data(using: .utf8)!, options: .fragmentsAllowed) + + // Check if JSON contains NSNull - this would create an invalid NSExpression + if json is NSNull { + return nil + } + // this is required because NSExpression.init(mglJSONObject: json) fails to create // a proper Expression if the data of is a hexString if isColor { @@ -392,6 +518,13 @@ class LayerPropertyConverter { // this is required because NSExpression.init(mglJSONObject: json) fails to create // a proper Expression if the data is an array of double return NSExpression(forConstantValue: [NSNumber(value: x), NSNumber(value: y)]) + } else { + // Handle arrays with any number of elements (e.g., dash arrays with 3+ elements) + // Convert to array of NSNumbers for proper expression creation + let numbers = offset.compactMap { $0 as? Double }.map { NSNumber(value: $0) } + if numbers.count == offset.count { + return NSExpression(forConstantValue: numbers) + } } } diff --git a/scripts/templates/LayerPropertyConverter.swift.template b/scripts/templates/LayerPropertyConverter.swift.template index b452b0e47..65d9e886d 100644 --- a/scripts/templates/LayerPropertyConverter.swift.template +++ b/scripts/templates/LayerPropertyConverter.swift.template @@ -7,7 +7,20 @@ class LayerPropertyConverter { {{#layerTypes}} class func add{{typePascal}}Properties({{typeCamel}}Layer: MLN{{typePascal}}StyleLayer, properties: [String: String]) { for (propertyName, propertyValue) in properties { - let expression = interpretExpression(propertyName: propertyName, expression: propertyValue) + // Check if the value is explicitly null to clear the property + let trimmedValue = propertyValue.trimmingCharacters(in: .whitespaces) + + // Prepare expression: nil for "null", parsed for valid values, skip for invalid + var expression: NSExpression? + if trimmedValue == "null" { + expression = nil + } else { + guard let expr = interpretExpression(propertyName: propertyName, expression: propertyValue) else { + continue + } + expression = expr + } + switch propertyName { {{#paint_properties}} case "{{{value}}}": @@ -29,8 +42,10 @@ class LayerPropertyConverter { {{/isIosAsCamelCase}} {{/isVisibilityProperty}} {{#isVisibilityProperty}} - let trimmedPropertyValue = propertyValue.trimmingCharacters(in: .init(charactersIn: "\"")) - {{typeCamel}}Layer.{{iosAsCamelCase}} = trimmedPropertyValue == "visible" + if trimmedValue != "null" { + let trimmedPropertyValue = propertyValue.trimmingCharacters(in: .init(charactersIn: "\"")) + {{typeCamel}}Layer.{{iosAsCamelCase}} = trimmedPropertyValue == "visible" + } {{/isVisibilityProperty}} {{/layout_properties}} @@ -48,6 +63,12 @@ class LayerPropertyConverter { do { let json = try JSONSerialization.jsonObject(with: expression.data(using: .utf8)!, options: .fragmentsAllowed) + + // Check if JSON contains NSNull - this would create an invalid NSExpression + if json is NSNull { + return nil + } + // this is required because NSExpression.init(mglJSONObject: json) fails to create // a proper Expression if the data of is a hexString if isColor { @@ -81,6 +102,13 @@ class LayerPropertyConverter { // this is required because NSExpression.init(mglJSONObject: json) fails to create // a proper Expression if the data is an array of double return NSExpression(forConstantValue: [NSNumber(value: x), NSNumber(value: y)]) + } else { + // Handle arrays with any number of elements (e.g., dash arrays with 3+ elements) + // Convert to array of NSNumbers for proper expression creation + let numbers = offset.compactMap { $0 as? Double }.map { NSNumber(value: $0) } + if numbers.count == offset.count { + return NSExpression(forConstantValue: numbers) + } } } From 910a10abed29937d1f9eb4a90b2150127f9abc0b Mon Sep 17 00:00:00 2001 From: Gabriel Palmisano Date: Wed, 7 Jan 2026 17:37:37 +0100 Subject: [PATCH 32/38] chore: updated changelogs and fixed pipelines failing --- CHANGELOG.md | 6 ++++++ maplibre_gl/CHANGELOG.md | 5 ++++- .../lib/examples/annotations/custom_marker.dart | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07f4f09a5..e1e0e920f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,10 +34,16 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Fixed * Min/max zoom preference on iOS (#5230fab). * `queryRenderedFeatures` now returns all targets when supplying empty layers list on iOS, aligning behavior with Android (#680). +* iOS: Enhanced LayerPropertyConverter to handle null values and improve expression parsing (#98660dc). + - Better handling of null values in layer properties + - Improved expression parsing for complex layer configurations * Web: Fixed `setPaintProperty` and `setLayoutProperty` to handle nullable `JSAny` values correctly (#12dfad2). * Web: Improved `jsify` function to create JS arrays correctly (#2b550ed). * Fixed `lineDasharray` and patterns reset to null in layer properties (#2b550ed). * Improved MapLibreMapController disposing to prevent memory leaks (#2b550ed). +* Removed unnecessary disposing of mapController in example app (#f989797). + - Cleaned up example code with minor fixes + - Improved example app stability * Fixed `setLayerProperties` and pattern images on web and Android (#9ce52a6). - Pattern images now correctly converted to RGBA format on web - Fixed mismatched image size error when loading pattern images diff --git a/maplibre_gl/CHANGELOG.md b/maplibre_gl/CHANGELOG.md index dd98316f9..aa8f1cf79 100644 --- a/maplibre_gl/CHANGELOG.md +++ b/maplibre_gl/CHANGELOG.md @@ -20,8 +20,11 @@ ### Fixed * Min/max zoom preference on iOS (#5230fab). * `queryRenderedFeatures` now returns all targets when supplying empty layers list on iOS, aligning behavior with Android (#680). +* iOS: Enhanced LayerPropertyConverter to handle null values and improve expression parsing (#98660dc). * Fixed `lineDasharray` and patterns reset to null in layer properties (#2b550ed). -* Improved MapLibreMapController disposing to prevent memory leaks.* Fixed `setLayerProperties` and pattern images on web and Android (#9ce52a6). +* Improved MapLibreMapController disposing to prevent memory leaks. +* Removed unnecessary disposing of mapController in example app (#f989797). +* Fixed `setLayerProperties` and pattern images on web and Android (#9ce52a6). - Pattern images now correctly converted to RGBA format on web - Fixed mismatched image size error when loading pattern images ### Refactor diff --git a/maplibre_gl_example/lib/examples/annotations/custom_marker.dart b/maplibre_gl_example/lib/examples/annotations/custom_marker.dart index cd870e14b..aa2384a9e 100644 --- a/maplibre_gl_example/lib/examples/annotations/custom_marker.dart +++ b/maplibre_gl_example/lib/examples/annotations/custom_marker.dart @@ -176,7 +176,7 @@ class MarkerState extends State with TickerProviderStateMixin { curve: Curves.elasticOut, ); } - + @override Widget build(BuildContext context) { var ratio = 1.0; From c001d13db9716df28385219bbd1d938aed24fe30 Mon Sep 17 00:00:00 2001 From: Gabriel Palmisano Date: Wed, 7 Jan 2026 18:22:56 +0100 Subject: [PATCH 33/38] feat: implement camera bounds constraints in example and enhanced camera bounds implementation on iOS --- .../maplibre_gl/MapLibreMapController.swift | 52 ++---------- .../camera/camera_bounds_example.dart | 82 +++++++++++++++---- 2 files changed, 72 insertions(+), 62 deletions(-) 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 148546086..e2db567b6 100644 --- a/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/MapLibreMapController.swift +++ b/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/MapLibreMapController.swift @@ -18,7 +18,6 @@ class MapLibreMapController: NSObject, FlutterPlatformView, MLNMapViewDelegate, private var dragFeature: MLNFeature? private var initialTilt: CGFloat? - private var cameraTargetBounds: MLNCoordinateBounds? private var trackCameraPosition = false private var myLocationEnabled = false private var scrollingEnabled = true @@ -1854,46 +1853,6 @@ class MapLibreMapController: NSObject, FlutterPlatformView, MLNMapViewDelegate, } 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) ] : [:] @@ -1994,12 +1953,11 @@ 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) + let bounds = bounds ?? MLNCoordinateBounds( + sw: CLLocationCoordinate2D(latitude: -90, longitude: -180), + ne: CLLocationCoordinate2D(latitude: 90, longitude: 180) + ) + mapView.maximumScreenBounds = bounds; } func setCompassEnabled(compassEnabled: Bool) { diff --git a/maplibre_gl_example/lib/examples/camera/camera_bounds_example.dart b/maplibre_gl_example/lib/examples/camera/camera_bounds_example.dart index 8df1b9944..83a8b1cd5 100644 --- a/maplibre_gl_example/lib/examples/camera/camera_bounds_example.dart +++ b/maplibre_gl_example/lib/examples/camera/camera_bounds_example.dart @@ -26,6 +26,7 @@ class _CameraBoundsBody extends StatefulWidget { class _CameraBoundsBodyState extends State<_CameraBoundsBody> { MapLibreMapController? _controller; LatLngBounds? _currentBounds; + LatLngBounds? _constrainedBounds; double? _minZoom; double? _maxZoom; @@ -60,26 +61,19 @@ class _CameraBoundsBodyState extends State<_CameraBoundsBody> { Future _setBounds(LatLngBounds bounds, String name) async { if (_controller == null) return; + var padding = 150.0; + if (bounds == _europeBounds) padding = 0.0; await _controller!.animateCamera( CameraUpdate.newLatLngBounds( bounds, - left: 50, - top: 50, - right: 50, - bottom: 50, + left: padding, + top: padding, + right: padding, + bottom: padding, ), ); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Camera moved to $name bounds'), - duration: const Duration(seconds: 1), - ), - ); - } - _updateInfo(); } @@ -102,6 +96,17 @@ class _CameraBoundsBodyState extends State<_CameraBoundsBody> { }); } + Future _setCameraBounds(LatLngBounds bounds, String name) async { + if (_controller == null) return; + + setState(() => _constrainedBounds = bounds); + } + + Future _clearCameraBounds() async { + if (_controller == null) return; + setState(() => _constrainedBounds = null); + } + @override Widget build(BuildContext context) { final hasController = _controller != null; @@ -118,6 +123,9 @@ class _CameraBoundsBodyState extends State<_CameraBoundsBody> { trackCameraPosition: true, initialCameraPosition: ExampleConstants.defaultCameraPosition, minMaxZoomPreference: MinMaxZoomPreference(_minZoom, _maxZoom), + cameraTargetBounds: _constrainedBounds != null + ? CameraTargetBounds(_constrainedBounds) + : CameraTargetBounds.unbounded, ), controls: [ InfoCard( @@ -133,6 +141,13 @@ class _CameraBoundsBodyState extends State<_CameraBoundsBody> { icon: Icons.lock, color: Colors.orange.shade100, ), + if (_constrainedBounds != null) + InfoCard( + title: 'Camera Target Bounds', + subtitle: 'Panning restricted to defined region', + icon: Icons.lock_outline, + color: Colors.red.shade100, + ), const SizedBox(height: 8), ControlGroup( title: 'Move to Bounds', @@ -177,8 +192,8 @@ class _CameraBoundsBodyState extends State<_CameraBoundsBody> { style: ExampleButtonStyle.tonal, ), ExampleButton( - label: 'Clear Limits', - icon: Icons.lock_open, + label: 'Clear', + icon: Icons.clear, onPressed: hasController && (_minZoom != null || _maxZoom != null) ? _clearZoomConstraints : null, @@ -186,6 +201,43 @@ class _CameraBoundsBodyState extends State<_CameraBoundsBody> { ), ], ), + ControlGroup( + title: 'Camera Bounds Constraint', + children: [ + ExampleButton( + label: 'Lock to Sydney', + icon: Icons.lock, + onPressed: hasController + ? () => _setCameraBounds(_sydneyBounds, 'Sydney') + : null, + style: ExampleButtonStyle.tonal, + ), + ExampleButton( + label: 'Lock to SF', + icon: Icons.lock, + onPressed: hasController + ? () => _setCameraBounds(_sanFranciscoBounds, 'San Francisco') + : null, + style: ExampleButtonStyle.tonal, + ), + ExampleButton( + label: 'Lock to Europe', + icon: Icons.lock, + onPressed: hasController + ? () => _setCameraBounds(_europeBounds, 'Europe') + : null, + style: ExampleButtonStyle.tonal, + ), + ExampleButton( + label: 'Clear', + icon: Icons.clear, + onPressed: hasController && _constrainedBounds != null + ? _clearCameraBounds + : null, + style: ExampleButtonStyle.outlined, + ), + ], + ), ], ); } From 2a4433e1af07047a7ad783d4b8019aecbfc747d1 Mon Sep 17 00:00:00 2001 From: Gabriel Palmisano Date: Wed, 7 Jan 2026 18:26:23 +0100 Subject: [PATCH 34/38] chore: camera bounds example --- .../examples/camera/camera_bounds_example.dart | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/maplibre_gl_example/lib/examples/camera/camera_bounds_example.dart b/maplibre_gl_example/lib/examples/camera/camera_bounds_example.dart index 83a8b1cd5..c30ec1508 100644 --- a/maplibre_gl_example/lib/examples/camera/camera_bounds_example.dart +++ b/maplibre_gl_example/lib/examples/camera/camera_bounds_example.dart @@ -62,7 +62,7 @@ class _CameraBoundsBodyState extends State<_CameraBoundsBody> { Future _setBounds(LatLngBounds bounds, String name) async { if (_controller == null) return; var padding = 150.0; - if (bounds == _europeBounds) padding = 0.0; + if (bounds == _europeBounds) padding = 50.0; await _controller!.animateCamera( CameraUpdate.newLatLngBounds( @@ -99,7 +99,18 @@ class _CameraBoundsBodyState extends State<_CameraBoundsBody> { Future _setCameraBounds(LatLngBounds bounds, String name) async { if (_controller == null) return; - setState(() => _constrainedBounds = bounds); + final adjustedBounds = LatLngBounds( + southwest: LatLng( + bounds.southwest.latitude - 0.2, + bounds.southwest.longitude - 0.2, + ), + northeast: LatLng( + bounds.northeast.latitude + 0.2, + bounds.northeast.longitude + 0.2, + ), + ); + + setState(() => _constrainedBounds = adjustedBounds); } Future _clearCameraBounds() async { @@ -202,7 +213,7 @@ class _CameraBoundsBodyState extends State<_CameraBoundsBody> { ], ), ControlGroup( - title: 'Camera Bounds Constraint', + title: 'Camera Target Bounds', children: [ ExampleButton( label: 'Lock to Sydney', From d2ed46c60649e348e8e0c29656b888364893d052 Mon Sep 17 00:00:00 2001 From: Gabriel Palmisano Date: Wed, 7 Jan 2026 20:12:08 +0100 Subject: [PATCH 35/38] feat: add attribution parsing for iOS. Enhanced various sources example. --- .../maplibre_gl/SourcePropertyConverter.swift | 66 ++++- .../lib/examples/layers/various_sources.dart | 255 +++++++++++++----- 2 files changed, 251 insertions(+), 70 deletions(-) diff --git a/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/SourcePropertyConverter.swift b/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/SourcePropertyConverter.swift index 01b8f1473..a4c5ec9fc 100644 --- a/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/SourcePropertyConverter.swift +++ b/maplibre_gl/ios/maplibre_gl/Sources/maplibre_gl/SourcePropertyConverter.swift @@ -22,8 +22,72 @@ class SourcePropertyConverter { let system: MLNTileCoordinateSystem = (scheme == "tms" ? .TMS : .XYZ) options[.tileCoordinateSystem] = system.rawValue } + if let attribution = properties["attribution"] as? String { + options[.attributionInfos] = parseAttributionHTML(attribution) + } return options - // TODO: attribution not implemneted for IOS + } + + class func parseAttributionHTML(_ html: String) -> [MLNAttributionInfo] { + // Parse HTML attribution string and create a single attributed string with clickable links + let pattern = #"]*>([^<]+)"# + + guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else { + // Regex failed, use plain text + let title = NSAttributedString(string: html) + return [MLNAttributionInfo(title: title, url: nil)] + } + + let nsString = html as NSString + let matches = regex.matches(in: html, options: [], range: NSRange(location: 0, length: nsString.length)) + + if matches.isEmpty { + // No links found, create simple attribution + let title = NSAttributedString(string: html) + return [MLNAttributionInfo(title: title, url: nil)] + } + + // Build a single attributed string with all text and apply link attributes + let attributedString = NSMutableAttributedString() + var lastLocation = 0 + var primaryURL: URL? + + for match in matches { + // Add text before the link + if match.range.location > lastLocation { + let plainRange = NSRange(location: lastLocation, length: match.range.location - lastLocation) + let plainText = nsString.substring(with: plainRange) + attributedString.append(NSAttributedString(string: plainText)) + } + + // Add the link text with link attribute + if match.numberOfRanges >= 3 { + let urlRange = match.range(at: 1) + let textRange = match.range(at: 2) + + let urlString = nsString.substring(with: urlRange) + let linkText = nsString.substring(with: textRange) + + if let url = URL(string: urlString) { + if primaryURL == nil { + primaryURL = url + } + let linkAttributedString = NSMutableAttributedString(string: linkText) + linkAttributedString.addAttribute(.link, value: url, range: NSRange(location: 0, length: linkText.count)) + attributedString.append(linkAttributedString) + } + } + + lastLocation = match.range.location + match.range.length + } + + // Add any remaining text after the last link + if lastLocation < nsString.length { + let remainingText = nsString.substring(from: lastLocation) + attributedString.append(NSAttributedString(string: remainingText)) + } + + return [MLNAttributionInfo(title: attributedString, url: primaryURL)] } class func buildRasterTileSource(identifier: String, diff --git a/maplibre_gl_example/lib/examples/layers/various_sources.dart b/maplibre_gl_example/lib/examples/layers/various_sources.dart index 6ca3a6dda..ee60e0bab 100644 --- a/maplibre_gl_example/lib/examples/layers/various_sources.dart +++ b/maplibre_gl_example/lib/examples/layers/various_sources.dart @@ -41,36 +41,40 @@ class FullMapState extends State { int selectedStyleId = 0; void _onMapCreated(MapLibreMapController controller) { - this.controller = controller; + setState(() => this.controller = controller); } static Future addRaster(MapLibreMapController controller) async { await controller.addSource( - "watercolor", + "osm-raster", const RasterSourceProperties( tiles: [ - 'https://stamen-tiles.a.ssl.fastly.net/watercolor/{z}/{x}/{y}.jpg' + '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: - 'Map tiles by Stamen Design, under CC BY 3.0. Data by OpenStreetMap, under CC BY SA'), + '© OpenStreetMap contributors'), ); await controller.addLayer( - "watercolor", "watercolor", const RasterLayerProperties()); + "osm-raster", "osm-raster", const RasterLayerProperties()); } static Future addGeojsonCluster( MapLibreMapController controller) async { await controller.addSource( - "earthquakes", - const GeojsonSourceProperties( - data: - 'https://docs.mapbox.com/mapbox-gl-js/assets/earthquakes.geojson', - cluster: true, - clusterMaxZoom: 14, // Max zoom to cluster points on - clusterRadius: - 50 // Radius of each cluster when clustering points (defaults to 50) - )); + "earthquakes", + const GeojsonSourceProperties( + data: + 'https://maplibre.org/maplibre-gl-js/docs/assets/earthquakes.geojson', + cluster: true, + clusterMaxZoom: 14, // Max zoom to cluster points on + clusterRadius: + 50, // Radius of each cluster when clustering points (defaults to 50) + attribution: + 'Earthquake data © MapLibre'), + ); await controller.addLayer( "earthquakes", "earthquakes-circles", @@ -103,34 +107,50 @@ class FullMapState extends State { static Future addVector(MapLibreMapController controller) async { await controller.addSource( - "terrain", + "openmaptiles", const VectorSourceProperties( - url: MapLibreStyles.demo, + tiles: [ + 'https://demotiles.maplibre.org/tiles/{z}/{x}/{y}.pbf', + ], + maxzoom: 14, + attribution: + '© MapLibre contributors', )); await controller.addLayer( - "terrain", - "contour", + "openmaptiles", + "water-fill", + const FillLayerProperties( + fillColor: "#0080ff", + fillOpacity: 0.5, + ), + sourceLayer: "water"); + + await controller.addLayer( + "openmaptiles", + "roads", const LineLayerProperties( lineColor: "#ff69b4", - lineWidth: 1, + lineWidth: 2, lineCap: "round", lineJoin: "round", ), - sourceLayer: "countries"); + sourceLayer: "transportation"); } static Future addImage(MapLibreMapController controller) async { await controller.addSource( - "radar", - const ImageSourceProperties( - url: "https://docs.mapbox.com/mapbox-gl-js/assets/radar.gif", - coordinates: [ - [-80.425, 46.437], - [-71.516, 46.437], - [-71.516, 37.936], - [-80.425, 37.936] - ])); + "radar", + const ImageSourceProperties( + url: "https://maplibre.org/maplibre-gl-js/docs/assets/radar.gif", + coordinates: [ + [-80.425, 46.437], + [-71.516, 46.437], + [-71.516, 37.936], + [-80.425, 37.936] + ], + ), + ); await controller.addRasterLayer( "radar", @@ -141,16 +161,20 @@ class FullMapState extends State { static Future addVideo(MapLibreMapController controller) async { await controller.addSource( - "video", - const VideoSourceProperties(urls: [ + "video", + const VideoSourceProperties( + urls: [ 'https://static-assets.mapbox.com/mapbox-gl-js/drone.mp4', 'https://static-assets.mapbox.com/mapbox-gl-js/drone.webm' - ], coordinates: [ + ], + coordinates: [ [-122.51596391201019, 37.56238816766053], [-122.51467645168304, 37.56410183312965], [-122.51309394836426, 37.563391708549425], [-122.51423120498657, 37.56161849366671] - ])); + ], + ), + ); await controller.addRasterLayer( "video", @@ -161,10 +185,14 @@ class FullMapState extends State { static Future addHeatMap(MapLibreMapController controller) async { await controller.addSource( - 'earthquakes-heatmap-source', - const GeojsonSourceProperties( - data: - 'https://maplibre.org/maplibre-gl-js/docs/assets/earthquakes.geojson')); + 'earthquakes-heatmap-source', + const GeojsonSourceProperties( + data: + 'https://maplibre.org/maplibre-gl-js/docs/assets/earthquakes.geojson', + attribution: + 'Earthquake data © MapLibre', + ), + ); await controller.addLayer( 'earthquakes-heatmap-source', @@ -236,20 +264,94 @@ class FullMapState extends State { ); } - static Future addDem(MapLibreMapController controller) async { - // TODO: adapt example? - // await controller.addSource( - // "dem", - // RasterDemSourceProperties( - // url: "mapbox://mapbox.mapbox-terrain-dem-v1")); + static Future addCountries(MapLibreMapController controller) async { + // Add a simple additional layer to demonstrate layering on the default style + // Remove existing layers/source if they exist (in case of re-loading) + try { + await controller.removeLayer("countries-fill"); + } catch (e) { + // Layer doesn't exist, ignore + } + try { + await controller.removeLayer("countries-outline"); + } catch (e) { + // Layer doesn't exist, ignore + } + try { + await controller.removeSource("countries-highlight"); + } catch (e) { + // Source doesn't exist, ignore + } - // await controller.addLayer( - // "dem", - // "hillshade", - // HillshadeLayerProperties( - // hillshadeExaggeration: 1, - // hillshadeShadowColor: Colors.blue.toHexStringRGB()), - // ); + // Source: Natural Earth public domain data + // Free vector and raster map data @ naturalearthdata.com + await controller.addSource( + "countries-highlight", + const GeojsonSourceProperties( + data: + 'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_110m_admin_0_countries.geojson', + attribution: + 'GeoJSON data courtesy of Natural Earth', + ), + ); + + await controller.addLayer( + "countries-highlight", + "countries-fill", + const FillLayerProperties( + fillColor: "#627BC1", + fillOpacity: 0.3, + ), + ); + + await controller.addLayer( + "countries-highlight", + "countries-outline", + const LineLayerProperties( + lineColor: "#627BC1", + lineWidth: 2, + ), + ); + } + + static Future addDemHillshade(MapLibreMapController controller) async { + // Remove existing layers/source if they exist + try { + await controller.removeLayer("hillshade-layer"); + } catch (e) { + // Layer doesn't exist, ignore + } + try { + await controller.removeSource("terrarium-dem"); + } catch (e) { + // Source doesn't exist, ignore + } + + // Source: Terrarium terrain tiles + await controller.addSource( + "terrarium-dem", + const RasterDemSourceProperties( + tiles: [ + 'https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png' + ], + minzoom: 0, + maxzoom: 15, + tileSize: 256, + encoding: 'terrarium', + attribution: + 'Elevation data © AWS Terrain Tiles', + ), + ); + + await controller.addLayer( + "terrarium-dem", + "hillshade-layer", + HillshadeLayerProperties( + hillshadeExaggeration: 0.8, + hillshadeShadowColor: Colors.blue.shade900.toHexStringRGB(), + hillshadeHighlightColor: Colors.white.toHexStringRGB(), + ), + ); } final _stylesAndLoaders = [ @@ -260,14 +362,20 @@ class FullMapState extends State { position: CameraPosition(target: LatLng(33.3832, -118.4333), zoom: 6), ), const StyleInfo( - name: "Default style", + name: "Countries GeoJSON", // Using the raw github file version of MapLibreStyles.DEMO here, because we need to // specify a different baseStyle for consecutive elements in this list, // otherwise the map will not update - baseStyle: - "https://raw.githubusercontent.com/maplibre/demotiles/gh-pages/style.json", - addDetails: addDem, - position: CameraPosition(target: LatLng(33.5, -118.1), zoom: 8), + baseStyle: MapLibreStyles.demo, + addDetails: addCountries, + position: CameraPosition(target: LatLng(20, 0), zoom: 2), + ), + const StyleInfo( + name: "DEM Hillshade", + baseStyle: MapLibreStyles.demo, + addDetails: addDemHillshade, + position: CameraPosition( + target: LatLng(46.5, 8.0), zoom: 8, bearing: 80, tilt: 60), ), const StyleInfo( name: "Geojson cluster", @@ -277,22 +385,19 @@ class FullMapState extends State { ), const StyleInfo( name: "Raster", - baseStyle: - "https://raw.githubusercontent.com/maplibre/demotiles/gh-pages/style.json", + baseStyle: MapLibreStyles.demo, addDetails: addRaster, position: CameraPosition(target: LatLng(40, -100), zoom: 3), ), const StyleInfo( name: "Image", - baseStyle: - "https://raw.githubusercontent.com/maplibre/demotiles/gh-pages/style.json?", + baseStyle: MapLibreStyles.demo, addDetails: addImage, position: CameraPosition(target: LatLng(43, -75), zoom: 6), ), const StyleInfo( name: "Heatmap", - baseStyle: - "https://raw.githubusercontent.com/maplibre/demotiles/gh-pages/style.json", + baseStyle: MapLibreStyles.demo, addDetails: addHeatMap, position: CameraPosition(target: LatLng(33.5, -118.1), zoom: 2), ), @@ -300,18 +405,27 @@ class FullMapState extends State { if (kIsWeb) const StyleInfo( name: "Video", - baseStyle: - "https://raw.githubusercontent.com/maplibre/demotiles/gh-pages/style.json", + baseStyle: MapLibreStyles.demo, addDetails: addVideo, position: CameraPosition( target: LatLng(37.562984, -122.514426), zoom: 17, bearing: -96), ), ]; + Future _loadCurrentSource() async { + if (controller == null) return; + final styleInfo = _stylesAndLoaders[selectedStyleId]; + // Reload the style to clear all previous layers/sources + await controller!.setStyle(styleInfo.baseStyle); + // Wait for style to load, then add the new source details + // The onStyleLoadedCallback will be triggered again, but we need to prevent recursion + } + Future _onStyleLoadedCallback() async { + if (controller == null) return; final styleInfo = _stylesAndLoaders[selectedStyleId]; - styleInfo.addDetails(controller!); - controller! + await styleInfo.addDetails(controller!); + await controller! .animateCamera(CameraUpdate.newCameraPosition(styleInfo.position)); } @@ -328,10 +442,13 @@ class FullMapState extends State { icon: const Icon(Icons.swap_horiz), label: SizedBox( width: 120, child: Center(child: Text("To $nextName"))), - onPressed: () => setState( - () => selectedStyleId = - (selectedStyleId + 1) % _stylesAndLoaders.length, - ), + onPressed: () async { + setState(() { + selectedStyleId = + (selectedStyleId + 1) % _stylesAndLoaders.length; + }); + await _loadCurrentSource(); + }, ), ), body: Stack( From 6d753c484481d87cc9f32a96d377f811d9c27756 Mon Sep 17 00:00:00 2001 From: Gabriel Palmisano Date: Wed, 7 Jan 2026 20:17:59 +0100 Subject: [PATCH 36/38] chore: updated changelog --- maplibre_gl/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/maplibre_gl/CHANGELOG.md b/maplibre_gl/CHANGELOG.md index aa8f1cf79..806122f13 100644 --- a/maplibre_gl/CHANGELOG.md +++ b/maplibre_gl/CHANGELOG.md @@ -3,6 +3,7 @@ ### Added * Logo customization options including visibility and position settings (#b4fb174). * Explicit annotation manager initialization with clear error handling (#668). +* iOS: Attribution support for tile and raster sources with HTML link parsing. ### Changed * MapLibre Android SDK upgraded from `11.13.5` to `12.3.0` (#690). @@ -27,6 +28,7 @@ * Fixed `setLayerProperties` and pattern images on web and Android (#9ce52a6). - Pattern images now correctly converted to RGBA format on web - Fixed mismatched image size error when loading pattern images + ### Refactor * Complete refactor of example app with new UI and improved user experience (#ac877a4). * Refactored `cameraTargetBounds` implementation on Android and iOS for consistent behavior (#8bcd74a). From 89dc6e6e45fddd68febb9b178b68f5ae26e12d8d Mon Sep 17 00:00:00 2001 From: Gabriel Palmisano Date: Wed, 7 Jan 2026 21:42:19 +0100 Subject: [PATCH 37/38] fix: update attribution text for various layers to include copyright symbols and improve formatting --- .../lib/examples/layers/various_sources.dart | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/maplibre_gl_example/lib/examples/layers/various_sources.dart b/maplibre_gl_example/lib/examples/layers/various_sources.dart index ee60e0bab..bd1f89077 100644 --- a/maplibre_gl_example/lib/examples/layers/various_sources.dart +++ b/maplibre_gl_example/lib/examples/layers/various_sources.dart @@ -55,7 +55,7 @@ class FullMapState extends State { ], tileSize: 256, attribution: - '© OpenStreetMap contributors'), + '© OpenStreetMap contributors'), ); await controller.addLayer( "osm-raster", "osm-raster", const RasterLayerProperties()); @@ -73,7 +73,7 @@ class FullMapState extends State { clusterRadius: 50, // Radius of each cluster when clustering points (defaults to 50) attribution: - 'Earthquake data © MapLibre'), + 'Earthquake data © MapLibre'), ); await controller.addLayer( "earthquakes", @@ -114,7 +114,7 @@ class FullMapState extends State { ], maxzoom: 14, attribution: - '© MapLibre contributors', + '© MapLibre contributors { data: 'https://maplibre.org/maplibre-gl-js/docs/assets/earthquakes.geojson', attribution: - 'Earthquake data © MapLibre', + 'Earthquake data © MapLibre', ), ); @@ -291,7 +291,7 @@ class FullMapState extends State { data: 'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_110m_admin_0_countries.geojson', attribution: - 'GeoJSON data courtesy of Natural Earth', + 'GeoJSON data courtesy of Natural Earth', ), ); @@ -339,7 +339,7 @@ class FullMapState extends State { tileSize: 256, encoding: 'terrarium', attribution: - 'Elevation data © AWS Terrain Tiles', + 'Elevation data © AWS Terrain Tiles', ), ); @@ -436,20 +436,17 @@ class FullMapState extends State { _stylesAndLoaders[(selectedStyleId + 1) % _stylesAndLoaders.length] .name; return Scaffold( - floatingActionButton: Padding( - padding: const EdgeInsets.all(32.0), - child: FloatingActionButton.extended( - icon: const Icon(Icons.swap_horiz), - label: SizedBox( - width: 120, child: Center(child: Text("To $nextName"))), - onPressed: () async { - setState(() { - selectedStyleId = - (selectedStyleId + 1) % _stylesAndLoaders.length; - }); - await _loadCurrentSource(); - }, - ), + floatingActionButton: FloatingActionButton.extended( + icon: const Icon(Icons.swap_horiz), + label: + SizedBox(width: 120, child: Center(child: Text("To $nextName"))), + onPressed: () async { + setState(() { + selectedStyleId = + (selectedStyleId + 1) % _stylesAndLoaders.length; + }); + await _loadCurrentSource(); + }, ), body: Stack( children: [ @@ -458,6 +455,8 @@ class FullMapState extends State { onMapCreated: _onMapCreated, initialCameraPosition: styleInfo.position, onStyleLoadedCallback: _onStyleLoadedCallback, + logoEnabled: true, + attributionButtonPosition: AttributionButtonPosition.bottomLeft, ), Container( padding: const EdgeInsets.all(8), From c78a5bec314e2c85ac718202e9fd0bf61c603e17 Mon Sep 17 00:00:00 2001 From: Gabriel Palmisano Date: Wed, 7 Jan 2026 21:43:10 +0100 Subject: [PATCH 38/38] fix: update attribution text in various layers to use copyright symbol --- .../lib/examples/layers/various_sources.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/maplibre_gl_example/lib/examples/layers/various_sources.dart b/maplibre_gl_example/lib/examples/layers/various_sources.dart index bd1f89077..6082b0a21 100644 --- a/maplibre_gl_example/lib/examples/layers/various_sources.dart +++ b/maplibre_gl_example/lib/examples/layers/various_sources.dart @@ -55,7 +55,7 @@ class FullMapState extends State { ], tileSize: 256, attribution: - '© OpenStreetMap contributors'), + '© OpenStreetMap contributors'), ); await controller.addLayer( "osm-raster", "osm-raster", const RasterLayerProperties()); @@ -73,7 +73,7 @@ class FullMapState extends State { clusterRadius: 50, // Radius of each cluster when clustering points (defaults to 50) attribution: - 'Earthquake data © MapLibre'), + 'Earthquake data © MapLibre'), ); await controller.addLayer( "earthquakes", @@ -114,7 +114,7 @@ class FullMapState extends State { ], maxzoom: 14, attribution: - '© MapLibre contributors© MapLibre contributors { data: 'https://maplibre.org/maplibre-gl-js/docs/assets/earthquakes.geojson', attribution: - 'Earthquake data © MapLibre', + 'Earthquake data © MapLibre', ), ); @@ -339,7 +339,7 @@ class FullMapState extends State { tileSize: 256, encoding: 'terrarium', attribution: - 'Elevation data © AWS Terrain Tiles', + 'Elevation data © AWS Terrain Tiles', ), );