From 44a6eac0dae8de5d310d023600691a757e43f14e Mon Sep 17 00:00:00 2001 From: m0nac0 <58807793+m0nac0@users.noreply.github.com> Date: Fri, 8 Dec 2023 08:46:47 +0100 Subject: [PATCH 1/5] Add docs for setCameraBounds (#351) Closes #349 --- lib/src/controller.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/src/controller.dart b/lib/src/controller.dart index d02d3d2f..24873b64 100644 --- a/lib/src/controller.dart +++ b/lib/src/controller.dart @@ -1214,6 +1214,10 @@ class MaplibreMapController extends ChangeNotifier { return _maplibreGlPlatform.addSource(sourceid, properties); } + /// Pans and zooms the map to contain its visible area within the specified geographical bounds. + /// + /// Also consider using [animateCamera] or [moveCamera], which allow you to set camera bounds (with different padding values per side) + /// as well as other camera properties. Future setCameraBounds({ required double west, required double north, From 58c7c0a8a3020ea4b9317d21a7b8bbdc61ed05c0 Mon Sep 17 00:00:00 2001 From: m0nac0 <58807793+m0nac0@users.noreply.github.com> Date: Fri, 8 Dec 2023 08:50:32 +0100 Subject: [PATCH 2/5] Add iOS long click duration parameter (#348) Closes #235 --- example/lib/custom_marker.dart | 1 + ios/Classes/MapboxMapController.swift | 14 +++++++++++++- lib/src/maplibre_map.dart | 9 +++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/example/lib/custom_marker.dart b/example/lib/custom_marker.dart index b61ec74e..c4ef1091 100644 --- a/example/lib/custom_marker.dart +++ b/example/lib/custom_marker.dart @@ -98,6 +98,7 @@ class CustomMarkerState extends State { onStyleLoadedCallback: _onStyleLoadedCallback, initialCameraPosition: const CameraPosition(target: LatLng(35.0, 135.0), zoom: 5), + iosLongClickDuration: const Duration(milliseconds: 200), ), IgnorePointer( ignoring: true, diff --git a/ios/Classes/MapboxMapController.swift b/ios/Classes/MapboxMapController.swift index 61ffb884..53e13e36 100644 --- a/ios/Classes/MapboxMapController.swift +++ b/ios/Classes/MapboxMapController.swift @@ -70,9 +70,10 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma { longPress.require(toFail: recognizer) } - mapView.addGestureRecognizer(longPress) + var longPressRecognizerAdded = false if let args = args as? [String: Any] { + Convert.interpretMapboxMapOptions(options: args["options"], delegate: self) if let initialCameraPosition = args["initialCameraPosition"] as? [String: Any], let camera = MGLMapCamera.fromDict(initialCameraPosition, mapView: mapView), @@ -98,6 +99,12 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma if let enabled = args["dragEnabled"] as? Bool { dragEnabled = enabled } + + if let iosLongClickDurationMilliseconds = args["iosLongClickDurationMilliseconds"] as? Int { + longPress.minimumPressDuration = TimeInterval(iosLongClickDurationMilliseconds) / 1000 + mapView.addGestureRecognizer(longPress) + longPressRecognizerAdded = true + } } if dragEnabled { let pan = UIPanGestureRecognizer( @@ -107,6 +114,11 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma pan.delegate = self mapView.addGestureRecognizer(pan) } + + if(!longPressRecognizerAdded) { + mapView.addGestureRecognizer(longPress) + longPressRecognizerAdded = true + } } func gestureRecognizer( diff --git a/lib/src/maplibre_map.dart b/lib/src/maplibre_map.dart index 2bcecf96..feae6e2f 100644 --- a/lib/src/maplibre_map.dart +++ b/lib/src/maplibre_map.dart @@ -36,6 +36,7 @@ class MaplibreMap extends StatefulWidget { this.compassViewMargins, this.attributionButtonPosition = AttributionButtonPosition.BottomRight, this.attributionButtonMargins, + this.iosLongClickDuration, this.onMapClick, this.onUserLocationUpdated, this.onMapLongClick, @@ -88,6 +89,11 @@ class MaplibreMap extends StatefulWidget { /// The initial position of the map's camera. final CameraPosition initialCameraPosition; + /// How long a user has to click the map **on iOS** until a long click is registered. + /// Has no effect on web or Android. Can not be changed at runtime, only the initial value is used. + /// If null, the default value of the native MapLibre library / of the OS is used. + final Duration? iosLongClickDuration; + /// True if the map should show a compass when rotated. final bool compassEnabled; @@ -260,6 +266,9 @@ class _MaplibreMapState extends State { 'options': _MaplibreMapOptions.fromWidget(widget).toMap(), //'onAttributionClickOverride': widget.onAttributionClick != null, 'dragEnabled': widget.dragEnabled, + if (widget.iosLongClickDuration != null) + 'iosLongClickDurationMilliseconds': + widget.iosLongClickDuration!.inMilliseconds, }; return _maplibreGlPlatform.buildView( creationParams, onPlatformViewCreated, widget.gestureRecognizers); From dc21739b9d4395b7df7f181e96d678ab0383294d Mon Sep 17 00:00:00 2001 From: Krupupakku Date: Sat, 9 Dec 2023 13:34:34 +0100 Subject: [PATCH 3/5] supporting fill-extrusion (#211) # Pull Request ## Description Hey! I needed to use extrusions in my project and I thought maybe you want to add support to them. This is the result in example Layer page, adding controller.addFillExtrusionLayer: --------- Co-authored-by: m0nac0 <58807793+m0nac0@users.noreply.github.com> --- README.md | 1 + .../mapboxgl/LayerPropertyConverter.java | 49 ++++- .../mapbox/mapboxgl/MapboxMapController.java | 65 ++++++ example/lib/layer.dart | 36 ++++ ios/Classes/LayerPropertyConverter.swift | 30 +++ ios/Classes/MapboxMapController.swift | 73 +++++++ lib/src/controller.dart | 46 +++++ lib/src/layer_properties.dart | 188 +++++++++++++++++- .../src/maplibre_gl_platform_interface.dart | 9 + .../lib/src/method_channel_maplibre_gl.dart | 23 +++ .../lib/src/source_properties.dart | 19 +- .../lib/src/maplibre_web_gl_platform.dart | 18 ++ scripts/README.md | 1 + scripts/input/style.json | 18 +- scripts/lib/conversions.dart | 3 + scripts/lib/generate.dart | 2 + .../LayerPropertyConverter.swift.template | 12 +- 17 files changed, 566 insertions(+), 27 deletions(-) create mode 100644 scripts/README.md diff --git a/README.md b/README.md index e2a064d0..cf86bfec 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ Include the following JavaScript and CSS files in the `` of the `web/index | Circle | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Line | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Fill | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Fill Extrusion | :white_check_mark: | :white_check_mark: | :white_check_mark: | ## Map Styles diff --git a/android/src/main/java/com/mapbox/mapboxgl/LayerPropertyConverter.java b/android/src/main/java/com/mapbox/mapboxgl/LayerPropertyConverter.java index 14dbd7e2..59992e62 100644 --- a/android/src/main/java/com/mapbox/mapboxgl/LayerPropertyConverter.java +++ b/android/src/main/java/com/mapbox/mapboxgl/LayerPropertyConverter.java @@ -15,6 +15,7 @@ import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; + import static com.mapbox.mapboxgl.Convert.toMap; class LayerPropertyConverter { @@ -106,9 +107,9 @@ static PropertyValue[] interpretSymbolLayerProperties(Object o) { properties.add(PropertyFactory.iconTextFitPadding(expression)); break; case "icon-image": - if (jsonElement.isJsonPrimitive() && jsonElement.getAsJsonPrimitive().isString()) { + if(jsonElement.isJsonPrimitive() && jsonElement.getAsJsonPrimitive().isString()){ properties.add(PropertyFactory.iconImage(jsonElement.getAsString())); - } else { + }else{ properties.add(PropertyFactory.iconImage(expression)); } break; @@ -375,6 +376,50 @@ static PropertyValue[] interpretFillLayerProperties(Object o) { return properties.toArray(new PropertyValue[properties.size()]); } + static PropertyValue[] interpretFillExtrusionLayerProperties(Object o) { + final Map data = (Map) toMap(o); + final List properties = new LinkedList(); + final JsonParser parser = new JsonParser(); + + for (Map.Entry entry : data.entrySet()) { + final JsonElement jsonElement = parser.parse(entry.getValue()); + Expression expression = Expression.Converter.convert(jsonElement); + switch (entry.getKey()) { + case "fill-extrusion-opacity": + properties.add(PropertyFactory.fillExtrusionOpacity(expression)); + break; + case "fill-extrusion-color": + properties.add(PropertyFactory.fillExtrusionColor(expression)); + break; + case "fill-extrusion-translate": + properties.add(PropertyFactory.fillExtrusionTranslate(expression)); + break; + case "fill-extrusion-translate-anchor": + properties.add(PropertyFactory.fillExtrusionTranslateAnchor(expression)); + break; + case "fill-extrusion-pattern": + properties.add(PropertyFactory.fillExtrusionPattern(expression)); + break; + case "fill-extrusion-height": + properties.add(PropertyFactory.fillExtrusionHeight(expression)); + break; + case "fill-extrusion-base": + properties.add(PropertyFactory.fillExtrusionBase(expression)); + break; + case "fill-extrusion-vertical-gradient": + properties.add(PropertyFactory.fillExtrusionVerticalGradient(expression)); + break; + case "visibility": + properties.add(PropertyFactory.visibility(entry.getValue().substring(1, entry.getValue().length() - 1))); + break; + default: + break; + } + } + + return properties.toArray(new PropertyValue[properties.size()]); + } + static PropertyValue[] interpretRasterLayerProperties(Object o) { final Map data = (Map) toMap(o); final List properties = new LinkedList(); diff --git a/android/src/main/java/com/mapbox/mapboxgl/MapboxMapController.java b/android/src/main/java/com/mapbox/mapboxgl/MapboxMapController.java index ed82f42a..7e47d502 100644 --- a/android/src/main/java/com/mapbox/mapboxgl/MapboxMapController.java +++ b/android/src/main/java/com/mapbox/mapboxgl/MapboxMapController.java @@ -510,6 +510,40 @@ private void addFillLayer( } } + private void addFillExtrusionLayer( + String layerName, + String sourceName, + String belowLayerId, + String sourceLayer, + Float minZoom, + Float maxZoom, + PropertyValue[] properties, + boolean enableInteraction, + Expression filter) { + FillExtrusionLayer fillLayer = new FillExtrusionLayer(layerName, sourceName); + fillLayer.setProperties(properties); + if (sourceLayer != null) { + fillLayer.setSourceLayer(sourceLayer); + } + if (minZoom != null) { + fillLayer.setMinZoom(minZoom); + } + if (maxZoom != null) { + fillLayer.setMaxZoom(maxZoom); + } + if (filter != null) { + fillLayer.setFilter(filter); + } + if (belowLayerId != null) { + style.addLayerBelow(fillLayer, belowLayerId); + } else { + style.addLayer(fillLayer); + } + if (enableInteraction) { + interactiveFeatureLayerIds.add(layerName); + } + } + private void addCircleLayer( String layerName, String sourceName, @@ -1037,6 +1071,37 @@ public void onError(@NonNull String message) { result.success(null); break; } + case "fillExtrusionLayer#add": + { + final String sourceId = call.argument("sourceId"); + final String layerId = call.argument("layerId"); + final String belowLayerId = call.argument("belowLayerId"); + final String sourceLayer = call.argument("sourceLayer"); + final Double minzoom = call.argument("minzoom"); + final Double maxzoom = call.argument("maxzoom"); + final String filter = call.argument("filter"); + final boolean enableInteraction = call.argument("enableInteraction"); + final PropertyValue[] properties = + LayerPropertyConverter.interpretFillExtrusionLayerProperties( + call.argument("properties")); + + Expression filterExpression = parseFilter(filter); + + addFillExtrusionLayer( + layerId, + sourceId, + belowLayerId, + sourceLayer, + minzoom != null ? minzoom.floatValue() : null, + maxzoom != null ? maxzoom.floatValue() : null, + properties, + enableInteraction, + filterExpression); + updateLocationComponentLayer(); + + result.success(null); + break; + } case "circleLayer#add": { final String sourceId = call.argument("sourceId"); diff --git a/example/lib/layer.dart b/example/lib/layer.dart index 8d7b9d88..c3b0c9cb 100644 --- a/example/lib/layer.dart +++ b/example/lib/layer.dart @@ -181,6 +181,25 @@ class LayerState extends State { 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", @@ -314,6 +333,23 @@ final _fills = { ] } }, + { + "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, diff --git a/ios/Classes/LayerPropertyConverter.swift b/ios/Classes/LayerPropertyConverter.swift index a3d51b45..ea7c196f 100644 --- a/ios/Classes/LayerPropertyConverter.swift +++ b/ios/Classes/LayerPropertyConverter.swift @@ -242,6 +242,36 @@ class LayerPropertyConverter { } } + class func addFillExtrusionProperties(fillExtrusionLayer: MGLFillExtrusionStyleLayer, properties: [String: String]) { + for (propertyName, propertyValue) in properties { + let expression = interpretExpression(propertyName: propertyName, expression: propertyValue) + switch propertyName { + case "fill-extrusion-opacity": + fillExtrusionLayer.fillExtrusionOpacity = expression + case "fill-extrusion-color": + fillExtrusionLayer.fillExtrusionColor = expression + case "fill-extrusion-translate": + fillExtrusionLayer.fillExtrusionTranslation = expression + case "fill-extrusion-translate-anchor": + fillExtrusionLayer.fillExtrusionTranslationAnchor = expression + case "fill-extrusion-pattern": + fillExtrusionLayer.fillExtrusionPattern = expression + case "fill-extrusion-height": + fillExtrusionLayer.fillExtrusionHeight = expression + case "fill-extrusion-base": + fillExtrusionLayer.fillExtrusionBase = expression + case "fill-extrusion-vertical-gradient": + fillExtrusionLayer.fillExtrusionHasVerticalGradient = expression + case "visibility": + let trimmedPropertyValue = propertyValue.trimmingCharacters(in: .init(charactersIn: "\"")) + fillExtrusionLayer.isVisible = trimmedPropertyValue == "visible" + + default: + break + } + } + } + class func addRasterProperties(rasterLayer: MGLRasterStyleLayer, properties: [String: String]) { for (propertyName, propertyValue) in properties { let expression = interpretExpression(propertyName: propertyName, expression: propertyValue) diff --git a/ios/Classes/MapboxMapController.swift b/ios/Classes/MapboxMapController.swift index 53e13e36..a624306e 100644 --- a/ios/Classes/MapboxMapController.swift +++ b/ios/Classes/MapboxMapController.swift @@ -467,6 +467,34 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma case let .failure(error): result(error.flutterError) } + case "fillExtrusionLayer#add": + guard let arguments = methodCall.arguments as? [String: Any] else { return } + guard let sourceId = arguments["sourceId"] as? String else { return } + guard let layerId = arguments["layerId"] as? String else { return } + guard let properties = arguments["properties"] as? [String: String] else { return } + guard let enableInteraction = arguments["enableInteraction"] as? Bool else { return } + let belowLayerId = arguments["belowLayerId"] as? String + let sourceLayer = arguments["sourceLayer"] as? String + let minzoom = arguments["minzoom"] as? Double + let maxzoom = arguments["maxzoom"] as? Double + let filter = arguments["filter"] as? String + + let addResult = addFillExtrusionLayer( + sourceId: sourceId, + layerId: layerId, + belowLayerId: belowLayerId, + sourceLayerIdentifier: sourceLayer, + minimumZoomLevel: minzoom, + maximumZoomLevel: maxzoom, + filter: filter, + enableInteraction: enableInteraction, + properties: properties + ) + switch addResult { + case .success: result(nil) + case let .failure(error): result(error.flutterError) + } + case "circleLayer#add": guard let arguments = methodCall.arguments as? [String: Any] else { return } guard let sourceId = arguments["sourceId"] as? String else { return } @@ -1273,6 +1301,51 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma return .success(()) } + func addFillExtrusionLayer( + sourceId: String, + layerId: String, + belowLayerId: String?, + sourceLayerIdentifier: String?, + minimumZoomLevel: Double?, + maximumZoomLevel: Double?, + filter: String?, + enableInteraction: Bool, + properties: [String: String] + ) -> Result { + if let style = mapView.style { + if let source = style.source(withIdentifier: sourceId) { + let layer = MGLFillExtrusionStyleLayer(identifier: layerId, source: source) + LayerPropertyConverter.addFillExtrusionProperties( + fillExtrusionLayer: layer, + properties: properties + ) + if let sourceLayerIdentifier = sourceLayerIdentifier { + layer.sourceLayerIdentifier = sourceLayerIdentifier + } + if let minimumZoomLevel = minimumZoomLevel { + layer.minimumZoomLevel = Float(minimumZoomLevel) + } + if let maximumZoomLevel = maximumZoomLevel { + layer.maximumZoomLevel = Float(maximumZoomLevel) + } + if let filter = filter { + if case let .failure(error) = setFilter(layer, filter) { + return .failure(error) + } + } + if let id = belowLayerId, let belowLayer = style.layer(withIdentifier: id) { + style.insertLayer(layer, below: belowLayer) + } else { + style.addLayer(layer) + } + if enableInteraction { + interactiveFeatureLayerIds.insert(layerId) + } + } + } + return .success(()) + } + func addCircleLayer( sourceId: String, layerId: String, diff --git a/lib/src/controller.dart b/lib/src/controller.dart index 24873b64..c2c3fc90 100644 --- a/lib/src/controller.dart +++ b/lib/src/controller.dart @@ -490,6 +490,46 @@ class MaplibreMapController extends ChangeNotifier { ); } + /// Add a fill extrusion layer to the map with the given properties + /// + /// Consider using [addLayer] for an unified layer api. + /// + /// The returned [Future] completes after the change has been made on the + /// platform side. + /// + /// Setting [belowLayerId] adds the new layer below the given id. + /// If [enableInteraction] is set the layer is considered for touch or drag + /// events. [sourceLayer] is used to selected a specific source layer from + /// Vector source. + /// [minzoom] is the minimum (inclusive) zoom level at which the layer is + /// visible. + /// [maxzoom] is the maximum (exclusive) zoom level at which the layer is + /// visible. + /// [filter] determines which features should be rendered in the layer. + /// Filters are written as [expressions]. + /// + /// [expressions]: https://maplibre.org/maplibre-style-spec/expressions/ + Future addFillExtrusionLayer( + String sourceId, String layerId, FillExtrusionLayerProperties properties, + {String? belowLayerId, + String? sourceLayer, + double? minzoom, + double? maxzoom, + dynamic filter, + bool enableInteraction = true}) async { + await _maplibreGlPlatform.addFillExtrusionLayer( + sourceId, + layerId, + properties.toJson(), + belowLayerId: belowLayerId, + sourceLayer: sourceLayer, + minzoom: minzoom, + maxzoom: maxzoom, + filter: filter, + enableInteraction: enableInteraction, + ); + } + /// Add a circle layer to the map with the given properties /// /// Consider using [addLayer] for an unified layer api. @@ -1270,6 +1310,12 @@ class MaplibreMapController extends ChangeNotifier { minzoom: minzoom, maxzoom: maxzoom, filter: filter); + } else if (properties is FillExtrusionLayerProperties) { + addFillExtrusionLayer(sourceId, layerId, properties, + belowLayerId: belowLayerId, + sourceLayer: sourceLayer, + minzoom: minzoom, + maxzoom: maxzoom); } else if (properties is LineLayerProperties) { addLineLayer(sourceId, layerId, properties, belowLayerId: belowLayerId, diff --git a/lib/src/layer_properties.dart b/lib/src/layer_properties.dart index d9fff19a..9167af04 100644 --- a/lib/src/layer_properties.dart +++ b/lib/src/layer_properties.dart @@ -205,9 +205,9 @@ class SymbolLayerProperties implements LayerProperties { /// collisions. Recommended in layers that don't have enough padding in /// the vector tile to prevent collisions, or if it is a point symbol /// layer placed after a line symbol layer. When using a client that - /// supports global collision detection, like MapLibre GL JS, - /// enabling this property is not needed to prevent clipped - /// labels at tile boundaries. + /// supports global collision detection, like MapLibre GL JS, enabling + /// this property is not needed to prevent clipped labels at tile + /// boundaries. /// /// Type: boolean /// default: false @@ -1751,6 +1751,188 @@ class FillLayerProperties implements LayerProperties { } } +class FillExtrusionLayerProperties implements LayerProperties { + // Paint Properties + /// The opacity of the entire fill extrusion layer. This is rendered on a + /// per-layer, not per-feature, basis, and data-driven styling is not + /// available. + /// + /// Type: number + /// default: 1 + /// minimum: 0 + /// maximum: 1 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic fillExtrusionOpacity; + + /// The base color of the extruded fill. The extrusion's surfaces will be + /// shaded differently based on this color in combination with the root + /// `light` settings. If this color is specified as `rgba` with an alpha + /// component, the alpha component will be ignored; use + /// `fill-extrusion-opacity` to set layer opacity. + /// + /// Type: color + /// default: #000000 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic fillExtrusionColor; + + /// The geometry's offset. Values are [x, y] where negatives indicate left + /// and up (on the flat plane), respectively. + /// + /// Type: array + /// default: [0, 0] + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic fillExtrusionTranslate; + + /// Controls the frame of reference for `fill-extrusion-translate`. + /// + /// Type: enum + /// default: map + /// Options: + /// "map" + /// The fill extrusion is translated relative to the map. + /// "viewport" + /// The fill extrusion is translated relative to the viewport. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic fillExtrusionTranslateAnchor; + + /// Name of image in sprite to use for drawing images on extruded fills. + /// For seamless patterns, image width and height must be a factor of two + /// (2, 4, 8, ..., 512). Note that zoom-dependent expressions will be + /// evaluated only at integer zoom levels. + /// + /// Type: resolvedImage + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, macos, ios + final dynamic fillExtrusionPattern; + + /// The height with which to extrude this layer. + /// + /// Type: number + /// default: 0 + /// minimum: 0 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic fillExtrusionHeight; + + /// The height with which to extrude the base of this layer. Must be less + /// than or equal to `fill-extrusion-height`. + /// + /// Type: number + /// default: 0 + /// minimum: 0 + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + /// data-driven styling with js, android, ios, macos + final dynamic fillExtrusionBase; + + /// Whether to apply a vertical gradient to the sides of a fill-extrusion + /// layer. If true, sides will be shaded slightly darker farther down. + /// + /// Type: boolean + /// default: true + /// + /// Sdk Support: + /// basic functionality with js, ios, macos + final dynamic fillExtrusionVerticalGradient; + + // Layout Properties + /// Whether this layer is displayed. + /// + /// Type: enum + /// default: visible + /// Options: + /// "visible" + /// The layer is shown. + /// "none" + /// The layer is not shown. + /// + /// Sdk Support: + /// basic functionality with js, android, ios, macos + final dynamic visibility; + + const FillExtrusionLayerProperties({ + this.fillExtrusionOpacity, + this.fillExtrusionColor, + this.fillExtrusionTranslate, + this.fillExtrusionTranslateAnchor, + this.fillExtrusionPattern, + this.fillExtrusionHeight, + this.fillExtrusionBase, + this.fillExtrusionVerticalGradient, + this.visibility, + }); + + FillExtrusionLayerProperties copyWith(FillExtrusionLayerProperties changes) { + return FillExtrusionLayerProperties( + fillExtrusionOpacity: + changes.fillExtrusionOpacity ?? fillExtrusionOpacity, + fillExtrusionColor: changes.fillExtrusionColor ?? fillExtrusionColor, + fillExtrusionTranslate: + changes.fillExtrusionTranslate ?? fillExtrusionTranslate, + fillExtrusionTranslateAnchor: + changes.fillExtrusionTranslateAnchor ?? fillExtrusionTranslateAnchor, + fillExtrusionPattern: + changes.fillExtrusionPattern ?? fillExtrusionPattern, + fillExtrusionHeight: changes.fillExtrusionHeight ?? fillExtrusionHeight, + fillExtrusionBase: changes.fillExtrusionBase ?? fillExtrusionBase, + fillExtrusionVerticalGradient: changes.fillExtrusionVerticalGradient ?? + fillExtrusionVerticalGradient, + visibility: changes.visibility ?? visibility, + ); + } + + Map toJson() { + final Map json = {}; + + void addIfPresent(String fieldName, dynamic value) { + if (value != null) { + json[fieldName] = value; + } + } + + addIfPresent('fill-extrusion-opacity', fillExtrusionOpacity); + addIfPresent('fill-extrusion-color', fillExtrusionColor); + addIfPresent('fill-extrusion-translate', fillExtrusionTranslate); + addIfPresent( + 'fill-extrusion-translate-anchor', fillExtrusionTranslateAnchor); + addIfPresent('fill-extrusion-pattern', fillExtrusionPattern); + addIfPresent('fill-extrusion-height', fillExtrusionHeight); + addIfPresent('fill-extrusion-base', fillExtrusionBase); + addIfPresent( + 'fill-extrusion-vertical-gradient', fillExtrusionVerticalGradient); + addIfPresent('visibility', visibility); + return json; + } + + factory FillExtrusionLayerProperties.fromJson(Map json) { + return FillExtrusionLayerProperties( + fillExtrusionOpacity: json['fill-extrusion-opacity'], + fillExtrusionColor: json['fill-extrusion-color'], + fillExtrusionTranslate: json['fill-extrusion-translate'], + fillExtrusionTranslateAnchor: json['fill-extrusion-translate-anchor'], + fillExtrusionPattern: json['fill-extrusion-pattern'], + fillExtrusionHeight: json['fill-extrusion-height'], + fillExtrusionBase: json['fill-extrusion-base'], + fillExtrusionVerticalGradient: json['fill-extrusion-vertical-gradient'], + visibility: json['visibility'], + ); + } +} + class RasterLayerProperties implements LayerProperties { // Paint Properties /// The opacity at which the image will be drawn. diff --git a/maplibre_gl_platform_interface/lib/src/maplibre_gl_platform_interface.dart b/maplibre_gl_platform_interface/lib/src/maplibre_gl_platform_interface.dart index 7138b9e4..9b8dc1bf 100644 --- a/maplibre_gl_platform_interface/lib/src/maplibre_gl_platform_interface.dart +++ b/maplibre_gl_platform_interface/lib/src/maplibre_gl_platform_interface.dart @@ -171,6 +171,15 @@ abstract class MapLibreGlPlatform { dynamic filter, required bool enableInteraction}); + Future addFillExtrusionLayer( + String sourceId, String layerId, Map properties, + {String? belowLayerId, + String? sourceLayer, + double? minzoom, + double? maxzoom, + dynamic filter, + required bool enableInteraction}); + Future addRasterLayer( String sourceId, String layerId, Map properties, {String? belowLayerId, 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 0a383584..db3f56bb 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 @@ -697,6 +697,29 @@ class MethodChannelMaplibreGl extends MapLibreGlPlatform { }); } + @override + Future addFillExtrusionLayer( + String sourceId, String layerId, Map properties, + {String? belowLayerId, + String? sourceLayer, + double? minzoom, + double? maxzoom, + dynamic filter, + required bool enableInteraction}) async { + await _channel.invokeMethod('fillExtrusionLayer#add', { + 'sourceId': sourceId, + 'layerId': layerId, + 'belowLayerId': belowLayerId, + 'sourceLayer': sourceLayer, + 'minzoom': minzoom, + 'maxzoom': maxzoom, + 'filter': jsonEncode(filter), + 'enableInteraction': enableInteraction, + 'properties': properties + .map((key, value) => MapEntry(key, jsonEncode(value))) + }); + } + @override void dispose() { super.dispose(); diff --git a/maplibre_gl_platform_interface/lib/src/source_properties.dart b/maplibre_gl_platform_interface/lib/src/source_properties.dart index 5e2c282c..0d42c1fb 100644 --- a/maplibre_gl_platform_interface/lib/src/source_properties.dart +++ b/maplibre_gl_platform_interface/lib/src/source_properties.dart @@ -8,7 +8,8 @@ abstract class SourceProperties { } class VectorSourceProperties implements SourceProperties { - /// A URL to a TileJSON resource. Supported protocols are `http:` and `https:` + /// A URL to a TileJSON resource. Supported protocols are `http:` and + /// `https:` /// /// Type: string final String? url; @@ -21,8 +22,8 @@ class VectorSourceProperties implements SourceProperties { /// An array containing the longitude and latitude of the southwest and /// northeast corners of the source's bounding box in the following order: /// `[sw.lng, sw.lat, ne.lng, ne.lat]`. When this property is included in - /// a source, no tiles outside of the given bounds are requested by MapLibre - /// GL. + /// a source, no tiles outside of the given bounds are requested by + /// MapLibre. /// /// Type: array /// default: [-180, -85.051129, 180, 85.051129] @@ -138,7 +139,8 @@ class VectorSourceProperties implements SourceProperties { } class RasterSourceProperties implements SourceProperties { - /// A URL to a TileJSON resource. Supported protocols are `http:` and `https:` + /// A URL to a TileJSON resource. Supported protocols are `http:` and + /// `https:`. /// /// Type: string final String? url; @@ -151,7 +153,8 @@ class RasterSourceProperties implements SourceProperties { /// An array containing the longitude and latitude of the southwest and /// northeast corners of the source's bounding box in the following order: /// `[sw.lng, sw.lat, ne.lng, ne.lat]`. When this property is included in - /// a source, no tiles outside of the given bounds are requested by MapLibre. + /// a source, no tiles outside of the given bounds are requested by + /// MapLibre. /// /// Type: array /// default: [-180, -85.051129, 180, 85.051129] @@ -266,7 +269,8 @@ class RasterSourceProperties implements SourceProperties { } class RasterDemSourceProperties implements SourceProperties { - /// A URL to a TileJSON resource. Supported protocols are `http:` and `https:`. + /// A URL to a TileJSON resource. Supported protocols are `http:` and + /// `https:`. /// /// Type: string final String? url; @@ -279,7 +283,8 @@ class RasterDemSourceProperties implements SourceProperties { /// An array containing the longitude and latitude of the southwest and /// northeast corners of the source's bounding box in the following order: /// `[sw.lng, sw.lat, ne.lng, ne.lat]`. When this property is included in - /// a source, no tiles outside of the given bounds are requested by MapLibre. + /// a source, no tiles outside of the given bounds are requested by + /// MapLibre. /// /// Type: array /// default: [-180, -85.051129, 180, 85.051129] 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 9ec2b7a1..657f84a2 100644 --- a/maplibre_gl_web/lib/src/maplibre_web_gl_platform.dart +++ b/maplibre_gl_web/lib/src/maplibre_web_gl_platform.dart @@ -827,6 +827,24 @@ class MaplibreMapController extends MapLibreGlPlatform {'padding': padding}); } + @override + Future addFillExtrusionLayer( + String sourceId, String layerId, Map properties, + {String? belowLayerId, + String? sourceLayer, + double? minzoom, + double? maxzoom, + dynamic filter, + required bool enableInteraction}) async { + return _addLayer(sourceId, layerId, properties, "fill-extrusion", + belowLayerId: belowLayerId, + sourceLayer: sourceLayer, + minzoom: minzoom, + maxzoom: maxzoom, + filter: filter, + enableInteraction: enableInteraction); + } + @override Future addCircleLayer( String sourceId, String layerId, Map properties, diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 00000000..3c375db9 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1 @@ +To run the script, run `dart run .\scripts\lib\generate.dart ` from the root of the project. Afterwards run `dart format .` to format the generated dart code. \ No newline at end of file diff --git a/scripts/input/style.json b/scripts/input/style.json index f04cf74c..38891916 100644 --- a/scripts/input/style.json +++ b/scripts/input/style.json @@ -131,7 +131,7 @@ }, "url":{ "type":"string", - "doc":"A URL to a TileJSON resource. Supported protocols are `http:`, `https:`, and `mapbox://`." + "doc":"A URL to a TileJSON resource. Supported protocols are `http:` and `https:`" }, "tiles":{ "type":"array", @@ -148,7 +148,7 @@ 180, 85.051129 ], - "doc":"An array containing the longitude and latitude of the southwest and northeast corners of the source's bounding box in the following order: `[sw.lng, sw.lat, ne.lng, ne.lat]`. When this property is included in a source, no tiles outside of the given bounds are requested by Mapbox GL." + "doc":"An array containing the longitude and latitude of the southwest and northeast corners of the source's bounding box in the following order: `[sw.lng, sw.lat, ne.lng, ne.lat]`. When this property is included in a source, no tiles outside of the given bounds are requested by MapLibre." }, "scheme":{ "type":"enum", @@ -199,7 +199,7 @@ }, "url":{ "type":"string", - "doc":"A URL to a TileJSON resource. Supported protocols are `http:`, `https:`, and `mapbox://`." + "doc":"A URL to a TileJSON resource. Supported protocols are `http:` and `https:`." }, "tiles":{ "type":"array", @@ -216,7 +216,7 @@ 180, 85.051129 ], - "doc":"An array containing the longitude and latitude of the southwest and northeast corners of the source's bounding box in the following order: `[sw.lng, sw.lat, ne.lng, ne.lat]`. When this property is included in a source, no tiles outside of the given bounds are requested by Mapbox GL." + "doc":"An array containing the longitude and latitude of the southwest and northeast corners of the source's bounding box in the following order: `[sw.lng, sw.lat, ne.lng, ne.lat]`. When this property is included in a source, no tiles outside of the given bounds are requested by MapLibre." }, "minzoom":{ "type":"number", @@ -269,7 +269,7 @@ }, "url":{ "type":"string", - "doc":"A URL to a TileJSON resource. Supported protocols are `http:`, `https:`, and `mapbox://`." + "doc":"A URL to a TileJSON resource. Supported protocols are `http:` and `https:`." }, "tiles":{ "type":"array", @@ -286,7 +286,7 @@ 180, 85.051129 ], - "doc":"An array containing the longitude and latitude of the southwest and northeast corners of the source's bounding box in the following order: `[sw.lng, sw.lat, ne.lng, ne.lat]`. When this property is included in a source, no tiles outside of the given bounds are requested by Mapbox GL." + "doc":"An array containing the longitude and latitude of the southwest and northeast corners of the source's bounding box in the following order: `[sw.lng, sw.lat, ne.lng, ne.lat]`. When this property is included in a source, no tiles outside of the given bounds are requested by MapLibre." }, "minzoom":{ "type":"number", @@ -365,7 +365,7 @@ "cluster":{ "type":"boolean", "default":false, - "doc":"If the data is a collection of point features, setting this to true clusters the points by radius into groups. Cluster groups become new `Point` features in the source with additional properties:\n * `cluster` Is `true` if the point is a cluster \n * `cluster_id` A unqiue id for the cluster to be used in conjunction with the [cluster inspection methods](https://www.mapbox.com/mapbox-gl-js/api/#geojsonsource#getclusterexpansionzoom)\n * `point_count` Number of original points grouped into this cluster\n * `point_count_abbreviated` An abbreviated point count" + "doc":"If the data is a collection of point features, setting this to true clusters the points by radius into groups. Cluster groups become new `Point` features in the source with additional properties:\n * `cluster` Is `true` if the point is a cluster \n * `cluster_id` A unqiue id for the cluster to be used in conjunction with the [cluster inspection methods](https://maplibre.org/maplibre-gl-js/docs/API/classes/maplibregl.GeoJSONSource/#getclusterexpansionzoom)\n * `point_count` Number of original points grouped into this cluster\n * `point_count_abbreviated` An abbreviated point count" }, "clusterRadius":{ "type":"number", @@ -1006,7 +1006,7 @@ "symbol-avoid-edges":{ "type":"boolean", "default":false, - "doc":"If true, the symbols will not cross tile edges to avoid mutual collisions. Recommended in layers that don't have enough padding in the vector tile to prevent collisions, or if it is a point symbol layer placed after a line symbol layer. When using a client that supports global collision detection, like Mapbox GL JS version 0.42.0 or greater, enabling this property is not needed to prevent clipped labels at tile boundaries.", + "doc":"If true, the symbols will not cross tile edges to avoid mutual collisions. Recommended in layers that don't have enough padding in the vector tile to prevent collisions, or if it is a point symbol layer placed after a line symbol layer. When using a client that supports global collision detection, like MapLibre GL JS, enabling this property is not needed to prevent clipped labels at tile boundaries.", "sdk-support":{ "basic functionality":{ "js":"0.10.0", @@ -3455,7 +3455,7 @@ } }, "is-supported-script":{ - "doc":"Returns `true` if the input string is expected to render legibly. Returns `false` if the input string contains sections that cannot be rendered without potential loss of meaning (e.g. Indic scripts that require complex text shaping, or right-to-left scripts if the the `mapbox-gl-rtl-text` plugin is not in use in Mapbox GL JS).", + "doc":"Returns `true` if the input string is expected to render legibly. Returns `false` if the input string contains sections that cannot be rendered without potential loss of meaning (e.g. Indic scripts that require complex text shaping, or right-to-left scripts if the the `mapbox-gl-rtl-text` plugin is not in use in MapLibre GL JS).", "group":"String", "sdk-support":{ "basic functionality":{ diff --git a/scripts/lib/conversions.dart b/scripts/lib/conversions.dart index 1cd5a411..016f8f86 100644 --- a/scripts/lib/conversions.dart +++ b/scripts/lib/conversions.dart @@ -35,6 +35,9 @@ const renamedIosProperties = { "visibility": "isVisible", "rasterBrightnessMin": "minimumRasterBrightness", "rasterBrightnessMax": "maximumRasterBrightness", + "fillExtrusionTranslate": "fillExtrusionTranslation", + "fillExtrusionTranslateAnchor": "fillExtrusionTranslationAnchor", + "fillExtrusionVerticalGradient": "fillExtrusionHasVerticalGradient", }; const dartTypeMappingTable = { diff --git a/scripts/lib/generate.dart b/scripts/lib/generate.dart index d72d41a8..46d57988 100644 --- a/scripts/lib/generate.dart +++ b/scripts/lib/generate.dart @@ -15,6 +15,7 @@ main() async { "circle", "line", "fill", + "fill-extrusion", "raster", "hillshade" ]; @@ -33,6 +34,7 @@ main() async { { "type": type, "typePascal": ReCase(type).pascalCase, + "typeCamel": ReCase(type).camelCase, "paint_properties": buildStyleProperties(styleJson, "paint_$type"), "layout_properties": buildStyleProperties(styleJson, "layout_$type"), }, diff --git a/scripts/templates/LayerPropertyConverter.swift.template b/scripts/templates/LayerPropertyConverter.swift.template index 8c173569..59d40d80 100644 --- a/scripts/templates/LayerPropertyConverter.swift.template +++ b/scripts/templates/LayerPropertyConverter.swift.template @@ -5,32 +5,32 @@ import Mapbox class LayerPropertyConverter { {{#layerTypes}} - class func add{{typePascal}}Properties({{type}}Layer: MGL{{typePascal}}StyleLayer, properties: [String: String]) { + class func add{{typePascal}}Properties({{typeCamel}}Layer: MGL{{typePascal}}StyleLayer, properties: [String: String]) { for (propertyName, propertyValue) in properties { let expression = interpretExpression(propertyName: propertyName, expression: propertyValue) switch propertyName { {{#paint_properties}} case "{{{value}}}": {{#isIosAsCamelCase}} - {{type}}Layer.{{iosAsCamelCase}} = expression + {{typeCamel}}Layer.{{iosAsCamelCase}} = expression {{/isIosAsCamelCase}} {{^isIosAsCamelCase}} - {{type}}Layer.{{valueAsCamelCase}} = expression + {{typeCamel}}Layer.{{valueAsCamelCase}} = expression {{/isIosAsCamelCase}} {{/paint_properties}} {{#layout_properties}} case "{{{value}}}": {{^isVisibilityProperty}} {{#isIosAsCamelCase}} - {{type}}Layer.{{iosAsCamelCase}} = expression + {{typeCamel}}Layer.{{iosAsCamelCase}} = expression {{/isIosAsCamelCase}} {{^isIosAsCamelCase}} - {{type}}Layer.{{valueAsCamelCase}} = expression + {{typeCamel}}Layer.{{valueAsCamelCase}} = expression {{/isIosAsCamelCase}} {{/isVisibilityProperty}} {{#isVisibilityProperty}} let trimmedPropertyValue = propertyValue.trimmingCharacters(in: .init(charactersIn: "\"")) - {{type}}Layer.{{iosAsCamelCase}} = trimmedPropertyValue == "visible" + {{typeCamel}}Layer.{{iosAsCamelCase}} = trimmedPropertyValue == "visible" {{/isVisibilityProperty}} {{/layout_properties}} From f234dd5f7a928446a73d4b020550503587eb71c0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Dec 2023 09:23:31 +0100 Subject: [PATCH 4/5] Bump actions/upload-artifact from 3 to 4 (#361) 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 3 to 4.
Release notes

Sourced from actions/upload-artifact's releases.

v4.0.0

What's Changed

The release of upload-artifact@v4 and download-artifact@v4 are major changes to the backend architecture of Artifacts. They have numerous performance and behavioral improvements.

For more information, see the @​actions/artifact documentation.

New Contributors

Full Changelog: https://github.com/actions/upload-artifact/compare/v3...v4.0.0

v3.1.3

What's Changed

Full Changelog: https://github.com/actions/upload-artifact/compare/v3...v3.1.3

v3.1.2

  • Update all @actions/* NPM packages to their latest versions- #374
  • Update all dev dependencies to their most recent versions - #375

v3.1.1

  • Update actions/core package to latest version to remove set-output deprecation warning #351

v3.1.0

What's Changed

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/upload-artifact&package-manager=github_actions&previous-version=3&new-version=4)](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> --- .github/workflows/flutter_beta.yml | 2 +- .github/workflows/flutter_ci.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/flutter_beta.yml b/.github/workflows/flutter_beta.yml index eeff2c7d..2903b130 100644 --- a/.github/workflows/flutter_beta.yml +++ b/.github/workflows/flutter_beta.yml @@ -78,7 +78,7 @@ jobs: - name: Build iOS package run: flutter build ios --simulator - name: Upload Runner.app as artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Runner.app path: example/build/ios/iphonesimulator diff --git a/.github/workflows/flutter_ci.yml b/.github/workflows/flutter_ci.yml index 3c0b2e7d..9cdab223 100644 --- a/.github/workflows/flutter_ci.yml +++ b/.github/workflows/flutter_ci.yml @@ -85,7 +85,7 @@ jobs: - name: Build iOS package run: flutter build ios --simulator - name: Upload Runner.app as artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Runner.app path: example/build/ios/iphonesimulator From 1fd40013432757c850cdae319f00322bd1343d25 Mon Sep 17 00:00:00 2001 From: lunaticcoding Date: Fri, 15 Dec 2023 10:50:45 +0100 Subject: [PATCH 5/5] make feature tap detection work for features with a null id on ios (same as android) (#357) Feature Id nullable on android but not on iOS. On android the feature id can be null which works fine in our app but on iOS clicking on points of interest the reaction is the same as clicking elsewhere on the map. In the android code ![Screenshot 2023-12-12 at 13 42 00](https://github.com/maplibre/flutter-maplibre-gl/assets/26603883/a532aa76-9ec4-4bf4-8da3-b673989a85e6) null is a valid value. I adapted the iOS code to allow for the same behaviour. Co-authored-by: m0nac0 <58807793+m0nac0@users.noreply.github.com> --- ios/Classes/MapboxMapController.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ios/Classes/MapboxMapController.swift b/ios/Classes/MapboxMapController.swift index a624306e..d422ef57 100644 --- a/ios/Classes/MapboxMapController.swift +++ b/ios/Classes/MapboxMapController.swift @@ -989,9 +989,9 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma let point = sender.location(in: mapView) let coordinate = mapView.convert(point, toCoordinateFrom: mapView) - if let feature = firstFeatureOnLayers(at: point), let id = feature.identifier { + if let feature = firstFeatureOnLayers(at: point) { channel?.invokeMethod("feature#onTap", arguments: [ - "id": id, + "id": feature.identifier, "x": point.x, "y": point.y, "lng": coordinate.longitude,