├── analysis_options.yaml ├── CHANGELOG.md ├── lib ├── flutter_map_geojson2.dart └── geojson2 │ ├── geojson_provider.dart │ ├── default_features.dart │ └── geojson_layer.dart ├── .metadata ├── pubspec.yaml ├── .gitignore ├── LICENSE ├── test ├── app.dart └── geojson_test.dart └── README.md /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | # Additional information about this file can be found at 4 | # https://dart.dev/guides/language/analysis-options 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.2 2 | 3 | * Fixed multi-geometries parsing, thanks @IguJl15. 4 | 5 | ## 1.0.1 6 | 7 | * Checking for `mounted` before calling `setState()`. 8 | * Fixed integer widths and opacities. 9 | 10 | ## 1.0.0 11 | 12 | * Initial version 13 | -------------------------------------------------------------------------------- /lib/flutter_map_geojson2.dart: -------------------------------------------------------------------------------- 1 | library; 2 | 3 | export 'package:flutter_map_geojson2/geojson2/geojson_layer.dart'; 4 | export 'package:flutter_map_geojson2/geojson2/geojson_provider.dart'; 5 | export 'package:flutter_map_geojson2/geojson2/default_features.dart'; 6 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "17025dd88227cd9532c33fa78f5250d548d87e9a" 8 | channel: "stable" 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_map_geojson2 2 | description: "GeoJSON layer for flutter_map" 3 | version: 1.0.2 4 | homepage: https://github.com/zverik/flutter_map_geojson2 5 | 6 | environment: 7 | sdk: ^3.6.0 8 | flutter: ">=3.27.0" 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | flutter_map: ">=7.0.0 <9.0.0" 14 | latlong2: ^0.9.0 15 | material_color_names: ^1.0.0 16 | 17 | dev_dependencies: 18 | flutter_test: 19 | sdk: flutter 20 | flutter_lints: ^5.0.0 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. 26 | /pubspec.lock 27 | **/doc/api/ 28 | .dart_tool/ 29 | .flutter-plugins 30 | .flutter-plugins-dependencies 31 | build/ 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2025, Ilya Zverev 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose 4 | with or without fee is hereby granted, provided that the above copyright notice 5 | and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 8 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 9 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 10 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 11 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 12 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 13 | THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /test/app.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_map/flutter_map.dart'; 3 | import 'package:flutter_map_geojson2/flutter_map_geojson2.dart'; 4 | import 'package:latlong2/latlong.dart'; 5 | 6 | class JsonTestApp extends StatelessWidget { 7 | final GeoJsonLayer layer; 8 | final LatLng? center; 9 | 10 | const JsonTestApp({super.key, required this.layer, this.center}); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return MaterialApp( 15 | home: Scaffold( 16 | body: FlutterMap( 17 | options: MapOptions( 18 | initialCenter: center ?? LatLng(59.4, 24.7), 19 | initialZoom: 12, 20 | ), 21 | children: [layer], 22 | ), 23 | ), 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/geojson_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_map/flutter_map.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:flutter_map_geojson2/flutter_map_geojson2.dart'; 5 | import 'app.dart'; 6 | 7 | void main() { 8 | testWidgets('Empty layer works', (tester) async { 9 | await tester.pumpWidget(JsonTestApp( 10 | layer: GeoJsonLayer( 11 | data: MemoryGeoJson({}), 12 | ), 13 | )); 14 | expect(find.byType(GeoJsonLayer), findsOneWidget); 15 | expect(find.byType(MarkerLayer), findsNothing); 16 | expect(find.byType(PolylineLayer), findsNothing); 17 | expect(find.byType(PolygonLayer), findsNothing); 18 | }); 19 | 20 | testWidgets('Creates a linestring from a single feature', (tester) async { 21 | await tester.pumpWidget(JsonTestApp( 22 | layer: GeoJsonLayer.memory({ 23 | 'type': 'Feature', 24 | 'geometry': { 25 | 'type': 'LineString', 26 | 'coordinates': [ 27 | [24.7, 59.4], 28 | [24.8, 59.401] 29 | ], 30 | }, 31 | 'properties': { 32 | 'stroke': '#22e', 33 | 'stroke-width': 11, 34 | }, 35 | }), 36 | ), 37 | ); 38 | await tester.pumpAndSettle(Duration(milliseconds: 100)); 39 | expect(find.byType(GeoJsonLayer), findsOneWidget); 40 | expect(find.byType(MarkerLayer), findsNothing); 41 | expect(find.byType(PolylineLayer), findsOneWidget); 42 | expect(find.byType(PolygonLayer), findsNothing); 43 | 44 | final polyline = tester.firstWidget(find.byType(PolylineLayer)); 45 | expect(polyline.polylines.length, equals(1)); 46 | expect(polyline.polylines.first.strokeWidth, equals(11.0)); 47 | expect(polyline.polylines.first.color, equals(Color(0xff2222ee))); 48 | }); 49 | 50 | /*testWidgets('Downloads data from the network', (tester) async { 51 | await tester.pumpWidget(JsonTestApp( 52 | layer: GeoJsonLayer( 53 | data: NetworkGeoJson( 54 | 'https://raw.githubusercontent.com/mapbox/simplestyle-spec/refs/heads/master/1.1.0/example.geojson'), 55 | ), 56 | )); 57 | expect(find.byType(GeoJsonLayer), findsOneWidget); 58 | await tester.pumpAndSettle(Duration(seconds: 1)); 59 | expect(find.byType(MarkerLayer), findsOneWidget); 60 | expect(find.byType(PolylineLayer), findsOneWidget); 61 | });*/ 62 | } 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GeoJSON layer for Flutter Map 2 | 3 | This package adds a `GeoJsonLayer` class, which can be added to a list 4 | of layers in `flutter_map` to display data from a GeoJSON source. There 5 | are convenience constructors for using GeoJSON data from an asset 6 | or to download it from a website. The layer supports 7 | [simplespec](https://github.com/mapbox/simplestyle-spec/blob/master/1.1.0/README.md) 8 | to style features according to their properties, and allows overriding 9 | the defaults and filtering the features. 10 | 11 | ## Getting started 12 | 13 | The same as for all other packages: copy the package name and version 14 | into your `pubspec.yaml`, and in your class, import the package: 15 | 16 | import 'package:flutter_map_geojson2/flutter_map_geojson2.dart'; 17 | 18 | ## Usage 19 | 20 | Pretty straightforward if you have used `flutter_map` before. Use the 21 | layer like this: 22 | 23 | ```dart 24 | @override 25 | Widget build(BuildContext context) { 26 | return FlutterMap( 27 | options: MapOptions(...), 28 | layers: [ 29 | GeoJsonLayer.memory({ 30 | 'type': 'Feature', 31 | 'geometry': { 32 | 'type': 'LineString', 33 | 'coordinates': [ 34 | [24.7, 59.4], 35 | [24.8, 59.401] 36 | ], 37 | }, 38 | 'properties': { 39 | 'stroke': '#22e', 40 | 'stroke-width': 11, 41 | }, 42 | }), 43 | ], 44 | ); 45 | } 46 | ``` 47 | 48 | Alternatively you can package a GeoJSON file in the assets (don't forget to 49 | list them in `pubspec.yaml`) and add to your map like this: 50 | 51 | ```dart 52 | layers: [ 53 | GeoJsonLayer.asset('data/overlay.geojson'), 54 | ], 55 | ``` 56 | 57 | To override marker and other object creation functions, see the corresponding 58 | layer class arguments. You can have lines and polygons tappable by supplying 59 | a `hitNotifier` object. For markers, you obviously can override the entire 60 | thing. 61 | 62 | ## Additional information 63 | 64 | Note that there is no explicit error handling in the widget. Meaning, if you get 65 | broken data, it just won't be displayed without any notification. Broken 66 | GeoJSON features get silently skipped. If you have any ideas on how to do this properly, 67 | I'd love to hear them. 68 | 69 | Thanks to [flutter_map](https://pub.dev/packages/flutter_map) authors for the 70 | ultimate mapping solution for Flutter, and to Joze, the 71 | [flutter_map_geojson](https://pub.dev/packages/flutter_map_geojson) author 72 | for the inspiration. This package differs from his in using cleaner API 73 | and recent Flutter Map features. -------------------------------------------------------------------------------- /lib/geojson2/geojson_provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert' show json, utf8; 3 | import 'dart:io' show File, HttpClient, HttpStatus, IOException; 4 | 5 | import 'package:flutter/services.dart'; 6 | 7 | class NotAGeoJson implements Exception { 8 | const NotAGeoJson(); 9 | } 10 | 11 | class GeoJsonLoadException implements IOException { 12 | final String message; 13 | 14 | const GeoJsonLoadException(this.message); 15 | 16 | @override 17 | String toString() => 'GeoJsonLoadException("$message")'; 18 | } 19 | 20 | void _validateGeoJson(dynamic data) { 21 | if (data is! Map) { 22 | throw GeoJsonLoadException("Contents are not a GeoJSON."); 23 | } 24 | const kValidTypes = {'FeatureCollection', 'Feature'}; 25 | if (!kValidTypes.contains(data['type'])) { 26 | throw GeoJsonLoadException( 27 | "Root object type is not valid for a GeoJSON: ${data['type']}"); 28 | } 29 | } 30 | 31 | /// Identifies a GeoJSON file without having the actual data. 32 | abstract class GeoJsonProvider { 33 | const GeoJsonProvider(); 34 | FutureOr> loadData(); 35 | } 36 | 37 | class MemoryGeoJson extends GeoJsonProvider { 38 | final Map data; 39 | 40 | const MemoryGeoJson(this.data); 41 | 42 | @override 43 | FutureOr> loadData() { 44 | _validateGeoJson(data); 45 | return data; 46 | } 47 | } 48 | 49 | class FileGeoJson extends GeoJsonProvider { 50 | final File file; 51 | 52 | const FileGeoJson(this.file); 53 | 54 | @override 55 | FutureOr> loadData() async { 56 | if (!await file.exists()) { 57 | throw GeoJsonLoadException("File ${file.path} does not exist"); 58 | } 59 | 60 | dynamic data; 61 | try { 62 | final String contents = await file.readAsString(); 63 | data = json.decode(contents); 64 | } on Exception catch (e) { 65 | throw GeoJsonLoadException("Error loading or parsing GeoJSON: $e"); 66 | } 67 | _validateGeoJson(data); 68 | return data; 69 | } 70 | } 71 | 72 | class AssetGeoJson extends GeoJsonProvider { 73 | final String asset; 74 | final AssetBundle? bundle; 75 | 76 | const AssetGeoJson(this.asset, {this.bundle}); 77 | 78 | @override 79 | FutureOr> loadData() async { 80 | final bundle = this.bundle ?? rootBundle; 81 | dynamic data; 82 | try { 83 | final String content = await bundle.loadString(asset, cache: false); 84 | data = json.decode(content); 85 | } on Exception catch (e) { 86 | throw GeoJsonLoadException( 87 | "Error loading or parsing GeoJSON from bundle: $e"); 88 | } 89 | _validateGeoJson(data); 90 | return data; 91 | } 92 | } 93 | 94 | class NetworkGeoJson extends GeoJsonProvider { 95 | final String url; 96 | final Map headers; 97 | 98 | const NetworkGeoJson(this.url, {this.headers = const {}}); 99 | 100 | static final _httpClient = HttpClient(); 101 | 102 | @override 103 | FutureOr> loadData() async { 104 | String content; 105 | try { 106 | final uri = Uri.base.resolve(url); 107 | final request = await _httpClient.getUrl(uri); 108 | headers.forEach((k, v) { 109 | request.headers.add(k, v); 110 | }); 111 | final response = await request.close(); 112 | if (response.statusCode != HttpStatus.ok) { 113 | await response.drain([]); 114 | throw GeoJsonLoadException( 115 | "Error requesting GeoJSON from $url: HTTP ${response.statusCode}"); 116 | } 117 | content = await response.transform(utf8.decoder).join(); 118 | } on Exception catch (e) { 119 | if (e is GeoJsonLoadException) rethrow; 120 | throw GeoJsonLoadException("Error downloading GeoJSON from $url: $e"); 121 | } 122 | 123 | dynamic data; 124 | try { 125 | data = json.decode(content); 126 | } on Exception catch (e) { 127 | throw GeoJsonLoadException("Error parsing GeoJSON: $e"); 128 | } 129 | 130 | _validateGeoJson(data); 131 | return data; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /lib/geojson2/default_features.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_map/flutter_map.dart'; 3 | import 'package:flutter_map_geojson2/flutter_map_geojson2.dart'; 4 | import 'package:latlong2/latlong.dart'; 5 | import 'package:material_color_names/material_color_names.dart'; 6 | 7 | /// Default values for default object builders. Functions [defaultOnPoint], 8 | /// [defaultOnPolyline], and [defaultOnPolygon] use values from this 9 | /// object for properties missing in GeoJSON objects. 10 | /// 11 | /// There are two singletons that can be used without instantiating 12 | /// this class: [initial] (used by default) and [leaflet]. 13 | class GeoJsonStyleDefaults { 14 | final Color markerColor; 15 | final String markerSize; 16 | final Color strokeColor; 17 | final double strokeOpacity; 18 | final double strokeWidth; 19 | final Color fillColor; 20 | final double fillOpacity; 21 | 22 | /// Defaults as defined in the [simplestyle-spec](https://github.com/mapbox/simplestyle-spec/blob/master/1.1.0/README.md). 23 | /// Colors are shades of gray, strokes are 2 pixels wide, 24 | /// and the default fill opacity is 0.6. 25 | static const initial = GeoJsonStyleDefaults(); 26 | 27 | /// Defaults as defined in the [Leaflet library](https://leafletjs.com/reference.html#path). 28 | /// Colors are blue, strokes are 3 pixels wide, and 29 | /// the default fill opacity is very faint, 0.2. 30 | static const leaflet = GeoJsonStyleDefaults( 31 | strokeColor: Color(0xff3388ff), 32 | strokeOpacity: 1.0, 33 | strokeWidth: 3, 34 | fillColor: Color(0xff3388ff), 35 | fillOpacity: 0.2, 36 | ); 37 | 38 | const GeoJsonStyleDefaults({ 39 | this.markerColor = const Color(0xff7e7e7e), 40 | this.markerSize = 'medium', 41 | this.strokeColor = const Color(0xff555555), 42 | this.strokeOpacity = 1.0, 43 | this.strokeWidth = 2.0, 44 | this.fillColor = const Color(0xff555555), 45 | this.fillOpacity = 0.6, 46 | }); 47 | } 48 | 49 | const _kMarkerSizes = { 50 | 'small': 20.0, 51 | 'medium': 36.0, 52 | 'large': 48.0, 53 | }; 54 | 55 | /// Default implementation for [GeoJsonLayer.onPoint]. Parses object properties 56 | /// to determine marker style. Those properties are supported: 57 | /// 58 | /// * `marker-color`: marker color, as a material color name or a hexadecimal value. 59 | /// * `marker-size`: a choice from "small", "medium", and "large". 60 | Marker defaultOnPoint(LatLng point, Map props, 61 | {GeoJsonStyleDefaults? defaults}) { 62 | defaults ??= GeoJsonStyleDefaults.initial; 63 | 64 | Color? color = props.containsKey('marker-color') 65 | ? colorFromString(props['marker-color']) 66 | : null; 67 | color ??= defaults.markerColor; 68 | 69 | final double size = 70 | _kMarkerSizes[props['marker-size'] ?? defaults.markerSize] ?? 71 | _kMarkerSizes[defaults.markerSize] ?? 72 | _kMarkerSizes['medium']!; 73 | 74 | return Marker( 75 | point: point, 76 | width: size, 77 | height: size, 78 | alignment: Alignment.bottomCenter, 79 | child: Icon( 80 | Icons.location_pin, 81 | color: color, 82 | size: size, 83 | ), 84 | ); 85 | } 86 | 87 | /// Default implementation for [GeoJsonLayer.onPolyline]. Parses object properties 88 | /// to determine polyline style. Those properties are supported: 89 | /// 90 | /// * `stroke`: line color, as a material color name or a hexadecimal value. 91 | /// * `stroke-opacity`: opacity, a floating-point number between 0.0 and 1.0. 92 | /// * `stroke-width`: line width in pixels. 93 | Polyline defaultOnPolyline(List points, Map props, 94 | {GeoJsonStyleDefaults? defaults}) { 95 | defaults ??= GeoJsonStyleDefaults.initial; 96 | 97 | Color? stroke = 98 | props.containsKey('stroke') ? colorFromString(props['stroke']) : null; 99 | stroke ??= defaults.strokeColor; 100 | 101 | dynamic opacity = props['stroke-opacity']; 102 | if (opacity is String) opacity = double.tryParse(opacity); 103 | if (opacity is num && opacity >= 0.0 && opacity <= 1.0) { 104 | stroke = stroke.withValues(alpha: opacity.toDouble()); 105 | } else if (stroke.a > 0.99) { 106 | stroke = stroke.withValues(alpha: defaults.strokeOpacity); 107 | } 108 | 109 | dynamic width = props['stroke-width']; 110 | if (width is String) width = double.tryParse(width); 111 | 112 | return Polyline( 113 | points: points, 114 | color: stroke, 115 | strokeWidth: width is num ? width.toDouble() : defaults.strokeWidth, 116 | ); 117 | } 118 | 119 | /// Default implementation for [GeoJsonLayer.onPolygon]. Parses object properties 120 | /// to determine polygon style. Those properties are supported: 121 | /// 122 | /// * `fill`: fill color, as a material color name or a hexadecimal value. 123 | /// * `fill-opacity`: opacity, a floating-point number between 0.0 and 1.0. 124 | /// * Stroke options from [defaultOnPolyline] are also included for the 125 | /// polygon border. 126 | Polygon defaultOnPolygon( 127 | List points, List>? holes, Map props, 128 | {GeoJsonStyleDefaults? defaults}) { 129 | defaults ??= GeoJsonStyleDefaults.initial; 130 | 131 | Color? fill = 132 | props.containsKey('fill') ? colorFromString(props['fill']) : null; 133 | fill ??= defaults.fillColor; 134 | 135 | dynamic opacity = props['fill-opacity']; 136 | if (opacity is String) opacity = double.tryParse(opacity); 137 | if (opacity is num && opacity >= 0.0 && opacity <= 1.0) { 138 | fill = opacity == 0.0 ? null : fill.withValues(alpha: opacity.toDouble()); 139 | } else if (fill.a > 0.99) { 140 | fill = fill.withValues(alpha: defaults.fillOpacity); 141 | } 142 | 143 | Color? stroke = 144 | props.containsKey('stroke') ? colorFromString(props['stroke']) : null; 145 | stroke ??= defaults.strokeColor; 146 | 147 | dynamic sOpacity = props['stroke-opacity']; 148 | if (sOpacity is String) sOpacity = double.tryParse(sOpacity); 149 | if (sOpacity is num && sOpacity >= 0.0 && sOpacity <= 1.0) { 150 | stroke = stroke.withValues(alpha: sOpacity.toDouble()); 151 | } else if (stroke.a > 0.99) { 152 | stroke = stroke.withValues(alpha: defaults.strokeOpacity); 153 | } 154 | 155 | dynamic width = props['stroke-width']; 156 | if (width is String) width = double.tryParse(width); 157 | 158 | return Polygon( 159 | points: points, 160 | holePointsList: holes, 161 | color: fill, 162 | borderColor: stroke, 163 | borderStrokeWidth: width is num ? width.toDouble() : defaults.strokeWidth, 164 | ); 165 | } 166 | -------------------------------------------------------------------------------- /lib/geojson2/geojson_layer.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_map/flutter_map.dart'; 5 | import 'package:flutter_map_geojson2/geojson2/default_features.dart'; 6 | import 'package:flutter_map_geojson2/geojson2/geojson_provider.dart'; 7 | import 'package:latlong2/latlong.dart'; 8 | 9 | /// A callback function that creates a [Marker] instance from a [point] 10 | /// geometry and GeoJSON object [properties]. 11 | typedef OnPointCallback = Marker Function( 12 | LatLng point, Map properties); 13 | 14 | /// A callback function that creates a [Polyline] instance from 15 | /// [points] making up the line, and GeoJSON object [properties]. 16 | typedef OnPolylineCallback = Polyline Function( 17 | List points, Map properties); 18 | 19 | /// A callback function that creates a [Polygon] instance from 20 | /// [points] that make up the outer ring, a list of [holes], and 21 | /// GeoJSON object [properties]. 22 | typedef OnPolygonCallback = Polygon Function(List points, 23 | List>? holes, Map properties); 24 | 25 | /// A filtering function that receives a GeoJSON object [geometryType] 26 | /// (can be "Point", "LineString" etc) and its [properties]. Return 27 | /// `false` to skip the object. 28 | typedef FeatureFilterCallback = bool Function( 29 | String geometryType, Map properties); 30 | 31 | /// Creates a layer that displays contents of a GeoJSON source. 32 | class GeoJsonLayer extends StatefulWidget { 33 | /// GeoJSON data source. Just like with [ImageProvider], it's 34 | /// possible to do network calls or use an asset-bundled file. 35 | /// Use [MemoryGeoJson] to supply a [Map] if you don't need 36 | /// any of that. 37 | final GeoJsonProvider data; 38 | 39 | /// This function receives a marker location and feature properties, 40 | /// and builds a flutter_map [Marker]. See [defaultOnPoint] for an example. 41 | /// You can call that function first to process styles, and then adjust 42 | /// the returned marker. 43 | final OnPointCallback? onPoint; 44 | 45 | /// This function takes a list of points and feature properties to build 46 | /// a flutter_map [Polyline]. See [defaultOnPolyline] for an example. 47 | final OnPolylineCallback? onPolyline; 48 | 49 | /// This function takes feature properties and some other parameters to build 50 | /// a flutter_map [Polygon]. See [defaultOnPolygon] for an example. 51 | final OnPolygonCallback? onPolygon; 52 | 53 | /// A function to exclude some features from the GeoJSON. It takes 54 | /// a geometryType and feature properties to make that decision. Return 55 | /// `false` when you don't need the feature. 56 | final FeatureFilterCallback? filter; 57 | 58 | /// Hit notifier for polylines and polygons. See [LayerHitNotifier] 59 | /// documentation for explanations and an example. 60 | final LayerHitNotifier? hitNotifier; 61 | 62 | /// For a polyline, a change to adjust its hit box. The default is the 63 | /// same as in [Polyline], 10 pixels. 64 | final double polylineHitbox; 65 | 66 | /// When not overriding callbacks, this layer uses the default builders. 67 | /// Those use object properties to determine colors and dimensions. 68 | /// To change default values, use this property. 69 | final GeoJsonStyleDefaults? styleDefaults; 70 | 71 | /// Creates a layer instance. You might be better off using a specialized 72 | /// constructor: 73 | /// 74 | /// * [GeoJsonLayer.memory] for already loaded data. 75 | /// * [GeoJsonLayer.file] for loading GeoJSON from files. 76 | /// * [GeoJsonLayer.asset] for GeoJSON files bundled in assets. 77 | /// * [GeoJsonLayer.network] for downloading GeoJSON files over the network. 78 | const GeoJsonLayer({ 79 | super.key, 80 | required this.data, 81 | this.onPoint, 82 | this.onPolyline, 83 | this.onPolygon, 84 | this.filter, 85 | this.styleDefaults, 86 | this.hitNotifier, 87 | this.polylineHitbox = 10, 88 | }); 89 | 90 | GeoJsonLayer.memory( 91 | Map data, { 92 | super.key, 93 | this.onPoint, 94 | this.onPolyline, 95 | this.onPolygon, 96 | this.filter, 97 | this.styleDefaults, 98 | this.hitNotifier, 99 | this.polylineHitbox = 10, 100 | }) : data = MemoryGeoJson(data); 101 | 102 | GeoJsonLayer.file( 103 | File file, { 104 | super.key, 105 | this.onPoint, 106 | this.onPolyline, 107 | this.onPolygon, 108 | this.filter, 109 | this.styleDefaults, 110 | this.hitNotifier, 111 | this.polylineHitbox = 10, 112 | }) : data = FileGeoJson(file); 113 | 114 | GeoJsonLayer.asset( 115 | String name, { 116 | super.key, 117 | AssetBundle? bundle, 118 | this.onPoint, 119 | this.onPolyline, 120 | this.onPolygon, 121 | this.filter, 122 | this.styleDefaults, 123 | this.hitNotifier, 124 | this.polylineHitbox = 10, 125 | }) : data = AssetGeoJson(name, bundle: bundle); 126 | 127 | GeoJsonLayer.network( 128 | String url, { 129 | super.key, 130 | this.onPoint, 131 | this.onPolyline, 132 | this.onPolygon, 133 | this.filter, 134 | this.styleDefaults, 135 | this.hitNotifier, 136 | this.polylineHitbox = 10, 137 | }) : data = NetworkGeoJson(url); 138 | 139 | @override 140 | State createState() => _GeoJsonLayerState(); 141 | } 142 | 143 | class _GeoJsonLayerState extends State { 144 | final List _markers = []; 145 | final List _polylines = []; 146 | final List _polygons = []; 147 | 148 | @override 149 | void initState() { 150 | super.initState(); 151 | _loadData(); 152 | } 153 | 154 | void _clear() { 155 | _markers.clear(); 156 | _polylines.clear(); 157 | _polygons.clear(); 158 | } 159 | 160 | Future _loadData() async { 161 | try { 162 | final data = await widget.data.loadData(); 163 | _clear(); 164 | _parseGeoJson(data); 165 | } on Exception { 166 | if (mounted) setState(() {}); 167 | } 168 | } 169 | 170 | void _parseGeoJson(Map data) { 171 | final type = data['type']; 172 | if (type == 'FeatureCollection' || data['features'] is List) { 173 | for (final f in data['features'] ?? []) { 174 | if (f is Map) { 175 | _parseFeature(f); 176 | } 177 | } 178 | } else if (type != null) { 179 | _parseFeature(data); 180 | } 181 | if (mounted) setState(() {}); 182 | } 183 | 184 | LatLng? _parseCoordinate(List data) { 185 | final doubles = data.whereType(); 186 | if (doubles.length < 2) return null; 187 | return LatLng(doubles.elementAt(1), doubles.first); 188 | } 189 | 190 | List? _parseLineString(List coordinates) { 191 | final points = coordinates 192 | .map((list) => _parseCoordinate(list)) 193 | .whereType() 194 | .toList(); 195 | if (points.length >= 2) { 196 | return points; 197 | } 198 | return null; 199 | } 200 | 201 | List>? _parsePolygon(List coordinates) { 202 | bool first = true; 203 | final List> result = []; 204 | final rings = coordinates.whereType(); 205 | for (final ring in rings) { 206 | final points = _parseLineString(ring); 207 | if (points != null && points.length >= 3) { 208 | result.add(points); 209 | } else if (first) { 210 | // We can skip holes, but not the outer ring. 211 | return null; 212 | } 213 | first = false; 214 | } 215 | return result.isEmpty ? null : result; 216 | } 217 | 218 | Marker _buildMarker(LatLng point, Map properties) { 219 | if (widget.onPoint != null) { 220 | return widget.onPoint!(point, properties); 221 | } else { 222 | return defaultOnPoint(point, properties, defaults: widget.styleDefaults); 223 | } 224 | } 225 | 226 | Polyline _buildLine(List points, Map properties) { 227 | if (widget.onPolyline != null) { 228 | return widget.onPolyline!(points, properties); 229 | } else { 230 | return defaultOnPolyline(points, properties, 231 | defaults: widget.styleDefaults); 232 | } 233 | } 234 | 235 | Polygon _buildPolygon( 236 | List> rings, Map properties) { 237 | if (widget.onPolygon != null) { 238 | return widget.onPolygon!( 239 | rings.first, 240 | rings.length == 1 ? null : rings.sublist(1), 241 | properties, 242 | ); 243 | } else { 244 | return defaultOnPolygon( 245 | rings.first, 246 | rings.length == 1 ? null : rings.sublist(1), 247 | properties, 248 | defaults: widget.styleDefaults, 249 | ); 250 | } 251 | } 252 | 253 | void _parseFeature(Map data) { 254 | if (data['type'] != 'Feature') return; 255 | final geometry = data['geometry']; 256 | if (geometry == null || geometry is! Map) return; 257 | final String? geometryType = geometry['type']; 258 | if (geometryType == null) return; 259 | final coordinates = geometry['coordinates']; 260 | if (coordinates is! List) return; 261 | final Map properties = data['properties'] ?? {}; 262 | 263 | if (widget.filter != null && !widget.filter!(geometryType, properties)) { 264 | return; 265 | } 266 | 267 | switch (geometryType) { 268 | case 'Point': 269 | final point = _parseCoordinate(coordinates); 270 | if (point != null) { 271 | _markers.add(_buildMarker(point, properties)); 272 | } 273 | 274 | case 'MultiPoint': 275 | coordinates.whereType().forEach((p) { 276 | final point = _parseCoordinate(p); 277 | if (point != null) { 278 | _markers.add(_buildMarker(point, properties)); 279 | } 280 | }); 281 | 282 | case 'LineString': 283 | final points = _parseLineString(coordinates); 284 | if (points != null) { 285 | _polylines.add(_buildLine(points, properties)); 286 | } 287 | 288 | case 'MultiLineString': 289 | coordinates.whereType().forEach((p) { 290 | final points = _parseLineString(p); 291 | if (points != null) { 292 | _polylines.add(_buildLine(points, properties)); 293 | } 294 | }); 295 | 296 | case 'Polygon': 297 | final rings = _parsePolygon(coordinates); 298 | if (rings != null) { 299 | _polygons.add(_buildPolygon(rings, properties)); 300 | } 301 | 302 | case 'MultiPolygon': 303 | coordinates.whereType().forEach((p) { 304 | final rings = _parsePolygon(p); 305 | if (rings != null) { 306 | _polygons.add(_buildPolygon(rings, properties)); 307 | } 308 | }); 309 | } 310 | } 311 | 312 | @override 313 | void didUpdateWidget(covariant GeoJsonLayer oldWidget) { 314 | super.didUpdateWidget(oldWidget); 315 | if (widget.data != oldWidget.data) _loadData(); 316 | } 317 | 318 | @override 319 | Widget build(BuildContext context) { 320 | return Stack( 321 | children: [ 322 | if (_polygons.isNotEmpty) 323 | PolygonLayer( 324 | polygons: _polygons, 325 | hitNotifier: widget.hitNotifier, 326 | drawLabelsLast: true, 327 | ), 328 | if (_polylines.isNotEmpty) 329 | PolylineLayer( 330 | polylines: _polylines, 331 | hitNotifier: widget.hitNotifier, 332 | minimumHitbox: widget.polylineHitbox, 333 | ), 334 | if (_markers.isNotEmpty) 335 | MarkerLayer( 336 | markers: _markers, 337 | ), 338 | ], 339 | ); 340 | } 341 | } 342 | --------------------------------------------------------------------------------