Skip to content

Commit

Permalink
feat: heatmap layer (#365)
Browse files Browse the repository at this point in the history
### Changes made

- Adds support for heatmap layer

<img
src='https://github.com/maplibre/flutter-maplibre-gl/assets/139912620/0d79b768-6cbc-4763-a470-fe40190cdb63'
alt='Heat-map-example' width=200 />

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: m0nac0 <58807793+m0nac0@users.noreply.github.com>
  • Loading branch information
3 people committed Jan 15, 2024
1 parent 1e66cb7 commit acb428a
Show file tree
Hide file tree
Showing 12 changed files with 449 additions and 8 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ Include the following JavaScript and CSS files in the `<head>` of the `web/index
| 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: |
Heatmap Layer | :white_check_mark: | :white_check_mark: | :white_check_mark: |


## Map Styles
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -502,4 +502,39 @@ static PropertyValue[] interpretHillshadeLayerProperties(Object o) {
return properties.toArray(new PropertyValue[properties.size()]);
}

static PropertyValue[] interpretHeatmapLayerProperties(Object o) {
final Map<String, String> data = (Map<String, String>) toMap(o);
final List<PropertyValue> properties = new LinkedList();
final JsonParser parser = new JsonParser();

for (Map.Entry<String, String> entry : data.entrySet()) {
final JsonElement jsonElement = parser.parse(entry.getValue());
Expression expression = Expression.Converter.convert(jsonElement);
switch (entry.getKey()) {
case "heatmap-radius":
properties.add(PropertyFactory.heatmapRadius(expression));
break;
case "heatmap-weight":
properties.add(PropertyFactory.heatmapWeight(expression));
break;
case "heatmap-intensity":
properties.add(PropertyFactory.heatmapIntensity(expression));
break;
case "heatmap-color":
properties.add(PropertyFactory.heatmapColor(expression));
break;
case "heatmap-opacity":
properties.add(PropertyFactory.heatmapOpacity(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()]);
}

}
45 changes: 45 additions & 0 deletions android/src/main/java/com/mapbox/mapboxgl/MapboxMapController.java
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,29 @@ private void addHillshadeLayer(
}
}

private void addHeatmapLayer(
String layerName,
String sourceName,
Float minZoom,
Float maxZoom,
String belowLayerId,
PropertyValue[] properties,
Expression filter) {
HeatmapLayer layer = new HeatmapLayer(layerName, sourceName);
layer.setProperties(properties);
if (minZoom != null) {
layer.setMinZoom(minZoom);
}
if (maxZoom != null) {
layer.setMaxZoom(maxZoom);
}
if (belowLayerId != null) {
style.addLayerBelow(layer, belowLayerId);
} else {
style.addLayer(layer);
}
}

private Feature firstFeatureOnLayers(RectF in) {
if (style != null) {
final List<Layer> layers = style.getLayers();
Expand Down Expand Up @@ -1173,6 +1196,28 @@ public void onError(@NonNull String message) {
null);
updateLocationComponentLayer();

result.success(null);
break;
}
case "heatmapLayer#add":
{
final String sourceId = call.argument("sourceId");
final String layerId = call.argument("layerId");
final String belowLayerId = call.argument("belowLayerId");
final Double minzoom = call.argument("minzoom");
final Double maxzoom = call.argument("maxzoom");
final PropertyValue[] properties =
LayerPropertyConverter.interpretHeatmapLayerProperties(call.argument("properties"));
addHeatmapLayer(
layerId,
sourceId,
minzoom != null ? minzoom.floatValue() : null,
maxzoom != null ? maxzoom.floatValue() : null,
belowLayerId,
properties,
null);
updateLocationComponentLayer();

result.success(null);
break;
}
Expand Down
98 changes: 91 additions & 7 deletions example/lib/sources.dart
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,83 @@ class FullMapState extends State<FullMap> {
);
}

static Future<void> addHeatMap(MaplibreMapController controller) async {
await controller.addSource(
'earthquakes-heatmap-source',
const GeojsonSourceProperties(
data:
'https://maplibre.org/maplibre-gl-js/docs/assets/earthquakes.geojson'));

await controller.addLayer(
'earthquakes-heatmap-source',
'earthquakes-heatmap-layer',
const HeatmapLayerProperties(
// Increase the heatmap weight based on frequency and property magnitude
heatmapWeight: [
Expressions.interpolate,
['linear'],
[Expressions.get, 'mag'],
0,
0,
6,
1,
],
// Increase the heatmap color weight weight by zoom level
// heatmap-intensity is a multiplier on top of heatmap-weight
heatmapIntensity: [
Expressions.interpolate,
['linear'],
[Expressions.zoom],
0,
1,
9,
3
],
// Color ramp for heatmap. Domain is 0 (low) to 1 (high).
// Begin color ramp at 0-stop with a 0-transparancy color
// to create a blur-like effect.
heatmapColor: [
Expressions.interpolate,
['linear'],
['heatmap-density'],
0,
'rgba(33.0, 102.0, 172.0, 0.0)',
0.2,
'rgb(103.0, 169.0, 207.0)',
0.4,
'rgb(209.0, 229.0, 240.0)',
0.6,
'rgb(253.0, 219.0, 119.0)',
0.8,
'rgb(239.0, 138.0, 98.0)',
1,
'rgb(178.0, 24.0, 43.0)',
],
// Adjust the heatmap radius by zoom level
heatmapRadius: [
Expressions.interpolate,
['linear'],
[Expressions.zoom],
0,
2,
9,
20,
],
// Transition from heatmap to circle layer by zoom level
heatmapOpacity: [
Expressions.interpolate,
['linear'],
[Expressions.zoom],
7,
1,
9,
0
],
),
maxzoom: 9,
);
}

static Future<void> addDem(MaplibreMapController controller) async {
// TODO: adapt example?
// await controller.addSource(
Expand All @@ -174,14 +251,14 @@ class FullMapState extends State<FullMap> {
// );
}

static const _stylesAndLoaders = [
StyleInfo(
final _stylesAndLoaders = [
const StyleInfo(
name: "Vector",
baseStyle: MaplibreStyles.DEMO,
addDetails: addVector,
position: CameraPosition(target: LatLng(33.3832, -118.4333), zoom: 6),
),
StyleInfo(
const StyleInfo(
name: "Default style",
// Using the raw github file version of MaplibreStyles.DEMO here, because we need to
// specify a different baseStyle for consecutive elements in this list,
Expand All @@ -191,29 +268,36 @@ class FullMapState extends State<FullMap> {
addDetails: addDem,
position: CameraPosition(target: LatLng(33.5, -118.1), zoom: 8),
),
StyleInfo(
const StyleInfo(
name: "Geojson cluster",
baseStyle: MaplibreStyles.DEMO,
addDetails: addGeojsonCluster,
position: CameraPosition(target: LatLng(33.5, -118.1), zoom: 5),
),
StyleInfo(
const StyleInfo(
name: "Raster",
baseStyle:
"https://raw.githubusercontent.com/maplibre/demotiles/gh-pages/style.json",
addDetails: addRaster,
position: CameraPosition(target: LatLng(40, -100), zoom: 3),
),
StyleInfo(
const StyleInfo(
name: "Image",
baseStyle:
"https://raw.githubusercontent.com/maplibre/demotiles/gh-pages/style.json?",
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",
addDetails: addHeatMap,
position: CameraPosition(target: LatLng(33.5, -118.1), zoom: 2),
),
//video only supported on web
if (kIsWeb)
StyleInfo(
const StyleInfo(
name: "Video",
baseStyle:
"https://raw.githubusercontent.com/maplibre/demotiles/gh-pages/style.json",
Expand Down
24 changes: 24 additions & 0 deletions ios/Classes/LayerPropertyConverter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,30 @@ class LayerPropertyConverter {
}
}

class func addHeatmapProperties(heatmapLayer: MGLHeatmapStyleLayer, properties: [String: String]) {
for (propertyName, propertyValue) in properties {
let expression = interpretExpression(propertyName: propertyName, expression: propertyValue)
switch propertyName {
case "heatmap-radius":
heatmapLayer.heatmapRadius = expression
case "heatmap-weight":
heatmapLayer.heatmapWeight = expression
case "heatmap-intensity":
heatmapLayer.heatmapIntensity = expression
case "heatmap-color":
heatmapLayer.heatmapColor = expression
case "heatmap-opacity":
heatmapLayer.heatmapOpacity = expression
case "visibility":
let trimmedPropertyValue = propertyValue.trimmingCharacters(in: .init(charactersIn: "\""))
heatmapLayer.isVisible = trimmedPropertyValue == "visible"

default:
break
}
}
}

private class func interpretExpression(propertyName: String, expression: String) -> NSExpression? {
let isColor = propertyName.contains("color");

Expand Down
48 changes: 48 additions & 0 deletions ios/Classes/MapboxMapController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,24 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma
properties: properties
)
result(nil)

case "heatmapLayer#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 }
let belowLayerId = arguments["belowLayerId"] as? String
let minzoom = arguments["minzoom"] as? Double
let maxzoom = arguments["maxzoom"] as? Double
addHeatmapLayer(
sourceId: sourceId,
layerId: layerId,
belowLayerId: belowLayerId,
minimumZoomLevel: minzoom,
maximumZoomLevel: maxzoom,
properties: properties
)
result(nil)

case "rasterLayer#add":
guard let arguments = methodCall.arguments as? [String: Any] else { return }
Expand Down Expand Up @@ -1444,6 +1462,36 @@ class MapboxMapController: NSObject, FlutterPlatformView, MGLMapViewDelegate, Ma
}
}

func addHeatmapLayer(
sourceId: String,
layerId: String,
belowLayerId: String?,
minimumZoomLevel: Double?,
maximumZoomLevel: Double?,
properties: [String: String]
) {
if let style = mapView.style {
if let source = style.source(withIdentifier: sourceId) {
let layer = MGLHeatmapStyleLayer(identifier: layerId, source: source)
LayerPropertyConverter.addHeatmapProperties(
heatmapLayer: layer,
properties: properties
)
if let minimumZoomLevel = minimumZoomLevel {
layer.minimumZoomLevel = Float(minimumZoomLevel)
}
if let maximumZoomLevel = maximumZoomLevel {
layer.maximumZoomLevel = Float(maximumZoomLevel)
}
if let id = belowLayerId, let belowLayer = style.layer(withIdentifier: id) {
style.insertLayer(layer, below: belowLayer)
} else {
style.addLayer(layer)
}
}
}
}

func addRasterLayer(
sourceId: String,
layerId: String,
Expand Down
37 changes: 37 additions & 0 deletions lib/src/controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,37 @@ class MaplibreMapController extends ChangeNotifier {
);
}

/// Add a heatmap 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.
/// [sourceLayer] is used to selected a specific source layer from
/// Raster 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.
Future<void> addHeatmapLayer(
String sourceId, String layerId, HeatmapLayerProperties properties,
{String? belowLayerId,
String? sourceLayer,
double? minzoom,
double? maxzoom}) async {
await _maplibreGlPlatform.addHeatmapLayer(
sourceId,
layerId,
properties.toJson(),
belowLayerId: belowLayerId,
sourceLayer: sourceLayer,
minzoom: minzoom,
maxzoom: maxzoom,
);
}

/// Updates user location tracking mode.
///
/// The returned [Future] completes after the change has been made on the
Expand Down Expand Up @@ -1362,6 +1393,12 @@ class MaplibreMapController extends ChangeNotifier {
sourceLayer: sourceLayer,
minzoom: minzoom,
maxzoom: maxzoom);
} else if (properties is HeatmapLayerProperties) {
addHeatmapLayer(sourceId, layerId, properties,
belowLayerId: belowLayerId,
sourceLayer: sourceLayer,
minzoom: minzoom,
maxzoom: maxzoom);
} else {
throw UnimplementedError("Unknown layer type $properties");
}
Expand Down

0 comments on commit acb428a

Please sign in to comment.