Skip to content

Commit 6cb793d

Browse files
authored
feat: add image helper functions, aboveLayerId, atIndex parameter (#391)
- [x] addImages(Map<String, bytes>) - [x] addImageFromIconData() - [x] addImageFromAssets() - [x] aboveLayerId parameter - [x] atIndex parameter - [x] fix offline region check - [x] remove manual release of NSObjects (causes crashes)
1 parent 05bfb44 commit 6cb793d

File tree

7 files changed

+190
-28
lines changed

7 files changed

+190
-28
lines changed

example/integration_test/controller_test.dart

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'dart:math';
44
import 'package:flutter/foundation.dart';
55
import 'package:flutter/material.dart';
66
import 'package:flutter_test/flutter_test.dart';
7+
import 'package:http/http.dart' as http;
78
import 'package:integration_test/integration_test.dart';
89
import 'package:maplibre/maplibre.dart';
910
import 'package:maplibre_example/utils/map_styles.dart';
@@ -943,4 +944,62 @@ void main() {
943944
await ctrl.style?.addLayer(layer);
944945
await tester.pumpAndSettle();
945946
});
947+
948+
testWidgets('addImage', (tester) async {
949+
final ctrlCompleter = Completer<MapController>();
950+
final app = App(onMapCreated: ctrlCompleter.complete);
951+
await tester.pumpWidget(app);
952+
final ctrl = await ctrlCompleter.future;
953+
954+
const imageUrl =
955+
'https://upload.wikimedia.org/wikipedia/commons/f/f2/678111-map-marker-512.png';
956+
final imageBytes = await http.readBytes(Uri.parse(imageUrl));
957+
958+
await ctrl.style?.addImage('test-icon', imageBytes);
959+
await tester.pumpAndSettle();
960+
});
961+
962+
testWidgets('addImages', (tester) async {
963+
final ctrlCompleter = Completer<MapController>();
964+
final app = App(onMapCreated: ctrlCompleter.complete);
965+
await tester.pumpWidget(app);
966+
final ctrl = await ctrlCompleter.future;
967+
968+
const redPinUrl =
969+
'https://upload.wikimedia.org/wikipedia/commons/f/f2/678111-map-marker-512.png';
970+
const blackPinUrl =
971+
'https://upload.wikimedia.org/wikipedia/commons/3/3b/Blackicon.png';
972+
973+
final images = <String, Uint8List>{
974+
'image1': await http.readBytes(Uri.parse(redPinUrl)),
975+
'image2': await http.readBytes(Uri.parse(blackPinUrl)),
976+
};
977+
978+
await ctrl.style?.addImages(images);
979+
await tester.pumpAndSettle();
980+
});
981+
982+
testWidgets('removeImage', (tester) async {
983+
final ctrlCompleter = Completer<MapController>();
984+
final app = App(onMapCreated: ctrlCompleter.complete);
985+
await tester.pumpWidget(app);
986+
final ctrl = await ctrlCompleter.future;
987+
988+
// Download the red pin image from Wikipedia (same as used in example)
989+
const imageUrl =
990+
'https://upload.wikimedia.org/wikipedia/commons/f/f2/678111-map-marker-512.png';
991+
final imageBytes = await http.readBytes(Uri.parse(imageUrl));
992+
993+
// Add image first
994+
await ctrl.style?.addImage('test-image', imageBytes);
995+
await tester.pumpAndSettle();
996+
997+
// Then remove it
998+
await ctrl.style?.removeImage('test-image');
999+
await tester.pumpAndSettle();
1000+
1001+
// Ensure no crash if image doesn't exist
1002+
await ctrl.style?.removeImage('test-image');
1003+
await tester.pumpAndSettle();
1004+
});
9461005
}

example/lib/layers_marker_page.dart

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import 'package:flutter/material.dart';
2-
import 'package:http/http.dart' as http;
32
import 'package:maplibre/maplibre.dart';
4-
import 'package:maplibre_example/style_layers_symbol_page.dart';
53

64
@immutable
75
class LayersMarkerPage extends StatefulWidget {
@@ -36,11 +34,11 @@ class _LayersMarkerPageState extends State<LayersMarkerPage> {
3634
switch (event) {
3735
case MapEventStyleLoaded():
3836
// add marker image to map
39-
final response = await http.get(
40-
Uri.parse(StyleLayersSymbolPage.imageUrl),
37+
await event.style.addImageFromIconData(
38+
id: 'marker',
39+
iconData: Icons.location_on,
40+
color: Colors.red,
4141
);
42-
final bytes = response.bodyBytes;
43-
await event.style.addImage('marker', bytes);
4442
setState(() {
4543
_imageLoaded = true;
4644
});
@@ -60,7 +58,7 @@ class _LayersMarkerPageState extends State<LayersMarkerPage> {
6058
textField: 'Marker',
6159
textAllowOverlap: true,
6260
iconImage: _imageLoaded ? 'marker' : null,
63-
iconSize: 0.08,
61+
iconSize: 0.15,
6462
iconAnchor: IconAnchor.bottom,
6563
textOffset: const [0, 1],
6664
),

lib/src/platform/android/style_controller.dart

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
part of 'map_state.dart';
22

33
/// Android specific implementation of the [StyleController].
4-
class StyleControllerAndroid implements StyleController {
4+
class StyleControllerAndroid extends StyleController {
55
const StyleControllerAndroid._(this._jStyle);
66

77
final jni.Style _jStyle;
@@ -10,6 +10,8 @@ class StyleControllerAndroid implements StyleController {
1010
Future<void> addLayer(
1111
StyleLayer layer, {
1212
String? belowLayerId,
13+
String? aboveLayerId,
14+
int? atIndex,
1315
}) async => using((arena) {
1416
final jId = layer.id.toJString()..releasedBy(arena);
1517
final prevLayer = _jStyle.getLayer(jId);
@@ -89,6 +91,10 @@ class StyleControllerAndroid implements StyleController {
8991
// add to style
9092
if (belowLayerId case final String belowId) {
9193
_jStyle.addLayerBelow(jLayer, belowId.toJString()..releasedBy(arena));
94+
} else if (aboveLayerId case final String aboveId) {
95+
_jStyle.addLayerAbove(jLayer, aboveId.toJString()..releasedBy(arena));
96+
} else if (atIndex case final int index) {
97+
_jStyle.addLayerAt(jLayer, index);
9298
} else {
9399
_jStyle.addLayer(jLayer);
94100
}

lib/src/platform/ios/offline_manager.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ class OfflineManagerIos extends OfflineManagerNative {
7979
final jsonBytes = ffiPack.context.toList();
8080
final json = jsonDecode(utf8.decode(jsonBytes)) as Map<String, Object?>;
8181
// print(json);
82-
if (json['id'] != regionId) {
82+
if (json['id'] == regionId) {
8383
final ffiRegion = MLNTilePyramidOfflineRegion.castFrom(ffiPack.region);
8484
return OfflineRegion(
8585
id: regionId,

lib/src/platform/ios/style_controller.dart

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
part of 'map_state.dart';
22

33
/// Android specific implementation of the [StyleController].
4-
class StyleControllerIos implements StyleController {
4+
class StyleControllerIos extends StyleController {
55
StyleControllerIos._(this._ffiStyle, this._hostApi);
66

77
final MLNStyle _ffiStyle;
@@ -24,7 +24,12 @@ class StyleControllerIos implements StyleController {
2424
}
2525

2626
@override
27-
Future<void> addLayer(StyleLayer layer, {String? belowLayerId}) async {
27+
Future<void> addLayer(
28+
StyleLayer layer, {
29+
String? belowLayerId,
30+
String? aboveLayerId,
31+
int? atIndex,
32+
}) async {
2833
final ffiId = layer.id.toNSString();
2934
final prevStyleLayer = _ffiStyle.layerWithIdentifier(ffiId);
3035
if (prevStyleLayer != null) {
@@ -85,9 +90,24 @@ class StyleControllerIos implements StyleController {
8590
ffiStyleLayer.maximumZoomLevel = layer.maxZoom;
8691
ffiStyleLayer.setProperties(layer.paint);
8792
ffiStyleLayer.setProperties(layer.layout);
88-
_ffiStyle.addLayer(ffiStyleLayer);
89-
ffiStyleLayer.release();
90-
ffiId.release();
93+
94+
if (belowLayerId case final String id) {
95+
final belowLayer = _ffiStyle.layerWithIdentifier(id.toNSString());
96+
if (belowLayer == null) {
97+
throw Exception('Layer "$id" does not exist.');
98+
}
99+
_ffiStyle.insertLayer$1(ffiStyleLayer, belowLayer: belowLayer);
100+
} else if (aboveLayerId case final String id) {
101+
final aboveLayer = _ffiStyle.layerWithIdentifier(id.toNSString());
102+
if (aboveLayer == null) {
103+
throw Exception('Layer "$id" does not exist.');
104+
}
105+
_ffiStyle.insertLayer$2(ffiStyleLayer, aboveLayer: aboveLayer);
106+
} else if (atIndex case final int index) {
107+
_ffiStyle.insertLayer(ffiStyleLayer, atIndex: index);
108+
} else {
109+
_ffiStyle.addLayer(ffiStyleLayer);
110+
}
91111
}
92112

93113
@override
@@ -200,8 +220,6 @@ class StyleControllerIos implements StyleController {
200220
);
201221
}
202222
_ffiStyle.addSource(ffiSource);
203-
ffiSource.release();
204-
ffiId.release();
205223
}
206224

207225
@override
@@ -220,7 +238,6 @@ class StyleControllerIos implements StyleController {
220238
Future<void> removeImage(String id) async {
221239
final ffiId = id.toNSString();
222240
_ffiStyle.removeImageForName(ffiId);
223-
ffiId.release();
224241
}
225242

226243
@override
@@ -229,7 +246,6 @@ class StyleControllerIos implements StyleController {
229246
final ffiLayer = _ffiStyle.layerWithIdentifier(ffiId);
230247
if (ffiLayer == null) return;
231248
_ffiStyle.removeLayer(ffiLayer);
232-
ffiId.release();
233249
}
234250

235251
@override
@@ -238,7 +254,6 @@ class StyleControllerIos implements StyleController {
238254
final ffiSource = _ffiStyle.sourceWithIdentifier(ffiId);
239255
if (ffiSource == null) return;
240256
_ffiStyle.removeSource(ffiSource);
241-
ffiId.release();
242257
}
243258

244259
@override

lib/src/platform/web/style_controller.dart

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
part of 'map_state.dart';
22

33
/// Web specific implementation of the [StyleController].
4-
class StyleControllerWeb implements StyleController {
4+
class StyleControllerWeb extends StyleController {
55
/// Create a new [StyleControllerWeb] instance.
66
const StyleControllerWeb(this._map);
77

@@ -70,7 +70,12 @@ class StyleControllerWeb implements StyleController {
7070
}
7171

7272
@override
73-
Future<void> addLayer(StyleLayer layer, {String? belowLayerId}) async {
73+
Future<void> addLayer(
74+
StyleLayer layer, {
75+
String? belowLayerId,
76+
String? aboveLayerId,
77+
int? atIndex,
78+
}) async {
7479
if (_map.getLayer(layer.id) != null) {
7580
throw Exception(
7681
'A Layer with the id "${layer.id}" already exists in the map style.',

lib/src/style_controller.dart

Lines changed: 86 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,55 @@
1+
import 'dart:ui';
2+
13
import 'package:flutter/foundation.dart';
4+
import 'package:flutter/services.dart';
5+
import 'package:flutter/widgets.dart';
26
import 'package:maplibre/maplibre.dart';
37

48
/// The [StyleController] can be used to manipulate the style of
59
/// a [MapLibreMap]. It can be accessed via [MapController.style].
610
///
711
/// {@category Basic}
8-
abstract interface class StyleController {
12+
abstract class StyleController {
13+
/// Abstract base constructor for implementations.
14+
const StyleController();
15+
916
/// Add a new source to the map.
1017
Future<void> addSource(Source source);
1118

1219
/// Add a new layer to the map. The source must be added before adding it to
1320
/// the map.
1421
///
15-
/// `belowLayerId` The ID of an existing layer to insert the new layer before,
22+
/// [belowLayerId] The ID of an existing layer to insert the new layer before,
1623
/// resulting in the new layer appearing visually beneath the existing layer.
17-
/// If this argument is not specified, the layer will be appended to the end
18-
/// of the layers array and appear visually above all other layers.
19-
Future<void> addLayer(StyleLayer layer, {String? belowLayerId});
24+
///
25+
/// [aboveLayerId] The ID of an existing layer to insert the new layer after,
26+
/// resulting in the new layer appearing visually above the existing layer.
27+
/// **This parameter will be ignored on web.**
28+
///
29+
/// [atIndex] is the position at which to insert the layer. An index of 0
30+
/// would send the layer to the back; an index equal to the number of
31+
/// objects in the layers property would bring the layer to the front.
32+
/// **This parameter will be ignored on web.**
33+
///
34+
/// If more than one positioning parameter is specified, it will first try to
35+
/// use [belowLayerId], then [aboveLayerId] and finally [atIndex].
36+
/// If no positioning parameter is specified, the layer
37+
/// will be appended to the end of the layers array and appear visually
38+
/// above all other layers.
39+
Future<void> addLayer(
40+
StyleLayer layer, {
41+
String? belowLayerId,
42+
String? aboveLayerId,
43+
int? atIndex,
44+
});
2045

2146
/// Update the data of a GeoJSON source.
2247
Future<void> updateGeoJsonSource({required String id, required String data});
2348

24-
/// Removes the layer with the given ID from the map's style.
49+
/// Removes the layer with the given [id] from the map's style.
2550
Future<void> removeLayer(String id);
2651

27-
/// Removes the source with the given ID from the map's style.
52+
/// Removes the source with the given [id] from the map's style.
2853
Future<void> removeSource(String id);
2954

3055
/// Get a list of all attributions from the map style.
@@ -40,6 +65,60 @@ abstract interface class StyleController {
4065
/// Add an image to the map.
4166
Future<void> addImage(String id, Uint8List bytes);
4267

68+
/// Add multiple images to the map where the key is the image ID and the
69+
/// value is the image bytes.
70+
Future<void> addImages(Map<String, Uint8List> images) => Future.wait(
71+
images.entries.map((e) => addImage(e.key, e.value)),
72+
);
73+
74+
/// Load an image from the Flutter assets to the map by its [asset] path.
75+
Future<void> addImageFromAssets({
76+
required String id,
77+
required String asset,
78+
}) async {
79+
final byteData = await rootBundle.load(asset);
80+
final bytes = byteData.buffer.asUint8List();
81+
await addImage(id, bytes);
82+
}
83+
84+
/// Create an image from [IconData] and add it to the map with the given [id].
85+
///
86+
/// The [size] parameter defines the width and height of the resulting image
87+
/// in pixels.
88+
///
89+
/// The [color] parameter defines the color of the icon. By default, it is
90+
/// black.
91+
Future<void> addImageFromIconData({
92+
required String id,
93+
required IconData iconData,
94+
int size = 200,
95+
Color color = const Color(0xFF000000),
96+
}) async {
97+
final pictureRecorder = PictureRecorder();
98+
final canvas = Canvas(pictureRecorder);
99+
100+
TextPainter(textDirection: TextDirection.ltr)
101+
..text = TextSpan(
102+
text: String.fromCharCode(iconData.codePoint),
103+
style: TextStyle(
104+
letterSpacing: 0,
105+
fontSize: size.toDouble(),
106+
fontFamily: iconData.fontFamily,
107+
package: iconData.fontPackage,
108+
color: color,
109+
),
110+
)
111+
..layout()
112+
..paint(canvas, Offset.zero);
113+
114+
final picture = pictureRecorder.endRecording();
115+
final image = await picture.toImage(size, size);
116+
final bytes = await image.toByteData(format: ImageByteFormat.png);
117+
if (bytes == null) return;
118+
119+
await addImage(id, bytes.buffer.asUint8List());
120+
}
121+
43122
/// Removes an image from the map
44123
Future<void> removeImage(String id);
45124

0 commit comments

Comments
 (0)