├── lib ├── which_polygon.dart ├── country_coder.dart └── src │ ├── bbox.dart │ ├── location_matcher.dart │ ├── location_set.dart │ ├── lineclip.dart │ ├── which_polygon.dart │ ├── region_feature.dart │ ├── region_features.dart │ └── country_coder.dart ├── analysis_options.yaml ├── tool └── update_borders.sh ├── .gitignore ├── pubspec.yaml ├── CHANGELOG.md ├── LICENSE ├── test ├── fixtures │ ├── overlapping.dart │ ├── locset_features.dart │ └── states.dart ├── which_polygon.dart ├── country_coder.dart ├── location_matcher.dart └── lineclip.dart └── README.md /lib/which_polygon.dart: -------------------------------------------------------------------------------- 1 | export 'src/lineclip.dart' show LineClip; 2 | export 'src/which_polygon.dart' show WhichPolygon; 3 | export 'src/bbox.dart' show BBox, Point; 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tool/update_borders.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | BORDERS=../lib/src/data/borders.dart 3 | TMP_BORDERS=/tmp/borders.json 4 | curl 'https://raw.githubusercontent.com/ideditor/country-coder/main/src/data/borders.json' > $TMP_BORDERS 5 | echo "final String bordersRaw = '''" > $BORDERS 6 | cat $TMP_BORDERS >> $BORDERS 7 | echo "''';" >> $BORDERS 8 | rm $TMP_BORDERS 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # Just in case these appear 19 | .metadata 20 | .packages 21 | pubspec.lock 22 | .dart_tool/ 23 | build/ 24 | 25 | bench_json/ 26 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: country_coder 2 | description: Convert longitude-latitude pairs to ISO 3166-1 codes quickly and locally. 3 | version: 1.2.0 4 | homepage: https://github.com/Zverik/flutter_country_coder 5 | 6 | environment: 7 | sdk: ">=2.12.0 <4.0.0" 8 | 9 | dependencies: 10 | rbush: ^1.1.0 11 | 12 | dev_dependencies: 13 | lints: ^2.1.1 14 | test: any 15 | flutter: 16 | sdk: flutter 17 | -------------------------------------------------------------------------------- /lib/country_coder.dart: -------------------------------------------------------------------------------- 1 | export 'src/region_feature.dart' 2 | show 3 | RegionFeature, 4 | RegionLevel, 5 | RegionProperties, 6 | RegionHeightUnit, 7 | RegionSpeedUnit, 8 | RegionDrivingSide, 9 | RegionIsoStatus; 10 | export 'src/region_features.dart' show RegionFeatureCollection; 11 | export 'src/location_set.dart' show LocationSet, LocationSetRadius; 12 | export 'src/location_matcher.dart' show LocationMatcher; 13 | export 'src/country_coder.dart' show CountryCoder; 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.2.0 2 | 3 | * LocationSet matching did not support exclude-only sets. 4 | * Updated polygons from country-coder 5.2.1. 5 | 6 | ## 1.1.1 7 | 8 | * Fixed type issues when building `LocationMatcher` and `LocationSet` from a raw json. 9 | * Replaced most factory methods with constructors. 10 | 11 | ## 1.1.0 12 | 13 | * Removed dependency on Flutter SDK. 14 | * To initialize the Country Coder instance asynchronously, now use `CountryCoder.prepareData` 15 | (see its dartdoc for details). 16 | 17 | ## 1.0.0 18 | 19 | * Initial release. 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2021, Ilya Zverev 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 14 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | PERFORMANCE OF THIS SOFTWARE. 16 | 17 | -------------------------------------------------------------------------------- /test/fixtures/overlapping.dart: -------------------------------------------------------------------------------- 1 | final overlapping = { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { 5 | "type": "Feature", 6 | "properties": {"name": "A"}, 7 | "geometry": { 8 | "type": "Polygon", 9 | "coordinates": [ 10 | [ 11 | [0, 0], 12 | [10, 0], 13 | [10, 10], 14 | [0, 10], 15 | [0, 0] 16 | ] 17 | ] 18 | } 19 | }, 20 | { 21 | "type": "Feature", 22 | "properties": {"name": "B"}, 23 | "geometry": { 24 | "type": "Polygon", 25 | "coordinates": [ 26 | [ 27 | [5, 5], 28 | [15, 5], 29 | [15, 15], 30 | [5, 15], 31 | [5, 5] 32 | ] 33 | ] 34 | } 35 | } 36 | ] 37 | }; 38 | -------------------------------------------------------------------------------- /test/fixtures/locset_features.dart: -------------------------------------------------------------------------------- 1 | final Map features = { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { 5 | "type": "Feature", 6 | "id": "dc_metro.geojson", 7 | "properties": {}, 8 | "geometry": { 9 | "type": "Polygon", 10 | "coordinates": [ 11 | [ 12 | [-77.04437, 38.70266], 13 | [-77.27783, 38.69409], 14 | [-77.57172, 38.91668], 15 | [-77.61017, 39.1258], 16 | [-77.27509, 39.21523], 17 | [-77.08694, 39.21204], 18 | [-76.87546, 39.05119], 19 | [-76.69968, 38.97863], 20 | [-76.71066, 38.77657], 21 | [-76.84662, 38.7048], 22 | [-77.04437, 38.70266] 23 | ] 24 | ] 25 | } 26 | }, 27 | { 28 | "type": "Feature", 29 | "properties": {"id": "philly_metro.geojson"}, 30 | "geometry": { 31 | "type": "Polygon", 32 | "coordinates": [ 33 | [ 34 | [-75.7, 40.3], 35 | [-75.3, 40.4], 36 | [-74.7, 40.3], 37 | [-74.45, 40.1], 38 | [-74.9, 39.55], 39 | [-75.5, 39.55], 40 | [-75.8, 39.7218], 41 | [-76.23, 39.7211], 42 | [-75.7, 40.3] 43 | ] 44 | ] 45 | } 46 | } 47 | ] 48 | }; 49 | -------------------------------------------------------------------------------- /lib/src/bbox.dart: -------------------------------------------------------------------------------- 1 | /// A single ([lon], [lat]) point. It is used for storage 2 | /// and function arguments only, so it doesn't have any 3 | /// utility methods. 4 | class Point { 5 | final double lat; 6 | final double lon; 7 | 8 | const Point(this.lon, this.lat); 9 | 10 | Point.fromJson(List coords) 11 | : lon = coords[0].toDouble(), 12 | lat = coords[1].toDouble(); 13 | 14 | @override 15 | bool operator ==(Object other) { 16 | return other is Point && other.lat == lat && other.lon == lon; 17 | } 18 | 19 | @override 20 | int get hashCode => lat.hashCode + lon.hashCode; 21 | 22 | @override 23 | String toString() => '{$lon, $lat}'; 24 | } 25 | 26 | /// A rectangular bounding box. Used for storage and for function 27 | /// arguments. 28 | class BBox { 29 | final double minLon; 30 | final double minLat; 31 | final double maxLon; 32 | final double maxLat; 33 | 34 | const BBox(this.minLon, this.minLat, this.maxLon, this.maxLat); 35 | 36 | BBox.fromJson(List coords) 37 | : minLon = coords[0].toDouble(), 38 | minLat = coords[1].toDouble(), 39 | maxLon = coords[2].toDouble(), 40 | maxLat = coords[3].toDouble(); 41 | 42 | Point get center => Point((minLon + maxLon) / 2.0, (minLat + maxLat) / 2.0); 43 | 44 | @override 45 | String toString() => '{$minLon, $minLat, $maxLon, $maxLat}'; 46 | } 47 | -------------------------------------------------------------------------------- /test/which_polygon.dart: -------------------------------------------------------------------------------- 1 | import 'package:country_coder/which_polygon.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:test/test.dart'; 4 | import 'fixtures/overlapping.dart'; 5 | import 'fixtures/states.dart'; 6 | 7 | dynamic loadAsync(Map featureCollection) { 8 | return WhichPolygon(states['features'] as List>).serialize(); 9 | } 10 | 11 | void main() { 12 | final query = WhichPolygon(states['features'] as List>); 13 | 14 | test('queries polygons with a point', () { 15 | expect(query(-100, 45)['name'], equals('South Dakota')); 16 | expect(query(-90, 30)['name'], equals('Louisiana')); 17 | expect(query(-50, 30), isNull); 18 | }); 19 | 20 | test('queries polygons with a bbox', () { 21 | final result = query.bbox(-100, 45, -99.5, 45.5); 22 | expect(result, isNotEmpty); 23 | expect(result[0]['name'], equals('South Dakota')); 24 | 25 | final qq = query.bbox(-104.2, 44, -103, 45); 26 | final names = qq.map((e) => e['name']).toList(); 27 | names.sort(); 28 | expect(names.length, equals(2)); 29 | expect(names, equals(['South Dakota', 'Wyoming'])); 30 | }); 31 | 32 | test('queries overlapping polygons with a point', () { 33 | final queryOver = WhichPolygon(overlapping['features'] as List>); 34 | expect(queryOver.one(7.5, 7.5)['name'], equals('A'), reason: 'without multi option'); 35 | expect(queryOver.all(7.5, 7.5), equals([{'name': 'A'}, {'name': 'B'}]), reason: 'with multi option'); 36 | expect(queryOver(-10, 10), isNull); 37 | }); 38 | 39 | test('can load data asynchronously', () async { 40 | final query = WhichPolygon.fromSerialized(await compute(loadAsync, states)); 41 | expect(query(-90, 30)['name'], equals('Louisiana')); 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /test/country_coder.dart: -------------------------------------------------------------------------------- 1 | import 'package:country_coder/country_coder.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | final query = CountryCoder.instance; 7 | 8 | test('Initial instance is not initialized', () { 9 | expect(query.ready, isFalse); 10 | }); 11 | 12 | test('Method throw an exception when not initialized', () { 13 | expect(() => query.iso1A2Code(query: 'Russia'), throwsStateError); 14 | }); 15 | 16 | test('Can load data asynchronously', () async { 17 | query.load(await compute(CountryCoder.prepareData, null)); 18 | expect(query.ready, isTrue); 19 | }); 20 | 21 | test('Processes basic string queries', () { 22 | expect(query.iso1A2Code(query: 'uk'), equals('GB')); 23 | expect(query.iso1A2Code(query: 'Russia'), equals('RU')); 24 | expect(query.region(query: 'NZ-CIT')?.country, equals('NZ')); 25 | }); 26 | 27 | test('Processes numeric queries', () { 28 | expect(query.iso1A2Code(query: 191), equals('HR')); 29 | expect(query.ccTLD(query: 70), equals('.ba')); 30 | expect(query.region(query: 1)?.nameEn, equals('World')); 31 | }); 32 | 33 | test('There is nothing at sea', () { 34 | expect(query.region(lon: -40, lat: 30), isNull); 35 | }); 36 | 37 | test('Finds regions by locations', () { 38 | expect(query.iso1A2Code(lon: 24.7, lat: 59.4), equals('EE')); 39 | expect(query.iso1A2Code(lon: 21, lat: 42.6), equals('XK')); 40 | expect(query.region(lon: 50, lat: -77)?.nameEn, equals('Antarctica')); 41 | }); 42 | 43 | test('Smallest region is not the default (country) one', () { 44 | expect(query.region(lon: 35, lat: 45)?.nameEn, equals('Russia')); 45 | expect(query.smallestOrMatchingRegion(lon: 35, lat: 45)?.nameEn, equals('Crimea')); 46 | }); 47 | 48 | // TODO: write more tests for the country coder 49 | // See https://github.com/ideditor/country-coder/blob/main/tests/country-coder.spec.ts 50 | } 51 | -------------------------------------------------------------------------------- /test/location_matcher.dart: -------------------------------------------------------------------------------- 1 | import 'package:country_coder/country_coder.dart'; 2 | import 'package:test/test.dart'; 3 | import 'fixtures/locset_features.dart'; 4 | 5 | void main() { 6 | CountryCoder.instance.load(); 7 | final matcher = LocationMatcher(features); 8 | 9 | test('empty location set matches anywhere', () { 10 | expect(matcher(10, 10, LocationSet()), isTrue); 11 | expect(matcher(-100, 70, LocationSet()), isTrue); 12 | expect(matcher(60, 35, LocationSet()), isTrue); 13 | }); 14 | 15 | test('exclude before include', () { 16 | final locSet = LocationSet.fromJson({ 17 | 'exclude': ['DE'], 18 | 'include': ['EU'] 19 | }); 20 | expect(matcher(19.7, 52.3, locSet), isTrue, reason: 'Poland'); 21 | expect(matcher(10.4, 53.1, locSet), isFalse, reason: 'Germany'); 22 | expect(matcher(38, 53, locSet), isFalse, reason: 'Russia'); 23 | }); 24 | 25 | test('custom polygons work', () { 26 | final locSet = LocationSet.fromJson({ 27 | 'include': ['usa'], 28 | 'exclude': ['philly_metro.geojson', 'dc_metro.geojson'] 29 | }); 30 | expect(matcher(-76.6, 39.3, locSet), isTrue, reason: 'Baltimore'); 31 | expect(matcher(-77.0, 38.9, locSet), isFalse, reason: 'Washington, DC'); 32 | expect(matcher(-75.2, 39.9, locSet), isFalse, reason: 'Philadelphia'); 33 | expect(matcher(-75.2, 39.3, locSet), isTrue, reason: 'around'); 34 | expect(matcher(-100.0, 24, locSet), isFalse, reason: 'Mexico'); 35 | }); 36 | 37 | test('circular areas, default radius is 25 km', () { 38 | final locSet = LocationSet.fromJson({ 39 | 'include': ['BRB', [18.55, 4.37]] 40 | }); 41 | expect(matcher(18.5, 4.48, locSet), isTrue, reason: 'near center'); 42 | expect(matcher(18.324, 4.371, locSet), isFalse, reason: '25.05 km west'); 43 | expect(matcher(18.331, 4.371, locSet), isTrue, reason: '24.5 km west'); 44 | expect(matcher(18.7357, 4.4973, locSet), isFalse, reason: '25.01 km northeast'); 45 | expect(matcher(18.7353, 4.4972, locSet), isTrue, reason: '24.98 km northeast'); 46 | expect(matcher(-59.54, 13.16, locSet), isTrue, reason: 'Barbados'); 47 | expect(matcher(-61.2, 13.26, locSet), isFalse, reason: 'Saint Vincent'); 48 | }); 49 | 50 | test('exclude-only sets work properly', () { 51 | final locSet = LocationSet.fromJson({ 52 | 'exclude': ['DE'] 53 | }); 54 | expect(matcher(19.7, 52.3, locSet), isTrue, reason: 'Poland'); 55 | expect(matcher(10.4, 53.1, locSet), isFalse, reason: 'Germany'); 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /lib/src/location_matcher.dart: -------------------------------------------------------------------------------- 1 | import 'country_coder.dart'; 2 | import 'location_set.dart'; 3 | import 'which_polygon.dart'; 4 | 5 | /// A callable class to match a location against a [LocationSet]. 6 | /// Pass a feature collection to the constructor to use additional 7 | /// features. These features need to have `id` properties ending with 8 | /// ".geojson". 9 | /// 10 | /// You need to initialize the [CountryCoder] before using this class 11 | /// with a `CountryCoder.instance.load()` or `loadAsync()`. 12 | class LocationMatcher { 13 | late final WhichPolygon additionalPolygons; 14 | final countryCoder = CountryCoder.instance; 15 | 16 | LocationMatcher([Map? featureCollection]) { 17 | if (featureCollection != null) { 18 | final features = 19 | featureCollection['features'].whereType>(); 20 | additionalPolygons = WhichPolygon(features); 21 | } else { 22 | additionalPolygons = WhichPolygon([]); 23 | } 24 | } 25 | 26 | /// Instantiates the class using an object from [serialize]. 27 | LocationMatcher.fromSerialized(dynamic data) 28 | : additionalPolygons = WhichPolygon.fromSerialized(data); 29 | 30 | /// Returns a serializable object that can be passed between threads. 31 | dynamic serialize() => additionalPolygons.serialize(); 32 | 33 | /// Tests whether a location ([lon], [lat]) matches the [locationSet] rules. 34 | /// First checks for `exclude` rules, so excluding a country but including 35 | /// a city in it won't work. Empty rules satisfy any location. 36 | bool call(double lon, double lat, LocationSet locationSet) { 37 | if (locationSet.isEmpty) return true; 38 | 39 | bool anyIncludes = false; 40 | 41 | // First check circular areas, since it's the fastest. 42 | if (locationSet.excludeCircular.any((area) => area.contains(lon, lat))) 43 | return false; 44 | 45 | anyIncludes |= 46 | locationSet.includeCircular.any((area) => area.contains(lon, lat)); 47 | 48 | // Now check additional polygons. 49 | final List includeGeojson = locationSet.include 50 | .where((element) => element.endsWith('.geojson')) 51 | .toList(); 52 | final List excludeGeojson = locationSet.exclude 53 | .where((element) => element.endsWith('.geojson')) 54 | .toList(); 55 | 56 | if (includeGeojson.isNotEmpty || excludeGeojson.isNotEmpty) { 57 | final List geojsonIds = additionalPolygons 58 | .all(lon, lat) 59 | .map((e) => e['id'] as String) 60 | .toList(); 61 | 62 | if (excludeGeojson.any((id) => geojsonIds.contains(id))) return false; 63 | anyIncludes |= includeGeojson.any((id) => geojsonIds.contains(id)); 64 | } 65 | 66 | // And finally employ CountryCoder for the remaining ids. 67 | if (locationSet.exclude 68 | .where((element) => !element.endsWith('.geojson')) 69 | .any((id) => countryCoder.isIn(lon: lon, lat: lat, inside: id))) 70 | return false; 71 | 72 | // If the list is empty, consider it including everything. 73 | if (locationSet.include.isEmpty) return true; 74 | 75 | anyIncludes |= locationSet.include 76 | .where((element) => !element.endsWith('.geojson')) 77 | .any((id) => countryCoder.isIn(lon: lon, lat: lat, inside: id)); 78 | 79 | return anyIncludes; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /test/lineclip.dart: -------------------------------------------------------------------------------- 1 | import 'package:country_coder/which_polygon.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | List toPoints(List> coords) => 5 | coords.map((e) => Point(e[0], e[1])).toList(); 6 | 7 | void main() { 8 | final clip = LineClip(); 9 | 10 | test('clips line', () { 11 | final result = clip(toPoints([ 12 | [-10, 10], [10, 10], [10, -10], [20, -10], [20, 10], [40, 10], 13 | [40, 20], [20, 20], [20, 40], [10, 40], [10, 20], [5, 20], [-10, 20] 14 | ]), BBox(0, 0, 30, 30)); 15 | 16 | expect(result, equals([ 17 | toPoints([[0, 10], [10, 10], [10, 0]]), 18 | toPoints([[20, 0], [20, 10], [30, 10]]), 19 | toPoints([[30, 20], [20, 20], [20, 30]]), 20 | toPoints([[10, 30], [10, 20], [5, 20], [0, 20]]), 21 | ])); 22 | }); 23 | 24 | test('clips line crossing through many times', () { 25 | final result = clip( 26 | toPoints([[10, -10], [10, 30], [20, 30], [20, -10]]), 27 | BBox(0, 0, 20, 20) 28 | ); 29 | 30 | expect(result, equals([ 31 | toPoints([[10, 0], [10, 20]]), 32 | toPoints([[20, 20], [20, 0]]), 33 | ])); 34 | }); 35 | 36 | test('clips polygon', () { 37 | final result = clip.polygon(toPoints([ 38 | [-10, 10], [0, 10], [10, 10], [10, 5], [10, -5], [10, -10], [20, -10], 39 | [20, 10], [40, 10], [40, 20], [20, 20], [20, 40], [10, 40], [10, 20], 40 | [5, 20], [-10, 20] 41 | ]), BBox(0, 0, 30, 30)); 42 | 43 | expect(result, equals(toPoints([ 44 | [0, 10], [0, 10], [10, 10], [10, 5], [10, 0], [20, 0], [20, 10], [30, 10], 45 | [30, 20], [20, 20], [20, 30], [10, 30], [10, 20], [5, 20], [0, 20] 46 | ]))); 47 | }); 48 | 49 | test('appends result if passed third argument', () { 50 | final List> arr = []; 51 | final result = clip(toPoints([[-10, 10], [30, 10]]), BBox(0, 0, 20, 20), arr); 52 | 53 | expect(result, equals([toPoints([[0, 10], [20, 10]])])); 54 | expect(result, same(arr)); 55 | }); 56 | 57 | test('clips floating point lines', () { 58 | final line = toPoints([ 59 | [-86.66015624999999, 42.22851735620852], [-81.474609375, 38.51378825951165], [-85.517578125, 37.125286284966776], 60 | [-85.8251953125, 38.95940879245423], [-90.087890625, 39.53793974517628], [-91.93359375, 42.32606244456202], 61 | [-86.66015624999999, 42.22851735620852] 62 | ]); 63 | final bbox = BBox(-91.93359375, 42.29356419217009, -91.7578125, 42.42345651793831); 64 | final result = clip(line, bbox); 65 | 66 | expect(result, equals([toPoints([ 67 | [-91.91208030440808, 42.29356419217009], 68 | [-91.93359375, 42.32606244456202], 69 | [-91.7578125, 42.3228109416169] 70 | ])])); 71 | }); 72 | 73 | test('preserves line if no protrusions exist', () { 74 | final result = clip(toPoints([[1, 1], [2, 2], [3, 3]]), BBox(0, 0, 30, 30)); 75 | 76 | expect(result, equals([toPoints([[1, 1], [2, 2], [3, 3]])])); 77 | }); 78 | 79 | test('clips without leaving empty parts', () { 80 | final result = clip(toPoints([[40, 40], [50, 50]]), BBox(0, 0, 30, 30)); 81 | 82 | expect(result, isEmpty); 83 | }); 84 | 85 | test('still works when polygon never crosses bbox', () { 86 | final result = clip.polygon(toPoints([ 87 | [3, 3], [5, 3], [5, 5], [3, 5], [3, 3] 88 | ]), BBox(0, 0, 2, 2)); 89 | 90 | expect(result, isEmpty); 91 | }); 92 | } 93 | -------------------------------------------------------------------------------- /lib/src/location_set.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math' as math; 2 | 3 | /// An object to define regions where something should or should not 4 | /// be included. Regions are referenced by any of the following: 5 | /// 6 | /// - Strings recognized by the [CountryCoder] class. These include ISO-3166-1 7 | /// codes, UN M.49 numeric codes, and some Wikidata QIDs. See 8 | /// [ideditor.codes](https://ideditor.codes) for a full list. 9 | /// - Filenames for custom geojson features. Pass them to [LocationMatcher] 10 | /// class if you need as a GeoJSON object. Each feature should have an `id` 11 | /// that ends in `.geojson`. For example, `new-jersey.geojson`. 12 | /// - Circular areas defined with [LocationSetRadius] objects. 13 | /// 14 | /// See [location-conflation](https://github.com/ideditor/location-conflation) 15 | /// for some information, although there can be differences due to 16 | /// different implementation in this library. 17 | class LocationSet { 18 | final List include; 19 | final List exclude; 20 | final List includeCircular; 21 | final List excludeCircular; 22 | 23 | const LocationSet({ 24 | this.include = const [], 25 | this.exclude = const [], 26 | this.includeCircular = const [], 27 | this.excludeCircular = const [], 28 | }); 29 | 30 | bool get isEmpty => 31 | include.isEmpty && 32 | exclude.isEmpty && 33 | includeCircular.isEmpty && 34 | excludeCircular.isEmpty; 35 | 36 | LocationSet.fromJson(Map data) 37 | : include = data['include'] == null 38 | ? [] 39 | : (data['include'] as List).whereType().toList(), 40 | exclude = data['exclude'] == null 41 | ? [] 42 | : (data['exclude'] as List).whereType().toList(), 43 | includeCircular = data['include'] == null 44 | ? [] 45 | : (data['include'] as List) 46 | .whereType() 47 | .map((e) => LocationSetRadius.fromJson(e)) 48 | .toList(), 49 | excludeCircular = data['exclude'] == null 50 | ? [] 51 | : (data['exclude'] as List) 52 | .whereType() 53 | .map((e) => LocationSetRadius.fromJson(e)) 54 | .toList(); 55 | 56 | Map> toJson() { 57 | Map> result = {}; 58 | 59 | if (include.isNotEmpty) result['include'] = include; 60 | 61 | if (includeCircular.isNotEmpty) { 62 | result['include'] = (result['include'] ?? []) + 63 | includeCircular.map((e) => e.toJson()).toList(); 64 | } 65 | 66 | if (exclude.isNotEmpty) result['exclude'] = exclude; 67 | 68 | if (excludeCircular.isNotEmpty) { 69 | result['exclude'] = (result['exclude'] ?? []) + 70 | excludeCircular.map((e) => e.toJson()).toList(); 71 | } 72 | 73 | return result; 74 | } 75 | } 76 | 77 | /// Use this class to include or exclude circular areas with a given 78 | /// center and given radius. The latter is defined in kilometers. 79 | /// If not specified, radius defaults to 25 km. 80 | class LocationSetRadius { 81 | final double longitude; 82 | final double latitude; 83 | final double radius; 84 | 85 | static const kDefaultRadius = 25.0; 86 | 87 | const LocationSetRadius(this.longitude, this.latitude, 88 | [this.radius = kDefaultRadius]); 89 | 90 | LocationSetRadius.fromJson(List data) 91 | : longitude = data[0].toDouble(), 92 | latitude = data[1].toDouble(), 93 | radius = data.length > 2 ? data[2].toDouble() : kDefaultRadius; 94 | 95 | List toJson() => 96 | [longitude, latitude, if (radius != kDefaultRadius) radius]; 97 | 98 | /// Returns `true` if the given location is inside the area. 99 | /// Calculates the distance using the Haversine algorithm. 100 | /// Accuracy can be out by 0.3%. 101 | bool contains(double lon, double lat) { 102 | final f1 = _degToRadian(latitude); 103 | final f2 = _degToRadian(lat); 104 | 105 | final sinDLat = math.sin((f2 - f1) / 2); 106 | final sinDLng = math.sin((_degToRadian(longitude) - _degToRadian(lon)) / 2); 107 | 108 | // Sides 109 | final a = 110 | sinDLat * sinDLat + sinDLng * sinDLng * math.cos(f1) * math.cos(f2); 111 | final c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)); 112 | 113 | return _earthRadius * c < radius; 114 | } 115 | 116 | static double _degToRadian(final double deg) => deg * (math.pi / 180.0); 117 | 118 | /// Equator radius in km (WGS84 ellipsoid) 119 | static const double _earthRadius = 6378.137; 120 | } 121 | -------------------------------------------------------------------------------- /lib/src/lineclip.dart: -------------------------------------------------------------------------------- 1 | // A very fast library for clipping polylines and polygons by a bounding box. 2 | // 3 | // Port of https://github.com/mapbox/lineclip v1.1.5 4 | // Code mainly by Vladimir Agafonkin, ported by Ilya Zverev. 5 | // Licensed ISC. 6 | 7 | import 'bbox.dart'; 8 | 9 | class LineClip { 10 | List> call(List points, BBox bbox, [List>? start]) => 11 | polyline(points, bbox, start); 12 | 13 | /// Cohen-Sutherland line clipping algorithm, adapted to efficiently 14 | /// handle polylines rather than just segments. 15 | List> polyline(List points, BBox bbox, [List>? start]) { 16 | final List> result = start ?? []; 17 | if (points.isEmpty) return result; 18 | 19 | final int len = points.length; 20 | int codeA = _bitCode(points.first, bbox); 21 | List part = []; 22 | 23 | for (int i = 1; i < len; i++) { 24 | Point a = points[i - 1]; 25 | Point b = points[i]; 26 | int codeB = _bitCode(b, bbox); 27 | int lastCode = codeB; 28 | 29 | int lastCodes = -1; 30 | while (true) { 31 | if ((codeA << 4) + codeB == lastCodes) { 32 | throw StateError('Stuck in an infinite loop. CodeA=$codeA, CodeB=$codeB.'); 33 | } 34 | lastCodes = (codeA << 4) + codeB; 35 | 36 | if (codeA | codeB == 0) { // accept 37 | part.add(a); 38 | 39 | if (codeB != lastCode) { // start a new line 40 | part.add(b); 41 | 42 | if (i < len - 1) { 43 | result.add(part); 44 | part = []; 45 | } 46 | } else if (i == len - 1) { 47 | part.add(b); 48 | } 49 | break; 50 | 51 | } else if (codeA & codeB != 0) { // trivial reject 52 | break; 53 | 54 | } else if (codeA != 0) { // a outside, intersect with clip edge 55 | a = _intersect(a, b, codeA, bbox); 56 | codeA = _bitCode(a, bbox); 57 | 58 | } else { // b outside 59 | b = _intersect(a, b, codeB, bbox); 60 | codeB = _bitCode(b, bbox); 61 | } 62 | } 63 | 64 | codeA = lastCode; 65 | } 66 | 67 | if (part.isNotEmpty) result.add(part); 68 | return result; 69 | } 70 | 71 | /// Sutherland-Hodgeman polygon clipping algorithm. 72 | List polygon(List points, BBox bbox) { 73 | List result = []; 74 | if (points.isEmpty) return result; 75 | 76 | // clip against each side of the clip rectangle 77 | for (int edge = 1; edge <= 8; edge *= 2) { 78 | result = []; 79 | Point prev = points.last; 80 | bool prevInside = (_bitCode(prev, bbox) & edge) == 0; 81 | 82 | for (final p in points) { 83 | final bool inside = (_bitCode(p, bbox) & edge) == 0; 84 | 85 | // if segment goes through the clip window, add an intersection 86 | if (inside != prevInside) 87 | result.add(_intersect(prev, p, edge, bbox)); 88 | 89 | if (inside) result.add(p); // add a point if it's inside 90 | 91 | prev = p; 92 | prevInside = inside; 93 | } 94 | 95 | points = result; 96 | 97 | if (points.isEmpty) break; 98 | } 99 | 100 | return result; 101 | } 102 | 103 | /// Intersect a segment against one of the 4 lines that make up the bbox. 104 | Point _intersect(Point a, Point b, int edge, BBox bbox) { 105 | if (edge & 8 != 0) { 106 | assert(a.lat != b.lat); 107 | return Point(a.lon + (b.lon - a.lon) * (bbox.maxLat - a.lat) / (b.lat - a.lat), bbox.maxLat); // top 108 | } else if (edge & 4 != 0) { 109 | assert(a.lat != b.lat); 110 | return Point(a.lon + (b.lon - a.lon) * (bbox.minLat - a.lat) / (b.lat - a.lat), bbox.minLat); // bottom 111 | } else if (edge & 2 != 0) { 112 | assert(a.lon != b.lon); 113 | return Point(bbox.maxLon, a.lat + (b.lat - a.lat) * (bbox.maxLon - a.lon) / (b.lon - a.lon)); // right 114 | } else if (edge & 1 != 0) { 115 | assert(a.lon != b.lon); 116 | return Point(bbox.minLon, a.lat + (b.lat - a.lat) * (bbox.minLon - a.lon) / (b.lon - a.lon)); // left 117 | } 118 | throw StateError('Segment does not intersect with the bbox.'); 119 | } 120 | 121 | /// Bit code reflects the point position relative to the bbox: 122 | /// 123 | /// ``` 124 | /// left mid right 125 | /// top 1001 1000 1010 126 | /// mid 0001 0000 0010 127 | /// bottom 0101 0100 0110 128 | /// ``` 129 | int _bitCode(Point p, BBox bbox) { 130 | int code = 0; 131 | 132 | if (p.lon < bbox.minLon) code |= 1; // left 133 | else if (p.lon > bbox.maxLon) code |= 2; // right 134 | 135 | if (p.lat < bbox.minLat) code |= 4; // bottom 136 | else if (p.lat > bbox.maxLat) code |= 8; // top 137 | 138 | return code; 139 | } 140 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Country Coder is a lightweight package that looks up region identifiers for geographic points 2 | without calling a server. It can code and convert between several common IDs: 3 | 4 | - 🆎 [ISO 3166-1 alpha-2 code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) (`ZA`) 5 | - 🔤 [ISO 3166-1 alpha-3 code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-3) (`ZAF`) 6 | - 3️⃣ [ISO 3166-1 numeric-3 code](https://en.wikipedia.org/wiki/ISO_3166-1_numeric) (`710`) 7 | - 3️⃣ [United Nations M49 code](https://en.wikipedia.org/wiki/UN_M49) (`710`) 8 | - 🌐 [Wikidata QID](https://www.wikidata.org/wiki/Q43649390) (`Q258`) 9 | - 🇺🇳 [Emoji flag](https://en.wikipedia.org/wiki/Regional_Indicator_Symbol) (🇿🇦) 10 | - 💻 [ccTLD (country code top-level domain)](https://en.wikipedia.org/wiki/Country_code_top-level_domain) (`.za`) 11 | 12 | In addition to identifiers, Country Coder can provide basic regional information: 13 | 14 | - ☎️ [Telephone Calling Codes](https://en.wikipedia.org/wiki/List_of_country_calling_codes) (+44) 15 | - 🛣 [Driving Side](https://en.wikipedia.org/wiki/Left-_and_right-hand_traffic) (right, left) 16 | - 🚗 [Traffic Speed Unit](https://en.wikipedia.org/wiki/Speed_limit#Signage) (km/h, mph) 17 | - 🚚 [Vehicle Height Unit](https://wiki.openstreetmap.org/wiki/Key:maxheight) (m, ft) 18 | - 🇪🇺 [European Union Membership](https://en.wikipedia.org/wiki/Member_state_of_the_European_Union) 19 | 20 | ## This Is A Port 21 | 22 | The original country coder and its data file was written by Quincy Morgan and Bryan Housel. 23 | See [its documentaion](https://github.com/ideditor/country-coder) for function reference 24 | and general description of what it can and cannot do. 25 | 26 | Note that `feature` in function names here was replaced with `region`. For example, instead 27 | of `featuresIn()`, the `CountryCoder` class has `regionsIn`. 28 | 29 | Also, geometries are not stored in `RegionFeature` objects that some of the methods return. 30 | This means, you get full information on a region, but not its boundaries. 31 | 32 | ## Usage 33 | 34 | You do not instantiate a `CountryCoder`, but use a static `instance` property. 35 | You should call `load()` once to initialize the instance. There is an option for 36 | asynchronous loading via `prepareData()` (see the API reference). 37 | 38 | ```dart 39 | final countries = CountryCoder.instance; 40 | countries.load(); // initialize the instance, does nothing the second time 41 | 42 | // Find a country's 2-letter ISO code by longitude and latitude 43 | final String? code = countries.iso1A2Code(lon: -4.5, lat: 54.2); 44 | 45 | // Get a full info on a country 46 | final gb = countries.region(query: 'UK'); 47 | assert(gb == countries.region(query: '.uk')); 48 | assert(gb == countries.region(lon: -4.5, lat: 54.2)); 49 | 50 | // Specify level if you need a sub-territory 51 | final im = countries.region(lon: -4.0, lat: 54.2, level: RegionLevel.territory); 52 | assert(im.name == 'Isle of Man'); 53 | 54 | // Useful convenience methods! 55 | assert(!countries.isInEuropeanUnion(query: 'GB')); 56 | 57 | // Which is equivalent to 58 | assert(countries.isIn(query: 'DE', outer: 'EU')); 59 | 60 | // And some country info 61 | assert(countries.drivingSide(query: 'UK') != countries.drivingSide(query: 'CH')); 62 | ``` 63 | 64 | ## Location Matcher 65 | 66 | When you have objects associated with groups of countries, you define these via a 67 | `LocationSet`. Usually you don't instantiate it, but read from a GeoJSON property. 68 | For a description of the `locationSet` structure, see the 69 | [location-conflation](https://github.com/ideditor/location-conflation#what-is-it) 70 | library documentation. All the features are supported, but this class does not produce 71 | any geometries. 72 | 73 | Use it like this: 74 | 75 | ```dart 76 | // You can pass additional GeoJSON features, with ids ending in ".geojson" 77 | final matcher = LocationMatcher(features); 78 | 79 | final locationSet = LocationSet.fromJson({ 80 | 'includes': ['uk'], 81 | 'excludes': ['im', [0.3, 51.5, 60]] 82 | }); 83 | 84 | bool inLondon = matcher(-0.11, 51.51, locationSet); // false 85 | bool inSheffield = matcher(-1.46, 53.28, locationSet); // true 86 | ``` 87 | 88 | ## Point-in-Polygon Index 89 | 90 | This library also includes a fast lookup engine for polygons containing a point or intersecting 91 | a bounding box. Its data structures are loosely based on GeoJSON geometries. In fact, passing 92 | a features list from a GeoJSON's `FeatureCollection` is the primary mode of its operation: 93 | 94 | ```dart 95 | final features = [ 96 | { 97 | 'geometry': {'type': 'Polygon', 'coordinates': [[[...]]]}, 98 | 'properties': {'id': 1, 'name': 'Something'} 99 | }, 100 | { 101 | 'geometry': {'type': 'MultiPolygon', 'coordinates': [[[...]]]}, 102 | 'properties': {'id': 2, 'name': 'Another one'} 103 | }, 104 | ]; 105 | 106 | final query = WhichPolygon(features); 107 | 108 | // Query the smallest polygon at a (longitude, latitude) location 109 | final name = query(-30, 41)?['name']; 110 | 111 | // Query all polygons at a location 112 | final names = query.all(-30, 41).map((p) => p['name']).toList(); 113 | 114 | // Query polygons in a bounding box 115 | final inBBox = query.bbox(-30, 41, -28, 51).length; 116 | ``` 117 | 118 | ## Upstream 119 | 120 | This library is a straight-up port of several JavaScript libraries: 121 | 122 | * [country-coder 5.2.1](https://github.com/ideditor/country-coder) by Quincy Morgan and Bryan Housel, ISC license. 123 | * [which-polygon 2.2.0](https://github.com/mapbox/which-polygon) by Vladimir Agafonkin, ISC license. 124 | * [lineclip 1.1.5](https://github.com/mapbox/lineclip) by Vladimir Agafonkin, ISC license. 125 | -------------------------------------------------------------------------------- /lib/src/which_polygon.dart: -------------------------------------------------------------------------------- 1 | // Port of https://github.com/mapbox/which-polygon v2.2.0 2 | // Code mainly by Vladimir Agafonkin, ported by Ilya Zverev. 3 | // Licensed ISC. 4 | 5 | import 'package:rbush/rbush.dart'; 6 | import 'bbox.dart'; 7 | import 'lineclip.dart'; 8 | import 'dart:math' as math; 9 | 10 | /// Index for matching points against a set of GeoJSON polygons. 11 | class WhichPolygon { 12 | final _tree = RBushBase<_TreeItem>( 13 | toBBox: (item) => item, 14 | getMinX: (item) => item.minX, 15 | getMinY: (item) => item.minY, 16 | ); 17 | 18 | /// Reads a GeoJSON-like collection of features, and 19 | /// stores all polygon and multipolygon geometries along 20 | /// with their properties in an r-tree. 21 | /// 22 | /// Note that properties can be of any type, marked with [T]. 23 | /// Sometimes you might want to preprocess a feature stream like this: 24 | /// 25 | /// ```dart 26 | /// final query = WhichPolygon(fc['features'].map((f) => { 27 | /// 'geometry': f['geometry'], 28 | /// 'properties': MyData.fromJson(f['properties']), 29 | /// }); 30 | /// ``` 31 | WhichPolygon(Iterable> features) { 32 | final List<_TreeItem> bboxes = []; 33 | for (final feature in features) { 34 | final Map? geom = feature['geometry']; 35 | if (geom == null || feature['properties'] == null) continue; 36 | 37 | if (feature['id'] != null && feature['properties'] is Map) { 38 | // Push id property into the properties map 39 | feature['properties']['id'] ??= feature['id']; 40 | } 41 | 42 | final List coords = geom['coordinates']; 43 | if (geom['type'] == 'Polygon') { 44 | bboxes.add(_TreeItem( 45 | _Polygon.fromJson(coords), 46 | feature['properties'], 47 | )); 48 | } else if (geom['type'] == 'MultiPolygon') { 49 | for (final List polygon in coords) { 50 | bboxes.add(_TreeItem( 51 | _Polygon.fromJson(polygon), 52 | feature['properties'], 53 | )); 54 | } 55 | } 56 | } 57 | _tree.load(bboxes); 58 | } 59 | 60 | /// Instantiates the class using an object from [serialize]. 61 | WhichPolygon.fromSerialized(dynamic data) { 62 | _tree.data = data; 63 | } 64 | 65 | /// Returns a serializable object that can be passed between threads. 66 | dynamic serialize() => _tree.data; 67 | 68 | /// Returns a single polygon matching the location. An alias for [one]. 69 | T? call(double lon, double lat) => one(lon, lat); 70 | 71 | /// Returns a single polygon containing the ([lon], [lat]) location. 72 | /// Or `null` if nothing is found. 73 | T? one(double lon, double lat) { 74 | final point = Point(lon, lat); 75 | final result = _findInTree(point: point); 76 | for (final item in result) { 77 | if (_insidePolygon(item.polygon, point)) { 78 | return item.props; 79 | } 80 | } 81 | return null; 82 | } 83 | 84 | /// Returns all polygons that contain the ([lon], [lat]) location. 85 | List all(double lon, double lat) { 86 | final List output = []; 87 | final point = Point(lon, lat); 88 | final result = _findInTree(point: point); 89 | for (final item in result) { 90 | if (_insidePolygon(item.polygon, point)) { 91 | output.add(item.props); 92 | } 93 | } 94 | return output; 95 | } 96 | 97 | /// Returns all polygons that intersect with the given bounding box. 98 | List bbox(double minLon, double minLat, double maxLon, double maxLat) { 99 | final List output = []; 100 | final bbox = BBox(minLon, minLat, maxLon, maxLat); 101 | final result = _findInTree(bbox: bbox); 102 | for (final item in result) { 103 | if (_polygonIntersectsBBox(item.polygon, bbox)) { 104 | output.add(item.props); 105 | } 106 | } 107 | return output; 108 | } 109 | 110 | List<_TreeItem> _findInTree({Point? point, BBox? bbox}) { 111 | RBushBox rbox; 112 | if (point != null) { 113 | rbox = RBushBox( 114 | minX: point.lon, minY: point.lat, maxX: point.lon, maxY: point.lat); 115 | } else { 116 | if (bbox == null) throw ArgumentError('point or bbox should not be null'); 117 | rbox = RBushBox( 118 | minX: bbox.minLon, 119 | minY: bbox.minLat, 120 | maxX: bbox.maxLon, 121 | maxY: bbox.maxLat); 122 | } 123 | return _tree.search(rbox); 124 | } 125 | 126 | static bool _polygonIntersectsBBox(_Polygon polygon, BBox bbox) { 127 | if (_insidePolygon(polygon, bbox.center)) return true; 128 | final lineclip = LineClip(); 129 | for (final ring in polygon.rings) { 130 | if (lineclip(ring, bbox).isNotEmpty) return true; 131 | } 132 | return false; 133 | } 134 | 135 | static bool _insidePolygon(_Polygon polygon, Point p) { 136 | bool inside = false; 137 | for (final ring in polygon.rings) { 138 | int j = 0; 139 | int len2 = ring.length; 140 | int k = len2 - 1; 141 | while (j < len2) { 142 | if (_rayIntersect(p, ring[j], ring[k])) { 143 | inside = !inside; 144 | } 145 | k = j++; 146 | } 147 | } 148 | return inside; 149 | } 150 | 151 | static bool _rayIntersect(Point p, Point p1, Point p2) { 152 | bool part1 = (p1.lat > p.lat && p2.lat <= p.lat) || 153 | (p1.lat <= p.lat && p2.lat > p.lat); 154 | bool part2 = p.lon < 155 | (p2.lon - p1.lon) * (p.lat - p1.lat) / (p2.lat - p1.lat) + p1.lon; 156 | return part1 && part2; 157 | } 158 | } 159 | 160 | class _Polygon { 161 | final List> rings; 162 | 163 | const _Polygon(this.rings); 164 | 165 | factory _Polygon.fromJson(List coords) { 166 | final List> rings = []; 167 | for (final List r in coords) { 168 | rings.add(r.map((pt) => Point.fromJson(pt)).toList()); 169 | } 170 | return _Polygon(rings); 171 | } 172 | 173 | List get outerRing => rings[0]; 174 | } 175 | 176 | class _TreeItem extends RBushBox { 177 | final _Polygon polygon; 178 | final T props; 179 | 180 | _TreeItem(this.polygon, this.props) { 181 | for (final p in polygon.outerRing) { 182 | minX = math.min(minX, p.lon); 183 | minY = math.min(minY, p.lat); 184 | maxX = math.max(maxX, p.lon); 185 | maxY = math.max(maxY, p.lat); 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /lib/src/region_feature.dart: -------------------------------------------------------------------------------- 1 | 2 | enum RegionLevel { 3 | /// Sark, Ascension Island, Diego Garcia, etc. 4 | subterritory, 5 | 6 | /// Puerto Rico, Gurnsey, Hong Kong, etc. 7 | territory, 8 | subcountryGroup, 9 | 10 | /// Ethiopia, Brazil, United States, etc. 11 | country, 12 | 13 | /// Great Britain, Macaronesia, Mariana Islands, etc. 14 | sharedLandform, 15 | 16 | /// Eastern Africa, South America, Channel Islands, etc. 17 | intermediateRegion, 18 | 19 | /// Sub-Saharan Africa, North America, Micronesia, etc. 20 | subregion, 21 | 22 | /// Africa, Americas, Antarctica, Asia, Europe, Oceania 23 | region, 24 | 25 | /// Outermost Regions of the EU, Overseas Countries and Territories of the EU 26 | subunion, 27 | 28 | /// European Union 29 | europeanUnion, 30 | 31 | /// United Nations 32 | unitedNations, 33 | 34 | /// all features 35 | world, 36 | } 37 | 38 | enum RegionIsoStatus { 39 | official, 40 | excReserved, 41 | userAssigned, 42 | } 43 | 44 | enum RegionDrivingSide { 45 | right, 46 | left, 47 | } 48 | 49 | enum RegionSpeedUnit { 50 | /// miles per hour 51 | mph, 52 | 53 | /// kilometers per hour 54 | kmh, 55 | } 56 | 57 | enum RegionHeightUnit { 58 | /// feet and inches 59 | feet, 60 | 61 | /// meters 62 | meters, 63 | } 64 | 65 | enum RegionProperties { 66 | iso1A2, 67 | iso1A3, 68 | iso1N3, 69 | m49, 70 | wikidata, 71 | emojiFlag, 72 | ccTLD, 73 | nameEn, 74 | aliases, 75 | country, 76 | groups, 77 | members, 78 | level, 79 | isoStatus, 80 | driveSide, 81 | roadSpeedUnit, 82 | roadHeightUnit, 83 | callingCodes, 84 | } 85 | 86 | class RegionFeature { 87 | /// Unique identifier specific to country-coder 88 | final String id; 89 | 90 | /// ISO 3166-1 alpha-2 code 91 | final String? iso1A2; 92 | 93 | /// ISO 3166-1 alpha-3 code 94 | final String? iso1A3; 95 | 96 | /// ISO 3166-1 numeric-3 code 97 | final String? iso1N3; 98 | 99 | /// UN M49 code 100 | String? m49; 101 | 102 | /// Wikidata QID 103 | final String wikidata; 104 | 105 | /// The emoji flag sequence derived from this feature's ISO 3166-1 alpha-2 code 106 | String? emojiFlag; 107 | 108 | /// The ccTLD (country code top-level domain) 109 | String? ccTLD; 110 | 111 | /// The common English name 112 | final String nameEn; 113 | 114 | /// Additional identifiers which can be used to look up this feature; 115 | /// these cannot collide with the identifiers for any other feature 116 | late final List aliases; 117 | 118 | /// For features entirely within a country, the ISO 3166-1 alpha-2 code for that country 119 | final String? country; 120 | 121 | /// The ISO 3166-1 alpha-2, M49, or QIDs of other features this feature is entirely within, including its country 122 | late final List groups; 123 | 124 | /// The ISO 3166-1 alpha-2, M49, or QIDs of other features this feature contains; 125 | /// the inverse of [groups] 126 | late final List members; 127 | 128 | /// The rough geographic type of this feature. 129 | /// Levels do not necessarily nest cleanly within each other. 130 | late final RegionLevel level; 131 | 132 | /// The status of this feature's ISO 3166-1 code(s), if any 133 | RegionIsoStatus? isoStatus; 134 | 135 | /// The side of the road that traffic drives on within this feature 136 | RegionDrivingSide? driveSide; 137 | 138 | /// The unit used for road traffic speeds within this feature 139 | RegionSpeedUnit? roadSpeedUnit; 140 | 141 | /// The unit used for road vehicle height restrictions within this feature 142 | RegionHeightUnit? roadHeightUnit; 143 | 144 | /// The international calling codes for this feature, sometimes including area codes 145 | /// e.g. `1`, `1 340` 146 | late List callingCodes; 147 | 148 | /// Whether this region has geometry. 149 | final bool hasGeometry; 150 | 151 | RegionFeature({ 152 | String? id, 153 | this.iso1A2, 154 | this.iso1A3, 155 | this.iso1N3, 156 | this.m49, 157 | required this.wikidata, 158 | this.emojiFlag, 159 | this.ccTLD, 160 | required this.nameEn, 161 | List? aliases, 162 | this.country, 163 | List? groups, 164 | List? members, 165 | RegionLevel? level, 166 | this.isoStatus, 167 | this.driveSide, 168 | this.roadSpeedUnit, 169 | this.roadHeightUnit, 170 | List? callingCodes, 171 | this.hasGeometry = true, 172 | }) : id = id ?? iso1A2 ?? m49 ?? wikidata { 173 | this.aliases = aliases ?? []; 174 | this.groups = groups ?? []; 175 | this.members = members ?? []; 176 | this.callingCodes = callingCodes ?? []; 177 | 178 | if (m49 == null && iso1N3 != null) { 179 | // M49 is a superset of ISO numerics so we only need to store one 180 | m49 = iso1N3; 181 | } 182 | 183 | if (level != RegionLevel.unitedNations) { 184 | if (ccTLD == null && iso1A2 != null) { 185 | // ccTLD is nearly the same as iso1A2, so we only need to explicitly code any exceptions 186 | ccTLD = '.${iso1A2!.toLowerCase()}'; 187 | } 188 | } 189 | 190 | if (level == null) { 191 | if (country == null) { 192 | level = RegionLevel.country; 193 | } else if (iso1A2 == null || isoStatus == RegionIsoStatus.official) { 194 | level = RegionLevel.territory; 195 | } else { 196 | level = RegionLevel.subterritory; 197 | } 198 | } 199 | this.level = level; 200 | 201 | if (country != null && hasGeometry) { 202 | this.groups.add(country!); 203 | } 204 | if (m49 != '001') { 205 | this.groups.add('001'); 206 | } 207 | 208 | if (iso1A2 != null) { 209 | // Calculates the emoji flag sequence from the alpha-2 code (if any) and caches it 210 | emojiFlag = String.fromCharCodes(iso1A2!.codeUnits.map((e) => e + 127397)); 211 | } 212 | } 213 | 214 | factory RegionFeature.fromJson(Map data, [bool hasGeometry = true]) { 215 | return RegionFeature( 216 | id: data['id'], 217 | iso1A2: data['iso1A2'], 218 | iso1A3: data['iso1A3'], 219 | iso1N3: data['iso1N3'], 220 | m49: data['m49'], 221 | wikidata: data['wikidata'], 222 | emojiFlag: data['emojiFlag'], 223 | ccTLD: data['ccTLD'], 224 | nameEn: data['nameEn'], 225 | aliases: data['aliases']?.whereType().toList(), 226 | country: data['country'], 227 | groups: data['groups']?.whereType().toList(), 228 | members: data['members']?.whereType().toList(), 229 | level: _levelFromString(data['level']), 230 | isoStatus: _isoStatusFromString(data['isoStatus']), 231 | driveSide: _driveSideFromString(data['driveSide']), 232 | roadSpeedUnit: _speedUnitFromString(data['roadSpeedUnit']), 233 | roadHeightUnit: _heightUnitFromString(data['roadHeightUnit']), 234 | callingCodes: data['callingCodes']?.whereType().toList(), 235 | hasGeometry: hasGeometry, 236 | ); 237 | } 238 | 239 | static RegionLevel? _levelFromString(String? level) { 240 | if (level == null) return null; 241 | switch (level) { 242 | case 'world': 243 | return RegionLevel.world; 244 | case 'unitedNations': 245 | return RegionLevel.unitedNations; 246 | case 'union': 247 | return RegionLevel.europeanUnion; 248 | case 'subunion': 249 | return RegionLevel.subunion; 250 | case 'region': 251 | return RegionLevel.region; 252 | case 'subregion': 253 | return RegionLevel.subregion; 254 | case 'intermediateRegion': 255 | return RegionLevel.intermediateRegion; 256 | case 'sharedLandform': 257 | return RegionLevel.sharedLandform; 258 | case 'country': 259 | return RegionLevel.country; 260 | case 'subcountryGroup': 261 | return RegionLevel.subcountryGroup; 262 | case 'territory': 263 | return RegionLevel.territory; 264 | case 'subterritory': 265 | return RegionLevel.subterritory; 266 | default: 267 | throw ArgumentError('Unknown level: $level'); 268 | } 269 | } 270 | 271 | static RegionIsoStatus? _isoStatusFromString(String? status) { 272 | switch (status) { 273 | case 'official': 274 | return RegionIsoStatus.official; 275 | case 'excRes': 276 | return RegionIsoStatus.excReserved; 277 | case 'usrAssn': 278 | return RegionIsoStatus.userAssigned; 279 | default: 280 | return null; 281 | } 282 | } 283 | 284 | static RegionDrivingSide? _driveSideFromString(String? side) { 285 | switch (side) { 286 | case 'left': 287 | return RegionDrivingSide.left; 288 | case 'right': 289 | return RegionDrivingSide.right; 290 | default: 291 | return null; 292 | } 293 | } 294 | 295 | static RegionSpeedUnit? _speedUnitFromString(String? unit) { 296 | switch (unit) { 297 | case 'mph': 298 | return RegionSpeedUnit.mph; 299 | case 'km/h': 300 | return RegionSpeedUnit.kmh; 301 | default: 302 | return null; 303 | } 304 | } 305 | 306 | static RegionHeightUnit? _heightUnitFromString(String? unit) { 307 | switch (unit) { 308 | case 'ft': 309 | return RegionHeightUnit.feet; 310 | case 'm': 311 | return RegionHeightUnit.meters; 312 | default: 313 | return null; 314 | } 315 | } 316 | 317 | List getFeatureIDs() { 318 | final List ids = []; 319 | if (iso1A2 != null) ids.add(iso1A2!); 320 | if (iso1A3 != null) ids.add(iso1A3!); 321 | if (m49 != null) ids.add(m49!); 322 | ids.add(wikidata); 323 | if (emojiFlag != null) ids.add(emojiFlag!); 324 | if (ccTLD != null) ids.add(ccTLD!); 325 | ids.add(nameEn); 326 | ids.addAll(aliases); 327 | return ids; 328 | } 329 | 330 | bool hasProperty(RegionProperties? prop) { 331 | if (prop == null) return true; 332 | switch (prop) { 333 | case RegionProperties.iso1A2: 334 | return iso1A2 != null; 335 | case RegionProperties.iso1A3: 336 | return iso1A3 != null; 337 | case RegionProperties.iso1N3: 338 | return iso1N3 != null; 339 | case RegionProperties.m49: 340 | return m49 != null; 341 | case RegionProperties.wikidata: 342 | return true; 343 | case RegionProperties.emojiFlag: 344 | return emojiFlag != null; 345 | case RegionProperties.ccTLD: 346 | return ccTLD != null; 347 | case RegionProperties.nameEn: 348 | return true; 349 | case RegionProperties.aliases: 350 | return aliases.isNotEmpty; 351 | case RegionProperties.country: 352 | return country != null; 353 | case RegionProperties.groups: 354 | return groups.isNotEmpty; 355 | case RegionProperties.members: 356 | return members.isNotEmpty; 357 | case RegionProperties.level: 358 | return true; 359 | case RegionProperties.isoStatus: 360 | return isoStatus != null; 361 | case RegionProperties.driveSide: 362 | return driveSide != null; 363 | case RegionProperties.roadSpeedUnit: 364 | return roadSpeedUnit != null; 365 | case RegionProperties.roadHeightUnit: 366 | return roadHeightUnit != null; 367 | case RegionProperties.callingCodes: 368 | return callingCodes.isNotEmpty; 369 | } 370 | } 371 | } 372 | -------------------------------------------------------------------------------- /lib/src/region_features.dart: -------------------------------------------------------------------------------- 1 | import 'region_feature.dart'; 2 | import 'which_polygon.dart'; 3 | 4 | class RegionFeatureCollection { 5 | late final List regions; 6 | final Map regionsByCode = {}; 7 | late final WhichPolygon whichPolygon; 8 | 9 | RegionFeatureCollection(Map featureCollection) { 10 | regions = []; 11 | final geometries = >[]; 12 | 13 | for (final feature in featureCollection['features']) { 14 | final geometry = feature['geometry']; 15 | final region = 16 | RegionFeature.fromJson(feature['properties'], geometry != null); 17 | regions.add(region); 18 | if (geometry != null) { 19 | geometries.add({ 20 | 'geometry': geometry, 21 | 'properties': region, 22 | }); 23 | } 24 | } 25 | 26 | _postProcessRegions(); 27 | 28 | whichPolygon = WhichPolygon(geometries); 29 | } 30 | 31 | /// Restores inner structures from the serialized data. 32 | RegionFeatureCollection.fromSerialized(List data) { 33 | regions = data[0]; 34 | for (final region in regions) _cacheFeatureByIDs(region); 35 | whichPolygon = WhichPolygon.fromSerialized(data[1]); 36 | } 37 | 38 | List serialize() { 39 | return [regions, whichPolygon.serialize()]; 40 | } 41 | 42 | /// Returns the smallest feature of any kind containing the point, if any. 43 | RegionFeature? smallestRegion(double lon, double lat) { 44 | final region = whichPolygon(lon, lat); 45 | return region == null ? null : regionsByCode[region.id]; 46 | } 47 | 48 | /// Returns the country feature containing `loc`, if any. 49 | RegionFeature? countryRegion(double lon, double lat) { 50 | final region = smallestRegion(lon, lat); 51 | if (region == null) return null; 52 | // a feature without `country` but with geometry is itself a country 53 | final countryCode = region.country ?? region.iso1A2; 54 | return regionsByCode[countryCode]; 55 | } 56 | 57 | /// Returns the feature containing `loc` for the `opts`, if any. 58 | RegionFeature? regionForLoc(double lon, double lat, 59 | {RegionLevel? level, RegionLevel? maxLevel, RegionProperties? withProp}) { 60 | final targetLevel = level ?? RegionLevel.country; 61 | maxLevel ??= RegionLevel.world; 62 | 63 | if (maxLevel.index < targetLevel.index) return null; 64 | 65 | if (targetLevel == RegionLevel.country) { 66 | // attempt fast path for country-level coding 67 | final fastRegion = countryRegion(lon, lat); 68 | if (fastRegion != null && fastRegion.hasProperty(withProp)) { 69 | return fastRegion; 70 | } 71 | } 72 | 73 | final regions = regionsContaining(lon: lon, lat: lat); 74 | 75 | for (final region in regions) { 76 | if (region.level == targetLevel || 77 | (region.level.index > targetLevel.index && 78 | region.level.index <= maxLevel.index)) { 79 | if (region.hasProperty(withProp)) return region; 80 | } 81 | } 82 | return null; 83 | } 84 | 85 | RegionFeature? regionForID(dynamic id) { 86 | if (id == null) return null; 87 | String sid; 88 | if (id is int) { 89 | sid = id.toString().padLeft(3, '0'); 90 | } else { 91 | sid = _canonicalID(id.toString()); 92 | } 93 | return regionsByCode[sid]; 94 | } 95 | 96 | List smallestRegionsForBBox( 97 | double minLon, double minLat, double maxLon, double maxLat) { 98 | return whichPolygon 99 | .bbox(minLon, minLat, maxLon, maxLat) 100 | .map((e) => regionsByCode[e.id]!) 101 | .toList(); 102 | } 103 | 104 | List regionsContaining( 105 | {double? lon, 106 | double? lat, 107 | List? bbox, 108 | dynamic query, 109 | bool strict = false}) { 110 | List matching; 111 | if (bbox != null) { 112 | assert(bbox.length == 4); 113 | matching = smallestRegionsForBBox(bbox[0], bbox[1], bbox[2], bbox[3]); 114 | } else if (lon != null && lat != null) { 115 | final region = smallestRegion(lon, lat); 116 | matching = region == null ? [] : [region]; 117 | } else if (query != null) { 118 | final region = regionForID(query); 119 | matching = region == null ? [] : [region]; 120 | } else { 121 | throw ArgumentError('Please specify either location, bbox or query.'); 122 | } 123 | 124 | if (matching.isEmpty) return matching; 125 | 126 | List result; 127 | 128 | if (!strict || lat != null) { 129 | result = List.of(matching); 130 | } else { 131 | result = []; 132 | } 133 | 134 | for (final region in matching) { 135 | for (final groupId in region.groups) { 136 | final groupFeature = regionsByCode[groupId]!; 137 | if (!result.contains(groupFeature)) { 138 | result.add(groupFeature); 139 | } 140 | } 141 | } 142 | 143 | return result; 144 | } 145 | 146 | /// Returns the region matching [id] and all regions it contains, if any. 147 | /// If passing `true` for [strict], an exact match will not be included. 148 | List regionsIn(dynamic id, [bool strict = false]) { 149 | final region = regionForID(id); 150 | if (region == null) return []; 151 | 152 | final List result = []; 153 | 154 | if (!strict) result.add(region); 155 | 156 | for (final memberId in region.members) { 157 | result.add(regionsByCode[memberId]!); 158 | } 159 | 160 | return result; 161 | } 162 | 163 | _postProcessRegions() { 164 | for (final r in regions) { 165 | _cacheFeatureByIDs(r); 166 | } 167 | 168 | // Must load `members` only after fully loading `featuresByID` 169 | for (final r in regions) { 170 | // ensure all groups are listed by their ID 171 | for (int i = 0; i < r.groups.length; i++) { 172 | r.groups[i] = regionsByCode[r.groups[i]]!.id; 173 | } 174 | } 175 | // Populate `members` as the inverse relationship of `groups` 176 | for (final r in regions) { 177 | for (final g in r.groups) { 178 | final groupFeature = regionsByCode[g]!; 179 | groupFeature.members.add(r.id); 180 | } 181 | } 182 | 183 | // Must load attributes only after loading geometry features into `members` 184 | for (final r in regions) { 185 | _loadSpeedUnit(r); 186 | _loadHeightUnit(r); 187 | _loadDrivingSide(r); 188 | _loadCallingCodes(r); 189 | _loadGroupGroups(r); 190 | } 191 | 192 | for (final r in regions) { 193 | r.groups.sort((id1, id2) { 194 | return regionsByCode[id1]! 195 | .level 196 | .index 197 | .compareTo(regionsByCode[id2]!.level.index); 198 | }); 199 | if (r.members.isNotEmpty) { 200 | r.members.sort((id1, id2) { 201 | int res = regionsByCode[id1]! 202 | .level 203 | .index 204 | .compareTo(regionsByCode[id2]!.level.index); 205 | if (res == 0) { 206 | res = regions 207 | .indexOf(regionsByCode[id1]!) 208 | .compareTo(regions.indexOf(regionsByCode[id2]!)); 209 | } 210 | return res; 211 | }); 212 | } 213 | } 214 | } 215 | 216 | /// Caches features by their identifying strings for rapid lookup 217 | _cacheFeatureByIDs(RegionFeature region) { 218 | for (final id in region.getFeatureIDs()) { 219 | regionsByCode[_canonicalID(id)] = region; 220 | } 221 | } 222 | 223 | static final _kIdFilterRegex = RegExp( 224 | r"(?=(?!^(and|the|of|el|la|de)$))(\b(and|the|of|el|la|de)\b)|[-_ .,'()&[\]/]", 225 | caseSensitive: false, 226 | ); 227 | 228 | String _canonicalID(String id) { 229 | return id.isEmpty || id[0] == '.' 230 | ? id.toUpperCase() 231 | : id.replaceAll(_kIdFilterRegex, '').toUpperCase(); 232 | } 233 | 234 | _loadGroupGroups(RegionFeature region) { 235 | if (region.hasGeometry || region.members.isEmpty) return; 236 | final int levelIndex = region.level.index; 237 | List sharedGroups = []; 238 | for (final memberId in region.members) { 239 | final member = regionsByCode[memberId]!; 240 | final memberGroups = member.groups.where((groupId) { 241 | return groupId != region.id && 242 | levelIndex < regionsByCode[groupId]!.level.index; 243 | }).toList(); 244 | if (memberId == region.members.first) { 245 | sharedGroups = memberGroups; 246 | } else { 247 | sharedGroups.retainWhere((groupId) => memberGroups.contains(groupId)); 248 | } 249 | } 250 | 251 | region.groups.addAll( 252 | sharedGroups.where((groupId) => !region.groups.contains(groupId))); 253 | 254 | for (final groupId in sharedGroups) { 255 | final groupFeature = regionsByCode[groupId]!; 256 | if (!groupFeature.members.contains(region.id)) 257 | groupFeature.members.add(region.id); 258 | } 259 | } 260 | 261 | _loadSpeedUnit(RegionFeature region) { 262 | if (region.hasGeometry) { 263 | // only `mph` regions are listed explicitly, else assume `km/h` 264 | if (region.roadSpeedUnit == null) 265 | region.roadSpeedUnit = RegionSpeedUnit.kmh; 266 | } else { 267 | final values = Set.of(region.members.map((id) { 268 | final member = regionsByCode[id]!; 269 | if (member.hasGeometry) 270 | return member.roadSpeedUnit ?? RegionSpeedUnit.kmh; 271 | }).whereType()); 272 | 273 | // if all members have the same value then that's also the value for this feature 274 | if (values.length == 1) region.roadSpeedUnit = values.first; 275 | } 276 | } 277 | 278 | _loadHeightUnit(RegionFeature region) { 279 | if (region.hasGeometry) { 280 | // only `ft` regions are listed explicitly, else assume `m` 281 | if (region.roadHeightUnit == null) 282 | region.roadHeightUnit = RegionHeightUnit.meters; 283 | } else { 284 | final values = Set.of(region.members.map((id) { 285 | final member = regionsByCode[id]!; 286 | if (member.hasGeometry) 287 | return member.roadHeightUnit ?? RegionHeightUnit.meters; 288 | }).whereType()); 289 | 290 | // if all members have the same value then that's also the value for this feature 291 | if (values.length == 1) region.roadHeightUnit = values.first; 292 | } 293 | } 294 | 295 | _loadDrivingSide(RegionFeature region) { 296 | if (region.hasGeometry) { 297 | // only `left` regions are listed explicitly, else assume `right` 298 | if (region.driveSide == null) region.driveSide = RegionDrivingSide.right; 299 | } else { 300 | final values = Set.of(region.members.map((id) { 301 | final member = regionsByCode[id]!; 302 | if (member.hasGeometry) 303 | return member.driveSide ?? RegionDrivingSide.right; 304 | }).whereType()); 305 | 306 | // if all members have the same value then that's also the value for this feature 307 | if (values.length == 1) region.driveSide = values.first; 308 | } 309 | } 310 | 311 | _loadCallingCodes(RegionFeature region) { 312 | if (!region.hasGeometry && region.members.isNotEmpty) { 313 | region.callingCodes = 314 | Set.of(region.members.fold>([], (array, id) { 315 | final member = regionsByCode[id]!; 316 | if (member.hasGeometry && member.callingCodes.isNotEmpty) { 317 | return array + member.callingCodes; 318 | } 319 | return array; 320 | })).toList(); 321 | } 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /lib/src/country_coder.dart: -------------------------------------------------------------------------------- 1 | import 'data/borders.dart'; 2 | import 'region_feature.dart'; 3 | import 'region_features.dart'; 4 | 5 | import 'dart:convert'; 6 | 7 | /// An offline geocoder for countries and other big territories. 8 | /// 9 | /// See also [LocationMatcher] for matching a location against multiple 10 | /// regions, and [WhichPolygon] for a general point-in-polygon index. 11 | class CountryCoder { 12 | RegionFeatureCollection? _borders; 13 | 14 | /// Returns a singleton instance of [CountryCoder]. 15 | /// Note that you must initialize it using either [load] 16 | /// or [loadAsync] before using. 17 | /// Check for [ready] property to know when it's loaded. 18 | static final CountryCoder instance = CountryCoder._(); 19 | 20 | // Prevent instantiating this class. 21 | CountryCoder._() {} 22 | 23 | /// Synchronously loads the border data and builds trees 24 | /// out of it. This can take up to a second, so in applications 25 | /// try calling [prepareData] in an non-UI thread first. 26 | CountryCoder load([List? prepared]) { 27 | if (!ready) { 28 | if (prepared != null) { 29 | _borders = RegionFeatureCollection.fromSerialized(prepared); 30 | } else { 31 | final data = JsonDecoder().convert(bordersRaw); 32 | _borders = RegionFeatureCollection(data); 33 | } 34 | } 35 | return this; 36 | } 37 | 38 | /// Loads the border data separately and returns it in a serialized form. 39 | /// Call this from inside the `compute()` function to process data 40 | /// in a background thread, and then pass the result to [load]. Like this: 41 | /// 42 | /// ```dart 43 | /// import 'package:flutter/foundation.dart' show compute; 44 | /// 45 | /// // ... 46 | /// CountryCoder.instance.load(await compute(CountryCoder.prepareData, null)); 47 | /// ``` 48 | static List prepareData(_) { 49 | final data = JsonDecoder().convert(bordersRaw); 50 | return RegionFeatureCollection(data).serialize(); 51 | } 52 | 53 | /// Returns `true` when the data can be used. 54 | bool get ready => _borders != null; 55 | 56 | /// Throws an exception in case the data hasn't been loaded. 57 | _checkReady() { 58 | if (_borders == null) 59 | throw StateError( 60 | 'Please call load() and wait until ready == true before using CountryCoder.'); 61 | } 62 | 63 | /// Returns the smallest region enclosing a ([lon], [lat]) location, 64 | /// or a region that matches the query string / number. 65 | RegionFeature? smallestOrMatchingRegion( 66 | {double? lon, double? lat, dynamic query}) { 67 | _checkReady(); 68 | if (lat != null && lon != null) { 69 | return _borders!.smallestRegion(lon, lat); 70 | } else if (query != null) { 71 | return _borders!.regionForID(query); 72 | } else { 73 | throw ArgumentError('Please specify either location or query.'); 74 | } 75 | } 76 | 77 | /// Returns the region matching the given query string / number, 78 | /// or the one containing the ([lon], [lat]) location and 79 | /// matching the [level], [maxLevel], and [withProp] filters. 80 | /// Default [level] is _country_. 81 | RegionFeature? region( 82 | {double? lon, 83 | double? lat, 84 | dynamic query, 85 | RegionLevel? level, 86 | RegionLevel? maxLevel, 87 | RegionProperties? withProp}) { 88 | _checkReady(); 89 | if (lat != null && lon != null) { 90 | return _borders!.regionForLoc(lon, lat, 91 | level: level, maxLevel: maxLevel, withProp: withProp); 92 | } else if (query != null) { 93 | return _borders!.regionForID(query); 94 | } else { 95 | throw ArgumentError('Please specify either location or query.'); 96 | } 97 | } 98 | 99 | /// Returns a list of regions that either contain the given ([lon], [lat]) 100 | /// location, or intersect with a given [bbox] (which is `minLon`, `minLat`, 101 | /// `maxLon`, `maxLat`), or contain the region matching the [query] 102 | /// string / number. The result includes the entire region hierarchy, including 103 | /// the `001` region for the entire world. 104 | List regionsContaining( 105 | {double? lon, 106 | double? lat, 107 | List? bbox, 108 | dynamic query, 109 | bool strict = false}) { 110 | _checkReady(); 111 | return _borders!.regionsContaining( 112 | lon: lon, lat: lat, bbox: bbox, query: query, strict: strict); 113 | } 114 | 115 | /// Returns the ISO 3166-1 alpha-2 code for the region matching the arguments, if any. 116 | String? iso1A2Code( 117 | {double? lat, 118 | double? lon, 119 | dynamic query, 120 | RegionLevel? level, 121 | RegionLevel? maxLevel}) { 122 | return region( 123 | lat: lat, 124 | lon: lon, 125 | query: query, 126 | level: level, 127 | maxLevel: maxLevel, 128 | withProp: RegionProperties.iso1A2, 129 | )?.iso1A2; 130 | } 131 | 132 | /// Returns the ISO 3166-1 alpha-3 code for the region matching the arguments, if any. 133 | String? iso1A3Code( 134 | {double? lat, 135 | double? lon, 136 | dynamic query, 137 | RegionLevel? level, 138 | RegionLevel? maxLevel}) { 139 | return region( 140 | lat: lat, 141 | lon: lon, 142 | query: query, 143 | level: level, 144 | maxLevel: maxLevel, 145 | withProp: RegionProperties.iso1A3, 146 | )?.iso1A3; 147 | } 148 | 149 | /// Returns the ISO 3166-1 numeric-3 code for the region matching the arguments, if any. 150 | String? iso1N3Code( 151 | {double? lat, 152 | double? lon, 153 | dynamic query, 154 | RegionLevel? level, 155 | RegionLevel? maxLevel}) { 156 | return region( 157 | lat: lat, 158 | lon: lon, 159 | query: query, 160 | level: level, 161 | maxLevel: maxLevel, 162 | withProp: RegionProperties.iso1N3, 163 | )?.iso1N3; 164 | } 165 | 166 | /// Returns the UN M49 code for the region matching the arguments, if any. 167 | String? m49Code( 168 | {double? lat, 169 | double? lon, 170 | dynamic query, 171 | RegionLevel? level, 172 | RegionLevel? maxLevel}) { 173 | return region( 174 | lat: lat, 175 | lon: lon, 176 | query: query, 177 | level: level, 178 | maxLevel: maxLevel, 179 | withProp: RegionProperties.m49, 180 | )?.m49; 181 | } 182 | 183 | /// Returns the Wikidata QID code for the region matching the arguments, if any. 184 | String? wikidataQID( 185 | {double? lat, 186 | double? lon, 187 | dynamic query, 188 | RegionLevel? level, 189 | RegionLevel? maxLevel}) { 190 | return region( 191 | lat: lat, 192 | lon: lon, 193 | query: query, 194 | level: level, 195 | maxLevel: maxLevel, 196 | withProp: RegionProperties.wikidata, 197 | )?.wikidata; 198 | } 199 | 200 | /// Returns the emoji flag sequence for the region matching the arguments, if any. 201 | String? emojiFlag( 202 | {double? lat, 203 | double? lon, 204 | dynamic query, 205 | RegionLevel? level, 206 | RegionLevel? maxLevel}) { 207 | return region( 208 | lat: lat, 209 | lon: lon, 210 | query: query, 211 | level: level, 212 | maxLevel: maxLevel, 213 | withProp: RegionProperties.emojiFlag, 214 | )?.emojiFlag; 215 | } 216 | 217 | /// Returns the ccTLD (country code top-level domain) for the region 218 | /// matching the arguments, if any. 219 | String? ccTLD( 220 | {double? lat, 221 | double? lon, 222 | dynamic query, 223 | RegionLevel? level, 224 | RegionLevel? maxLevel}) { 225 | return region( 226 | lat: lat, 227 | lon: lon, 228 | query: query, 229 | level: level, 230 | maxLevel: maxLevel, 231 | withProp: RegionProperties.ccTLD, 232 | )?.ccTLD; 233 | } 234 | 235 | List _propertiesForQuery(double? lon, double? lat, List? bbox, 236 | String? Function(RegionFeature r) property) { 237 | return regionsContaining(lon: lon, lat: lat, bbox: bbox, strict: false) 238 | .map(property) 239 | .whereType() 240 | .toList(); 241 | } 242 | 243 | /// Returns all the ISO 3166-1 alpha-2 codes of regions at the location. 244 | List iso1A2Codes(double? lon, double? lat, List? bbox) { 245 | return _propertiesForQuery(lon, lat, bbox, (r) => r.iso1A2); 246 | } 247 | 248 | /// Returns all the ISO 3166-1 alpha-3 codes of regions at the location. 249 | List iso1A3Codes(double? lon, double? lat, List? bbox) { 250 | return _propertiesForQuery(lon, lat, bbox, (r) => r.iso1A3); 251 | } 252 | 253 | /// Returns all the ISO 3166-1 numeric-3 codes of regions at the location. 254 | List iso1N3Codes(double? lon, double? lat, List? bbox) { 255 | return _propertiesForQuery(lon, lat, bbox, (r) => r.iso1N3); 256 | } 257 | 258 | /// Returns all the UN M49 codes of regions at the location. 259 | List m49Codes(double? lon, double? lat, List? bbox) { 260 | return _propertiesForQuery(lon, lat, bbox, (r) => r.m49); 261 | } 262 | 263 | /// Returns all the Wikidata QIDs of regions at the location. 264 | List wikidataQIDs(double? lon, double? lat, List? bbox) { 265 | return _propertiesForQuery(lon, lat, bbox, (r) => r.wikidata); 266 | } 267 | 268 | /// Returns all the emoji flag sequences of regions at the location. 269 | List emojiFlags(double? lon, double? lat, List? bbox) { 270 | return _propertiesForQuery(lon, lat, bbox, (r) => r.emojiFlag); 271 | } 272 | 273 | /// Returns all the ccTLD (country code top-level domain) sequences 274 | /// of regions at the location. 275 | List ccTLDs(double? lon, double? lat, List? bbox) { 276 | return _propertiesForQuery(lon, lat, bbox, (r) => r.ccTLD); 277 | } 278 | 279 | /// Returns the region matching [id] and all regions it contains, if any. 280 | /// If passing `true` for [strict], an exact match will not be included. 281 | List regionsIn(dynamic id, [bool strict = false]) { 282 | _checkReady(); 283 | return _borders!.regionsIn(id, strict); 284 | } 285 | 286 | // Returns true if the feature matching [query] is, or is a part of, 287 | // the feature matching [bounds]. 288 | bool isIn({double? lon, double? lat, dynamic query, dynamic inside}) { 289 | final queried = smallestOrMatchingRegion(lon: lon, lat: lat, query: query); 290 | final boundsRegion = region(query: inside); 291 | 292 | if (queried == null) return false; // Outside any of the regions. 293 | if (boundsRegion == null) 294 | throw StateError('Could not find bounds by query $inside'); 295 | 296 | if (queried.id == boundsRegion.id) return true; 297 | return queried.groups.contains(boundsRegion.id); 298 | } 299 | 300 | /// Returns true if the region matching [query] is within EU jurisdiction. 301 | bool isInEuropeanUnion({double? lon, double? lat, dynamic query}) { 302 | return isIn(lon: lon, lat: lat, query: query, inside: 'EU'); 303 | } 304 | 305 | /// Returns true if the region matching [query] is, or is within, 306 | /// a United Nations member state. 307 | bool isInUnitedNations({double? lon, double? lat, dynamic query}) { 308 | return isIn(lon: lon, lat: lat, query: query, inside: 'UN'); 309 | } 310 | 311 | /// Returns the side traffic drives on in the region matching [query]. 312 | RegionDrivingSide? drivingSide({double? lon, double? lat, dynamic query}) { 313 | final region = smallestOrMatchingRegion(lon: lon, lat: lat, query: query); 314 | return region?.driveSide; 315 | } 316 | 317 | /// Returns the road speed unit for the region matching [query]. 318 | RegionSpeedUnit? roadSpeedUnit({double? lon, double? lat, dynamic query}) { 319 | final region = smallestOrMatchingRegion(lon: lon, lat: lat, query: query); 320 | return region?.roadSpeedUnit; 321 | } 322 | 323 | /// Returns the road vehicle height restriction unit for the region matching [query]. 324 | RegionHeightUnit? roadHeightUnit({double? lon, double? lat, dynamic query}) { 325 | final region = smallestOrMatchingRegion(lon: lon, lat: lat, query: query); 326 | return region?.roadHeightUnit; 327 | } 328 | 329 | /// Returns the full international calling codes for phone numbers in 330 | /// the region matching [query], if any. 331 | List callingCodes({double? lon, double? lat, dynamic query}) { 332 | final region = smallestOrMatchingRegion(lon: lon, lat: lat, query: query); 333 | return region?.callingCodes ?? []; 334 | } 335 | } 336 | -------------------------------------------------------------------------------- /test/fixtures/states.dart: -------------------------------------------------------------------------------- 1 | final states = {"type":"FeatureCollection","features":[{"type":"Feature","properties":{"name":"Minnesota","postal":"MN"},"geometry":{"type":"Polygon","coordinates":[[[-89.614,47.819],[-89.728,47.642],[-89.843,47.465],[-89.958,47.287],[-90.132,47.293],[-90.306,47.298],[-90.48,47.304],[-90.654,47.309],[-90.858,47.213],[-91.061,47.117],[-91.265,47.021],[-91.468,46.925],[-91.592,46.876],[-91.717,46.828],[-91.841,46.778],[-91.965,46.73],[-92.012,46.712],[-92.275,46.656],[-92.265,46.095],[-92.297,46.096],[-92.544,45.986],[-92.757,45.89],[-92.9,45.706],[-92.689,45.518],[-92.765,45.267],[-92.766,44.996],[-92.797,44.776],[-92.505,44.584],[-92.385,44.575],[-92.062,44.433],[-91.95,44.365],[-91.88,44.257],[-91.628,44.085],[-91.29,43.937],[-91.257,43.855],[-91.255,43.614],[-91.228,43.501],[-92.54,43.52],[-94.001,43.513],[-95.36,43.5],[-96.453,43.502],[-96.439,44.436],[-96.561,45.393],[-96.736,45.471],[-96.835,45.625],[-96.781,45.761],[-96.557,45.872],[-96.539,46.018],[-96.539,46.199],[-96.601,46.351],[-96.685,46.513],[-96.734,46.716],[-96.746,46.945],[-96.78,46.999],[-96.82,47.292],[-96.825,47.427],[-96.844,47.546],[-96.894,47.749],[-97.015,47.954],[-97.131,48.137],[-97.149,48.319],[-97.161,48.515],[-97.127,48.642],[-97.12,48.759],[-97.214,48.902],[-97.229,49.001],[-95.159,49],[-95.156,49.384],[-94.818,49.389],[-94.64,48.84],[-94.329,48.671],[-93.631,48.609],[-92.61,48.45],[-91.64,48.14],[-90.83,48.27],[-89.6,48.01],[-89.599,48.01],[-89.49028126070573,48.012965056526205],[-89.489,48.013],[-89.523,47.961],[-89.614,47.819]]]}}, 2 | {"type":"Feature","properties":{"name":"Montana","postal":"MT"},"geometry":{"type":"Polygon","coordinates":[[[-116.048,49],[-113.06,49.001],[-110.071,49.003],[-107.082,49.005],[-104.093,49.006],[-104.077,47.172],[-104.027,45.957],[-104.078,45.041],[-105.746,45.051],[-107.547,45.046],[-109.102,45.057],[-111.071,45.05],[-111.067,44.542],[-111.085,44.506],[-111.194,44.561],[-111.292,44.701],[-111.4,44.729],[-111.542,44.53],[-111.771,44.498],[-112.336,44.561],[-112.363,44.462],[-112.69,44.499],[-112.875,44.36],[-113.052,44.62],[-113.175,44.765],[-113.379,44.79],[-113.439,44.863],[-113.503,45.124],[-113.68,45.249],[-113.794,45.565],[-113.914,45.703],[-114.036,45.73],[-114.138,45.589],[-114.335,45.47],[-114.514,45.569],[-114.524,45.825],[-114.407,45.89],[-114.491,46.147],[-114.394,46.41],[-114.285,46.632],[-114.586,46.641],[-114.843,46.786],[-115.122,47.095],[-115.288,47.25],[-115.519,47.345],[-115.705,47.505],[-115.704,47.685],[-115.968,47.95],[-116.048,49]]]}}, 3 | {"type":"Feature","properties":{"name":"North Dakota","postal":"ND"},"geometry":{"type":"Polygon","coordinates":[[[-96.539,46.018],[-98.442,45.963],[-100.067,45.966],[-102.117,45.961],[-104.027,45.957],[-104.077,47.172],[-104.093,49.006],[-100.63,49.003],[-97.229,49.001],[-97.214,48.902],[-97.12,48.759],[-97.127,48.642],[-97.161,48.515],[-97.149,48.319],[-97.131,48.137],[-97.015,47.954],[-96.894,47.749],[-96.844,47.546],[-96.825,47.427],[-96.82,47.292],[-96.78,46.999],[-96.746,46.945],[-96.734,46.716],[-96.685,46.513],[-96.601,46.351],[-96.539,46.199],[-96.539,46.018]]]}}, 4 | {"type":"Feature","properties":{"name":"Hawaii","postal":"HI"},"geometry":{"type":"MultiPolygon","coordinates":[[[[-155.937,19.059],[-155.908,19.339],[-156.073,19.703],[-156.024,19.814],[-155.85,19.977],[-155.919,20.174],[-155.861,20.267],[-155.785,20.249],[-155.402,20.08],[-155.225,19.993],[-155.062,19.859],[-154.807,19.509],[-154.831,19.453],[-155.222,19.24],[-155.542,19.083],[-155.688,18.916],[-155.937,19.059]]],[[[-155.996,20.764],[-156.079,20.644],[-156.414,20.572],[-156.587,20.783],[-156.702,20.864],[-156.711,20.927],[-156.613,21.012],[-156.257,20.917],[-155.996,20.764]]],[[[-156.758,21.177],[-156.789,21.069],[-157.325,21.098],[-157.25,21.22],[-156.758,21.177]]],[[[-158.127,21.312],[-158.254,21.539],[-158.293,21.579],[-158.025,21.717],[-157.942,21.653],[-157.653,21.322],[-157.707,21.264],[-157.779,21.277],[-158.127,21.312]]],[[[-159.801,22.065],[-159.749,22.138],[-159.596,22.236],[-159.366,22.215],[-159.345,21.982],[-159.464,21.883],[-159.801,22.065]]]]}}, 5 | {"type":"Feature","properties":{"name":"Idaho","postal":"ID"},"geometry":{"type":"Polygon","coordinates":[[[-111.05,42.002],[-114.034,41.993],[-117.028,42],[-117.014,43.797],[-116.927,44.081],[-117.008,44.211],[-117.194,44.279],[-117.192,44.439],[-117.052,44.666],[-116.836,44.864],[-116.693,45.187],[-116.559,45.444],[-116.458,45.575],[-116.511,45.726],[-116.679,45.807],[-116.915,46],[-116.907,46.178],[-116.998,46.33],[-117.027,47.723],[-117.031,48.999],[-116.048,49],[-115.968,47.95],[-115.704,47.685],[-115.705,47.505],[-115.519,47.345],[-115.288,47.25],[-115.122,47.095],[-114.843,46.786],[-114.586,46.641],[-114.285,46.632],[-114.394,46.41],[-114.491,46.147],[-114.407,45.89],[-114.524,45.825],[-114.514,45.569],[-114.335,45.47],[-114.138,45.589],[-114.036,45.73],[-113.914,45.703],[-113.794,45.565],[-113.68,45.249],[-113.503,45.124],[-113.439,44.863],[-113.379,44.79],[-113.175,44.765],[-113.052,44.62],[-112.875,44.36],[-112.69,44.499],[-112.363,44.462],[-112.336,44.561],[-111.771,44.498],[-111.542,44.53],[-111.4,44.729],[-111.292,44.701],[-111.194,44.561],[-111.085,44.506],[-111.05,44.488],[-111.05,42.002]]]}}, 6 | {"type":"Feature","properties":{"name":"Washington","postal":"WA"},"geometry":{"type":"Polygon","coordinates":[[[-116.915,46],[-118.978,45.993],[-119.337,45.888],[-119.592,45.932],[-119.787,45.851],[-120.158,45.741],[-120.61,45.754],[-120.837,45.673],[-121.047,45.637],[-121.204,45.69],[-121.561,45.733],[-121.788,45.701],[-122.175,45.595],[-122.42,45.592],[-122.652,45.63],[-122.726,45.77],[-122.752,45.938],[-122.938,46.129],[-123.228,46.186],[-123.471,46.277],[-123.777,46.282],[-123.999,46.283],[-124.08,46.865],[-124.396,47.72],[-124.687,48.184],[-124.566,48.38],[-123.12,48.04],[-122.587,47.096],[-122.34,47.36],[-122.5,48.18],[-122.84,49],[-120.003,49.001],[-117.031,48.999],[-117.027,47.723],[-116.998,46.33],[-116.907,46.178],[-116.915,46]]]}}, 7 | {"type":"Feature","properties":{"name":"Arizona","postal":"AZ"},"geometry":{"type":"Polygon","coordinates":[[[-109.045,37],[-109.044,31.342],[-111.024,31.335],[-113.305,32.039],[-114.815,32.525],[-114.721,32.721],[-114.59,32.716],[-114.48,32.916],[-114.656,33.054],[-114.691,33.204],[-114.743,33.38],[-114.549,33.61],[-114.469,34.067],[-114.166,34.273],[-114.355,34.465],[-114.485,34.653],[-114.567,34.828],[-114.622,34.964],[-114.642,35.053],[-114.581,35.249],[-114.63,35.445],[-114.651,35.639],[-114.65,35.854],[-114.74,35.991],[-114.671,36.115],[-114.461,36.115],[-114.269,36.044],[-114.133,36.004],[-114.066,36.156],[-114.024,36.19],[-114.031,36.994],[-112.418,37.009],[-110.496,37.007],[-109.045,37]]]}}, 8 | {"type":"Feature","properties":{"name":"California","postal":"CA"},"geometry":{"type":"Polygon","coordinates":[[[-114.721,32.721],[-115.991,32.613],[-117.128,32.535],[-117.296,33.046],[-117.944,33.621],[-118.411,33.741],[-118.52,34.028],[-119.081,34.078],[-119.439,34.348],[-120.368,34.447],[-120.623,34.609],[-120.745,35.157],[-121.715,36.162],[-122.547,37.552],[-122.512,37.784],[-122.953,38.114],[-123.727,38.951],[-123.865,39.767],[-124.398,40.313],[-124.179,41.142],[-124.214,41.999],[-119.999,41.993],[-120,38.995],[-118.115,37.644],[-116.321,36.322],[-114.642,35.053],[-114.622,34.964],[-114.567,34.828],[-114.485,34.653],[-114.355,34.465],[-114.166,34.273],[-114.469,34.067],[-114.549,33.61],[-114.743,33.38],[-114.691,33.204],[-114.656,33.054],[-114.48,32.916],[-114.59,32.716],[-114.721,32.721]]]}}, 9 | {"type":"Feature","properties":{"name":"Colorado","postal":"CO"},"geometry":{"type":"Polygon","coordinates":[[[-102.05,40.001],[-102.04,38.46],[-102.041,36.992],[-103.003,36.995],[-104.2,36.996],[-105.9,36.997],[-107.48,37],[-109.045,37],[-109.053,41.002],[-108.051,41.003],[-107.05,41.003],[-105.047,41.004],[-104.045,41.004],[-102.048,41.004],[-102.05,40.033],[-102.05,40.001]]]}}, 10 | {"type":"Feature","properties":{"name":"Nevada","postal":"NV"},"geometry":{"type":"Polygon","coordinates":[[[-114.031,36.994],[-114.024,36.19],[-114.066,36.156],[-114.133,36.004],[-114.269,36.044],[-114.461,36.115],[-114.671,36.115],[-114.74,35.991],[-114.65,35.854],[-114.651,35.639],[-114.63,35.445],[-114.581,35.249],[-114.642,35.053],[-116.321,36.322],[-118.115,37.644],[-120,38.995],[-119.999,41.993],[-117.028,41.997],[-117.028,42],[-114.034,41.993],[-114.031,36.994]]]}}, 11 | {"type":"Feature","properties":{"name":"New Mexico","postal":"NM"},"geometry":{"type":"Polygon","coordinates":[[[-106.507,31.754],[-108.24,31.755],[-108.242,31.342],[-109.044,31.342],[-109.045,37],[-107.48,37],[-105.9,36.997],[-104.2,36.996],[-103.003,36.995],[-103.002,36.499],[-103.002,33.88],[-103.002,31.999],[-103.93,31.999],[-105.73,31.999],[-106.63,31.999],[-106.62,31.914],[-106.507,31.754]]]}}, 12 | {"type":"Feature","properties":{"name":"Oregon","postal":"OR"},"geometry":{"type":"Polygon","coordinates":[[[-119.999,41.993],[-124.214,41.999],[-124.533,42.766],[-124.142,43.708],[-123.899,45.523],[-123.999,46.283],[-123.777,46.282],[-123.471,46.277],[-123.228,46.186],[-122.938,46.129],[-122.752,45.938],[-122.726,45.77],[-122.652,45.63],[-122.42,45.592],[-122.175,45.595],[-121.788,45.701],[-121.561,45.733],[-121.204,45.69],[-121.047,45.637],[-120.837,45.673],[-120.61,45.754],[-120.158,45.741],[-119.787,45.851],[-119.592,45.932],[-119.337,45.888],[-118.978,45.993],[-116.915,46],[-116.679,45.807],[-116.511,45.726],[-116.458,45.575],[-116.559,45.444],[-116.693,45.187],[-116.836,44.864],[-117.052,44.666],[-117.192,44.439],[-117.194,44.279],[-117.008,44.211],[-116.927,44.081],[-117.014,43.797],[-117.028,42],[-117.028,41.997],[-119.999,41.993]]]}}, 13 | {"type":"Feature","properties":{"name":"Utah","postal":"UT"},"geometry":{"type":"Polygon","coordinates":[[[-109.053,41.002],[-109.045,37],[-110.496,37.007],[-112.418,37.009],[-114.031,36.994],[-114.034,41.993],[-111.05,42.002],[-111.054,41.028],[-109.053,41.002]]]}}, 14 | {"type":"Feature","properties":{"name":"Wyoming","postal":"WY"},"geometry":{"type":"Polygon","coordinates":[[[-104.078,45.041],[-104.053,43],[-104.045,41.004],[-105.047,41.004],[-107.05,41.003],[-108.051,41.003],[-109.053,41.002],[-111.054,41.028],[-111.05,42.002],[-111.05,44.488],[-111.085,44.506],[-111.067,44.542],[-111.071,45.05],[-109.102,45.057],[-107.547,45.046],[-105.746,45.051],[-104.078,45.041]]]}}, 15 | {"type":"Feature","properties":{"name":"Arkansas","postal":"AR"},"geometry":{"type":"Polygon","coordinates":[[[-89.663,36.023],[-89.674,35.94],[-89.775,35.799],[-89.95,35.702],[-89.989,35.536],[-90.147,35.405],[-90.135,35.114],[-90.249,35.021],[-90.268,34.941],[-90.447,34.867],[-90.45,34.722],[-90.584,34.454],[-90.7,34.397],[-90.876,34.261],[-90.982,34.055],[-91.201,33.706],[-91.223,33.469],[-91.108,33.207],[-91.156,33.01],[-92.001,33.044],[-93.094,33.011],[-94.06,33.012],[-94.002,33.58],[-94.233,33.584],[-94.428,33.57],[-94.48,33.636],[-94.451,34.511],[-94.43,35.483],[-94.629,36.541],[-93.413,36.526],[-92.307,36.524],[-91.251,36.523],[-90.112,36.462],[-90.029,36.338],[-90.142,36.231],[-90.254,36.123],[-90.315,36.023],[-89.663,36.023]]]}}, 16 | {"type":"Feature","properties":{"name":"Iowa","postal":"IA"},"geometry":{"type":"Polygon","coordinates":[[[-91.43,40.369],[-91.567,40.452],[-91.758,40.614],[-92.852,40.592],[-94.002,40.585],[-94.898,40.583],[-95.796,40.584],[-95.862,40.765],[-95.834,40.944],[-95.856,41.116],[-95.958,41.405],[-96.025,41.524],[-96.097,41.557],[-96.104,41.788],[-96.167,41.953],[-96.349,42.142],[-96.347,42.224],[-96.41,42.389],[-96.455,42.489],[-96.454,42.581],[-96.616,42.692],[-96.535,42.856],[-96.483,43.016],[-96.46,43.124],[-96.587,43.257],[-96.586,43.501],[-96.453,43.502],[-95.36,43.5],[-94.001,43.513],[-92.54,43.52],[-91.228,43.501],[-91.214,43.447],[-91.084,43.288],[-91.173,43.212],[-91.17,43.002],[-91.129,42.913],[-91.065,42.754],[-90.738,42.658],[-90.641,42.505],[-90.583,42.429],[-90.465,42.378],[-90.417,42.27],[-90.26,42.19],[-90.157,42.104],[-90.21,41.835],[-90.395,41.608],[-90.462,41.536],[-90.691,41.479],[-91.034,41.43],[-91.123,41.258],[-90.999,41.18],[-90.957,41.025],[-91.087,40.852],[-91.154,40.7],[-91.41,40.551],[-91.43,40.369]]]}}, 17 | {"type":"Feature","properties":{"name":"Kansas","postal":"KS"},"geometry":{"type":"Polygon","coordinates":[[[-94.623,37],[-95.5,36.999],[-97.3,36.997],[-99.1,36.995],[-100.1,36.994],[-101.1,36.993],[-102.041,36.992],[-102.04,38.46],[-102.05,40.001],[-102.048,40.001],[-100.3,40.001],[-99,40.001],[-96.7,40.001],[-95.323,40.001],[-95.085,39.868],[-94.955,39.87],[-94.927,39.725],[-95.067,39.54],[-94.991,39.445],[-94.868,39.235],[-94.605,39.14],[-94.615,38.069],[-94.623,37]]]}}, 18 | {"type":"Feature","properties":{"name":"Missouri","postal":"MO"},"geometry":{"type":"Polygon","coordinates":[[[-94.623,37],[-94.615,38.069],[-94.605,39.14],[-94.868,39.235],[-94.991,39.445],[-95.067,39.54],[-94.927,39.725],[-94.955,39.87],[-95.085,39.868],[-95.323,40.001],[-95.453,40.215],[-95.608,40.343],[-95.776,40.501],[-95.796,40.584],[-94.898,40.583],[-94.002,40.585],[-92.852,40.592],[-91.758,40.614],[-91.567,40.452],[-91.43,40.369],[-91.518,40.12],[-91.428,39.821],[-91.263,39.615],[-91.072,39.445],[-90.841,39.311],[-90.749,39.265],[-90.666,39.075],[-90.65,38.908],[-90.536,38.866],[-90.347,38.93],[-90.156,38.769],[-90.213,38.585],[-90.305,38.439],[-90.37,38.264],[-90.228,38.113],[-90.03,37.972],[-89.917,37.968],[-89.655,37.749],[-89.554,37.719],[-89.479,37.477],[-89.516,37.327],[-89.388,37.081],[-89.28,37.107],[-89.103,36.952],[-89.134,36.852],[-89.115,36.695],[-89.274,36.612],[-89.498,36.506],[-89.524,36.409],[-89.585,36.267],[-89.663,36.023],[-90.315,36.023],[-90.254,36.123],[-90.142,36.231],[-90.029,36.338],[-90.112,36.462],[-91.251,36.523],[-92.307,36.524],[-93.413,36.526],[-94.629,36.541],[-94.618,37],[-94.623,37]]]}}, 19 | {"type":"Feature","properties":{"name":"Nebraska","postal":"NE"},"geometry":{"type":"Polygon","coordinates":[[[-95.323,40.001],[-96.7,40.001],[-99,40.001],[-100.3,40.001],[-102.048,40.001],[-102.05,40.001],[-102.05,40.033],[-102.048,41.004],[-104.045,41.004],[-104.053,43],[-102.1,43],[-100.6,43],[-98.594,43],[-98.336,42.874],[-97.968,42.794],[-97.882,42.84],[-97.644,42.836],[-97.287,42.846],[-97.028,42.718],[-96.754,42.634],[-96.709,42.551],[-96.623,42.503],[-96.455,42.489],[-96.41,42.389],[-96.347,42.224],[-96.349,42.142],[-96.167,41.953],[-96.104,41.788],[-96.097,41.557],[-96.025,41.524],[-95.958,41.405],[-95.856,41.116],[-95.834,40.944],[-95.862,40.765],[-95.796,40.584],[-95.776,40.501],[-95.608,40.343],[-95.453,40.215],[-95.323,40.001]]]}}, 20 | {"type":"Feature","properties":{"name":"Oklahoma","postal":"OK"},"geometry":{"type":"Polygon","coordinates":[[[-94.629,36.541],[-94.43,35.483],[-94.451,34.511],[-94.48,33.636],[-94.91,33.832],[-95.191,33.938],[-95.418,33.87],[-95.769,33.881],[-95.977,33.879],[-96.149,33.798],[-96.316,33.756],[-96.463,33.805],[-96.797,33.751],[-96.948,33.918],[-97.104,33.774],[-97.377,33.838],[-97.657,33.994],[-97.957,33.894],[-98.088,34.134],[-98.554,34.111],[-98.852,34.165],[-99.187,34.236],[-99.336,34.443],[-99.599,34.376],[-99.762,34.458],[-100,34.565],[-100,35.52],[-100,36.499],[-101.001,36.499],[-102.001,36.499],[-103.002,36.499],[-103.003,36.995],[-102.041,36.992],[-101.1,36.993],[-100.1,36.994],[-99.1,36.995],[-97.3,36.997],[-95.5,36.999],[-94.623,37],[-94.618,37],[-94.629,36.541]]]}}, 21 | {"type":"Feature","properties":{"name":"South Dakota","postal":"SD"},"geometry":{"type":"Polygon","coordinates":[[[-104.053,43],[-104.078,45.041],[-104.027,45.957],[-102.117,45.961],[-100.067,45.966],[-98.442,45.963],[-96.539,46.018],[-96.557,45.872],[-96.781,45.761],[-96.835,45.625],[-96.736,45.471],[-96.561,45.393],[-96.439,44.436],[-96.453,43.502],[-96.586,43.501],[-96.587,43.257],[-96.46,43.124],[-96.483,43.016],[-96.535,42.856],[-96.616,42.692],[-96.454,42.581],[-96.455,42.489],[-96.623,42.503],[-96.709,42.551],[-96.754,42.634],[-97.028,42.718],[-97.287,42.846],[-97.644,42.836],[-97.882,42.84],[-97.968,42.794],[-98.336,42.874],[-98.594,43],[-100.6,43],[-102.1,43],[-104.053,43]]]}}, 22 | {"type":"Feature","properties":{"name":"Louisiana","postal":"LA"},"geometry":{"type":"Polygon","coordinates":[[[-93.848,29.714],[-93.918,29.822],[-93.817,29.968],[-93.667,30.101],[-93.664,30.3],[-93.738,30.367],[-93.65,30.605],[-93.586,30.714],[-93.49,31.08],[-93.578,31.216],[-93.694,31.444],[-93.779,31.675],[-93.835,31.83],[-93.999,31.943],[-94.06,33.012],[-93.094,33.011],[-92.001,33.044],[-91.156,33.01],[-91.085,32.953],[-91.176,32.808],[-91.031,32.603],[-91.072,32.479],[-90.943,32.307],[-91.082,32.205],[-91.128,32.016],[-91.321,31.86],[-91.411,31.65],[-91.502,31.409],[-91.625,31.297],[-91.584,31.047],[-90.702,31.016],[-89.759,31.013],[-89.788,30.847],[-89.854,30.683],[-89.79,30.557],[-89.659,30.441],[-89.623,30.275],[-89.605,30.176],[-89.414,29.894],[-89.43,29.489],[-89.218,29.291],[-89.408,29.16],[-89.779,29.307],[-90.155,29.117],[-90.88,29.149],[-91.627,29.677],[-92.499,29.552],[-93.226,29.784],[-93.848,29.714]]]}}, 23 | {"type":"Feature","properties":{"name":"Texas","postal":"TX"},"geometry":{"type":"Polygon","coordinates":[[[-93.848,29.714],[-94.69,29.48],[-95.6,28.739],[-96.594,28.307],[-97.14,27.83],[-97.37,27.38],[-97.38,26.69],[-97.33,26.21],[-97.14,25.87],[-97.53,25.84],[-98.24,26.06],[-99.02,26.37],[-99.3,26.84],[-99.52,27.54],[-100.11,28.11],[-100.456,28.696],[-100.957,29.381],[-101.662,29.779],[-102.48,29.76],[-103.11,28.97],[-103.94,29.27],[-104.457,29.572],[-104.706,30.122],[-105.037,30.644],[-105.632,31.084],[-106.143,31.4],[-106.507,31.754],[-106.62,31.914],[-106.63,31.999],[-105.73,31.999],[-103.93,31.999],[-103.002,31.999],[-103.002,33.88],[-103.002,36.499],[-102.001,36.499],[-101.001,36.499],[-100,36.499],[-100,35.52],[-100,34.565],[-99.762,34.458],[-99.599,34.376],[-99.336,34.443],[-99.187,34.236],[-98.852,34.165],[-98.554,34.111],[-98.088,34.134],[-97.957,33.894],[-97.657,33.994],[-97.377,33.838],[-97.104,33.774],[-96.948,33.918],[-96.797,33.751],[-96.463,33.805],[-96.316,33.756],[-96.149,33.798],[-95.977,33.879],[-95.769,33.881],[-95.418,33.87],[-95.191,33.938],[-94.91,33.832],[-94.48,33.636],[-94.428,33.57],[-94.233,33.584],[-94.002,33.58],[-94.06,33.012],[-93.999,31.943],[-93.835,31.83],[-93.779,31.675],[-93.694,31.444],[-93.578,31.216],[-93.49,31.08],[-93.586,30.714],[-93.65,30.605],[-93.738,30.367],[-93.664,30.3],[-93.667,30.101],[-93.817,29.968],[-93.918,29.822],[-93.848,29.714]]]}}, 24 | {"type":"Feature","properties":{"name":"Connecticut","postal":"CT"},"geometry":{"type":"Polygon","coordinates":[[[-73.657,40.985],[-73.693,41.107],[-73.475,41.205],[-73.553,41.29],[-73.498,42.055],[-72.732,42.036],[-71.801,42.013],[-71.793,41.467],[-71.854,41.32],[-72.295,41.27],[-72.876,41.221],[-73.648,40.953],[-73.64850727272727,40.954803636363636],[-73.657,40.985]]]}}, 25 | {"type":"Feature","properties":{"name":"Massachusetts","postal":"MA"},"geometry":{"type":"Polygon","coordinates":[[[-71.12,41.495],[-71.148,41.648],[-71.305,41.762],[-71.379,42.024],[-71.801,42.013],[-72.732,42.036],[-73.498,42.055],[-73.282,42.743],[-72.457,42.727],[-71.249,42.718],[-71.146,42.817],[-70.934,42.884],[-70.815,42.865],[-70.825,42.335],[-70.495,41.805],[-70.08,41.78],[-70.185,42.145],[-69.885,41.923],[-69.965,41.637],[-70.64,41.475],[-71.12,41.495]]]}}, 26 | {"type":"Feature","properties":{"name":"New Hampshire","postal":"NH"},"geometry":{"type":"Polygon","coordinates":[[[-70.944,43.466],[-70.982,43.368],[-70.798,43.22],[-70.751,43.08],[-70.646,43.09],[-70.815,42.865],[-70.934,42.884],[-71.146,42.817],[-71.249,42.718],[-72.457,42.727],[-72.538,42.831],[-72.459,42.96],[-72.434,43.223],[-72.404,43.285],[-72.37,43.522],[-72.26,43.721],[-72.178,43.809],[-72.059,44.046],[-72.036,44.207],[-72.003,44.304],[-71.81,44.352],[-71.586,44.468],[-71.546,44.592],[-71.62,44.736],[-71.504,45.008],[-71.505,45.008],[-71.405,45.255],[-71.085,45.305],[-70.944,43.466]]]}}, 27 | {"type":"Feature","properties":{"name":"Rhode Island","postal":"RI"},"geometry":{"type":"Polygon","coordinates":[[[-71.12,41.495],[-71.854,41.32],[-71.793,41.467],[-71.801,42.013],[-71.379,42.024],[-71.305,41.762],[-71.148,41.648],[-71.12,41.495]]]}}, 28 | {"type":"Feature","properties":{"name":"Vermont","postal":"VT"},"geometry":{"type":"Polygon","coordinates":[[[-73.282,42.743],[-73.24,43.568],[-73.383,43.575],[-73.402,43.613],[-73.338,43.758],[-73.43,44.02],[-73.329,44.227],[-73.384,44.379],[-73.408,44.676],[-73.368,44.805],[-73.348,45.007],[-71.505,45.008],[-71.504,45.008],[-71.62,44.736],[-71.546,44.592],[-71.586,44.468],[-71.81,44.352],[-72.003,44.304],[-72.036,44.207],[-72.059,44.046],[-72.178,43.809],[-72.26,43.721],[-72.37,43.522],[-72.404,43.285],[-72.434,43.223],[-72.459,42.96],[-72.538,42.831],[-72.457,42.727],[-73.282,42.743]]]}}, 29 | {"type":"Feature","properties":{"name":"Alabama","postal":"AL"},"geometry":{"type":"Polygon","coordinates":[[[-88.167,35],[-86.91,34.999],[-85.625,34.986],[-85.366,33.744],[-85.13,32.751],[-84.986,32.438],[-84.899,32.259],[-85.064,32.083],[-85.12,31.765],[-85.066,31.577],[-85.09,31.4],[-85.118,31.236],[-85.054,31.109],[-85.005,30.991],[-87.046,30.985],[-87.617,30.928],[-87.633,30.852],[-87.405,30.609],[-87.458,30.411],[-87.53,30.274],[-88.418,30.385],[-88.45,31.912],[-88.273,33.51],[-88.096,34.806],[-88.167,35]]]}}, 30 | {"type":"Feature","properties":{"name":"Florida","postal":"FL"},"geometry":{"type":"Polygon","coordinates":[[[-84.853,30.721],[-83.848,30.675],[-82.226,30.526],[-82.152,30.351],[-82.023,30.44],[-82.02,30.788],[-81.9,30.822],[-81.702,30.748],[-81.49,30.73],[-81.314,30.036],[-80.98,29.18],[-80.536,28.472],[-80.53,28.04],[-80.057,26.88],[-80.088,26.206],[-80.132,25.817],[-80.381,25.206],[-80.68,25.08],[-81.172,25.201],[-81.33,25.64],[-81.71,25.87],[-82.24,26.73],[-82.705,27.495],[-82.855,27.886],[-82.65,28.55],[-82.93,29.1],[-83.71,29.937],[-84.1,30.09],[-85.109,29.636],[-85.288,29.686],[-85.773,30.153],[-86.4,30.4],[-87.53,30.274],[-87.458,30.411],[-87.405,30.609],[-87.633,30.852],[-87.617,30.928],[-87.046,30.985],[-85.005,30.991],[-84.853,30.721]]]}}, 31 | {"type":"Feature","properties":{"name":"Georgia","postal":"GA"},"geometry":{"type":"Polygon","coordinates":[[[-80.865,32.033],[-81.336,31.44],[-81.49,30.73],[-81.702,30.748],[-81.9,30.822],[-82.02,30.788],[-82.023,30.44],[-82.152,30.351],[-82.226,30.526],[-83.848,30.675],[-84.853,30.721],[-85.005,30.991],[-85.054,31.109],[-85.118,31.236],[-85.09,31.4],[-85.066,31.577],[-85.12,31.765],[-85.064,32.083],[-84.899,32.259],[-84.986,32.438],[-85.13,32.751],[-85.366,33.744],[-85.625,34.986],[-84.854,34.977],[-84.321,34.987],[-83.076,34.979],[-83.186,34.896],[-83.346,34.707],[-83.076,34.54],[-82.903,34.479],[-82.717,34.163],[-82.597,33.986],[-82.249,33.749],[-82.181,33.624],[-81.943,33.461],[-81.827,33.223],[-81.507,33.022],[-81.436,32.793],[-81.377,32.682],[-81.411,32.609],[-81.225,32.499],[-81.126,32.312],[-81.128,32.122],[-81.036,32.084],[-80.865,32.033]]]}}, 32 | {"type":"Feature","properties":{"name":"Mississippi","postal":"MS"},"geometry":{"type":"Polygon","coordinates":[[[-89.623,30.275],[-89.659,30.441],[-89.79,30.557],[-89.854,30.683],[-89.788,30.847],[-89.759,31.013],[-90.702,31.016],[-91.584,31.047],[-91.625,31.297],[-91.502,31.409],[-91.411,31.65],[-91.321,31.86],[-91.128,32.016],[-91.082,32.205],[-90.943,32.307],[-91.072,32.479],[-91.031,32.603],[-91.176,32.808],[-91.085,32.953],[-91.156,33.01],[-91.108,33.207],[-91.223,33.469],[-91.201,33.706],[-90.982,34.055],[-90.876,34.261],[-90.7,34.397],[-90.584,34.454],[-90.45,34.722],[-90.447,34.867],[-90.268,34.941],[-90.249,35.021],[-89.264,35.021],[-88.167,35],[-88.096,34.806],[-88.273,33.51],[-88.45,31.912],[-88.418,30.385],[-89.18,30.316],[-89.594,30.16],[-89.623,30.275]]]}}, 33 | {"type":"Feature","properties":{"name":"South Carolina","postal":"SC"},"geometry":{"type":"Polygon","coordinates":[[[-78.554,33.861],[-79.061,33.494],[-79.204,33.158],[-80.301,32.509],[-80.865,32.033],[-81.036,32.084],[-81.128,32.122],[-81.126,32.312],[-81.225,32.499],[-81.411,32.609],[-81.377,32.682],[-81.436,32.793],[-81.507,33.022],[-81.827,33.223],[-81.943,33.461],[-82.181,33.624],[-82.249,33.749],[-82.597,33.986],[-82.717,34.163],[-82.903,34.479],[-83.076,34.54],[-83.346,34.707],[-83.186,34.896],[-83.076,34.979],[-82.976,35.009],[-82.437,35.18],[-81.514,35.172],[-81.046,35.126],[-81.038,35.037],[-80.937,35.103],[-80.781,34.934],[-80.784,34.818],[-79.673,34.808],[-78.554,33.861]]]}}, 34 | {"type":"Feature","properties":{"name":"Illinois","postal":"IL"},"geometry":{"type":"Polygon","coordinates":[[[-90.641,42.505],[-89.62,42.505],[-88.577,42.503],[-87.807,42.494],[-87.807,42.497],[-87.49,42.495],[-87.039,42.493],[-87.085,42.31],[-87.13,42.127],[-87.176,41.944],[-87.221,41.761],[-87.372,41.761],[-87.521,41.761],[-87.521,41.708],[-87.526,41.708],[-87.527,40.55],[-87.528,39.392],[-87.642,39.114],[-87.56,39.04],[-87.507,38.869],[-87.515,38.735],[-87.599,38.674],[-87.671,38.509],[-87.879,38.291],[-88.019,38.022],[-88.051,37.82],[-88.044,37.745],[-88.157,37.606],[-88.072,37.512],[-88.247,37.439],[-88.474,37.355],[-88.435,37.136],[-88.566,37.054],[-88.808,37.146],[-89.074,37.2],[-89.142,37.104],[-89.103,36.952],[-89.28,37.107],[-89.388,37.081],[-89.516,37.327],[-89.479,37.477],[-89.554,37.719],[-89.655,37.749],[-89.917,37.968],[-90.03,37.972],[-90.228,38.113],[-90.37,38.264],[-90.305,38.439],[-90.213,38.585],[-90.156,38.769],[-90.347,38.93],[-90.536,38.866],[-90.65,38.908],[-90.666,39.075],[-90.749,39.265],[-90.841,39.311],[-91.072,39.445],[-91.263,39.615],[-91.428,39.821],[-91.518,40.12],[-91.43,40.369],[-91.41,40.551],[-91.154,40.7],[-91.087,40.852],[-90.957,41.025],[-90.999,41.18],[-91.123,41.258],[-91.034,41.43],[-90.691,41.479],[-90.462,41.536],[-90.395,41.608],[-90.21,41.835],[-90.157,42.104],[-90.26,42.19],[-90.417,42.27],[-90.465,42.378],[-90.583,42.429],[-90.641,42.505]]]}}, 35 | {"type":"Feature","properties":{"name":"Indiana","postal":"IN"},"geometry":{"type":"Polygon","coordinates":[[[-84.807,41.678],[-84.81,40.773],[-84.818,39.8],[-84.824,39.107],[-84.881,39.059],[-84.8,38.855],[-84.843,38.781],[-85.012,38.779],[-85.168,38.691],[-85.404,38.727],[-85.426,38.535],[-85.567,38.462],[-85.698,38.29],[-85.84,38.259],[-86.06,37.961],[-86.263,38.047],[-86.325,38.169],[-86.5,37.97],[-86.61,37.859],[-86.826,37.976],[-87.056,37.881],[-87.131,37.784],[-87.439,37.936],[-87.654,37.826],[-87.911,37.904],[-87.921,37.794],[-88.051,37.82],[-88.019,38.022],[-87.879,38.291],[-87.671,38.509],[-87.599,38.674],[-87.515,38.735],[-87.507,38.869],[-87.56,39.04],[-87.642,39.114],[-87.528,39.392],[-87.527,40.55],[-87.526,41.708],[-87.521,41.761],[-87.221,41.761],[-86.918,41.761],[-86.824,41.761],[-86.824,41.756],[-85.748,41.751],[-84.807,41.756],[-84.807,41.678]]]}}, 36 | {"type":"Feature","properties":{"name":"Kentucky","postal":"KY"},"geometry":{"type":"Polygon","coordinates":[[[-81.973,37.536],[-82.373,37.238],[-82.685,37.121],[-82.709,37.04],[-82.816,36.935],[-83.089,36.816],[-83.179,36.718],[-83.384,36.656],[-83.673,36.6],[-84.35,36.567],[-85.231,36.61],[-85.519,36.598],[-86.093,36.626],[-86.678,36.634],[-87.217,36.639],[-87.842,36.611],[-87.874,36.656],[-88.073,36.654],[-88.07,36.497],[-89.498,36.506],[-89.274,36.612],[-89.115,36.695],[-89.134,36.852],[-89.103,36.952],[-89.142,37.104],[-89.074,37.2],[-88.808,37.146],[-88.566,37.054],[-88.435,37.136],[-88.474,37.355],[-88.247,37.439],[-88.072,37.512],[-88.157,37.606],[-88.044,37.745],[-88.051,37.82],[-87.921,37.794],[-87.911,37.904],[-87.654,37.826],[-87.439,37.936],[-87.131,37.784],[-87.056,37.881],[-86.826,37.976],[-86.61,37.859],[-86.5,37.97],[-86.325,38.169],[-86.263,38.047],[-86.06,37.961],[-85.84,38.259],[-85.698,38.29],[-85.567,38.462],[-85.426,38.535],[-85.404,38.727],[-85.168,38.691],[-85.012,38.779],[-84.843,38.781],[-84.8,38.855],[-84.881,39.059],[-84.824,39.107],[-84.481,39.083],[-84.305,38.987],[-84.039,38.761],[-83.827,38.69],[-83.673,38.609],[-83.435,38.637],[-83.259,38.579],[-83.045,38.635],[-82.855,38.651],[-82.775,38.511],[-82.589,38.415],[-82.57,38.32],[-82.581,38.113],[-82.461,37.957],[-82.413,37.805],[-82.267,37.676],[-82.167,37.554],[-81.973,37.536]]]}}, 37 | {"type":"Feature","properties":{"name":"North Carolina","postal":"NC"},"geometry":{"type":"Polygon","coordinates":[[[-78.554,33.861],[-79.673,34.808],[-80.784,34.818],[-80.781,34.934],[-80.937,35.103],[-81.038,35.037],[-81.046,35.126],[-81.514,35.172],[-82.437,35.18],[-82.976,35.009],[-83.076,34.979],[-84.321,34.987],[-84.299,35.199],[-84.087,35.262],[-84.018,35.369],[-83.876,35.49],[-83.673,35.517],[-83.438,35.563],[-83.21,35.649],[-83.111,35.737],[-82.92,35.817],[-82.926,35.89],[-82.674,36.025],[-82.593,35.937],[-82.224,36.126],[-82.051,36.106],[-81.897,36.274],[-81.694,36.317],[-81.705,36.46],[-81.679,36.586],[-79.992,36.542],[-78,36.537],[-76.941,36.546],[-75.868,36.551],[-75.727,35.551],[-76.363,34.809],[-77.398,34.512],[-78.055,33.925],[-78.554,33.861]]]}}, 38 | {"type":"Feature","properties":{"name":"Ohio","postal":"OH"},"geometry":{"type":"Polygon","coordinates":[[[-82.589,38.415],[-82.775,38.511],[-82.855,38.651],[-83.045,38.635],[-83.259,38.579],[-83.435,38.637],[-83.673,38.609],[-83.827,38.69],[-84.039,38.761],[-84.305,38.987],[-84.481,39.083],[-84.824,39.107],[-84.818,39.8],[-84.81,40.773],[-84.807,41.678],[-84.295,41.685],[-83.84,41.685],[-83.463,41.694],[-83.142,41.976],[-83.122,41.95],[-83.03,41.833],[-82.866,41.753],[-82.69,41.675],[-82.439,41.675],[-82.213,41.779],[-81.974,41.889],[-81.761,41.987],[-81.507,42.104],[-81.278,42.209],[-81.028,42.247],[-80.682,42.3],[-80.521,42.324],[-80.516,42.325],[-80.516,42.324],[-80.519,40.641],[-80.658,40.591],[-80.615,40.464],[-80.662,40.234],[-80.765,39.973],[-80.862,39.757],[-80.879,39.654],[-81.151,39.426],[-81.266,39.377],[-81.401,39.349],[-81.522,39.372],[-81.745,39.2],[-81.786,39.019],[-81.817,38.922],[-81.906,38.882],[-81.919,38.994],[-82.054,39.018],[-82.195,38.801],[-82.211,38.579],[-82.341,38.441],[-82.589,38.415]]]}}, 39 | {"type":"Feature","properties":{"name":"Tennessee","postal":"TN"},"geometry":{"type":"Polygon","coordinates":[[[-85.625,34.986],[-86.91,34.999],[-88.167,35],[-89.264,35.021],[-90.249,35.021],[-90.135,35.114],[-90.147,35.405],[-89.989,35.536],[-89.95,35.702],[-89.775,35.799],[-89.674,35.94],[-89.663,36.023],[-89.585,36.267],[-89.524,36.409],[-89.498,36.506],[-88.07,36.497],[-88.073,36.654],[-87.874,36.656],[-87.842,36.611],[-87.217,36.639],[-86.678,36.634],[-86.093,36.626],[-85.519,36.598],[-85.231,36.61],[-84.35,36.567],[-83.673,36.6],[-82.186,36.566],[-81.679,36.586],[-81.705,36.46],[-81.694,36.317],[-81.897,36.274],[-82.051,36.106],[-82.224,36.126],[-82.593,35.937],[-82.674,36.025],[-82.926,35.89],[-82.92,35.817],[-83.111,35.737],[-83.21,35.649],[-83.438,35.563],[-83.673,35.517],[-83.876,35.49],[-84.018,35.369],[-84.087,35.262],[-84.299,35.199],[-84.321,34.987],[-84.854,34.977],[-85.625,34.986]]]}}, 40 | {"type":"Feature","properties":{"name":"Virginia","postal":"VA"},"geometry":{"type":"MultiPolygon","coordinates":[[[[-75.378,38.015],[-75.94,37.217],[-76.031,37.257],[-75.722,37.937],[-75.61,38],[-75.378,38.015]]],[[[-77.723,39.322],[-77.576,39.289],[-77.443,39.213],[-77.517,39.106],[-77.306,39.046],[-77.12,38.934],[-77.036,38.848],[-77.041,38.79],[-77.059,38.709],[-77.23,38.614],[-77.343,38.392],[-77.211,38.337],[-77.048,38.381],[-76.99,38.24],[-76.302,37.918],[-76.259,36.966],[-75.972,36.897],[-75.868,36.551],[-76.941,36.546],[-78,36.537],[-79.992,36.542],[-81.679,36.586],[-82.186,36.566],[-83.673,36.6],[-83.384,36.656],[-83.179,36.718],[-83.089,36.816],[-82.816,36.935],[-82.709,37.04],[-82.685,37.121],[-82.373,37.238],[-81.973,37.536],[-81.928,37.366],[-81.815,37.276],[-81.664,37.195],[-81.348,37.316],[-81.228,37.245],[-80.855,37.329],[-80.833,37.418],[-80.72,37.383],[-80.596,37.456],[-80.457,37.442],[-80.298,37.519],[-80.277,37.611],[-80.293,37.728],[-80.158,37.901],[-79.964,38.032],[-79.915,38.179],[-79.744,38.357],[-79.648,38.575],[-79.515,38.497],[-79.366,38.426],[-79.223,38.465],[-79.175,38.556],[-79.076,38.68],[-78.965,38.822],[-78.893,38.78],[-78.745,38.909],[-78.549,39.04],[-78.424,39.139],[-78.346,39.406],[-77.835,39.135],[-77.723,39.322]]]]}}, 41 | {"type":"Feature","properties":{"name":"Wisconsin","postal":"WI"},"geometry":{"type":"Polygon","coordinates":[[[-86.265,45.227],[-86.402,45.133],[-86.536,45.042],[-86.715,44.846],[-86.822,44.591],[-86.919,44.362],[-87.032,44.091],[-87.083,43.89],[-87.142,43.658],[-87.145,43.571],[-87.155,43.327],[-87.114,43.03],[-87.08,42.782],[-87.039,42.493],[-87.49,42.495],[-87.807,42.497],[-87.807,42.494],[-88.577,42.503],[-89.62,42.505],[-90.641,42.505],[-90.738,42.658],[-91.065,42.754],[-91.129,42.913],[-91.17,43.002],[-91.173,43.212],[-91.084,43.288],[-91.214,43.447],[-91.228,43.501],[-91.255,43.614],[-91.257,43.855],[-91.29,43.937],[-91.628,44.085],[-91.88,44.257],[-91.95,44.365],[-92.062,44.433],[-92.385,44.575],[-92.505,44.584],[-92.797,44.776],[-92.766,44.996],[-92.765,45.267],[-92.689,45.518],[-92.9,45.706],[-92.757,45.89],[-92.544,45.986],[-92.297,46.096],[-92.265,46.095],[-92.275,46.656],[-92.012,46.712],[-91.965,46.73],[-91.841,46.778],[-91.717,46.828],[-91.592,46.876],[-91.468,46.925],[-91.265,47.021],[-91.061,47.117],[-90.858,47.213],[-90.654,47.309],[-90.48,47.304],[-90.306,47.298],[-90.132,47.293],[-89.958,47.287],[-90.015,47.2],[-90.071,47.112],[-90.128,47.024],[-90.185,46.936],[-90.242,46.848],[-90.299,46.761],[-90.356,46.673],[-90.395,46.612],[-90.411,46.584],[-90.397,46.576],[-90.335,46.597],[-90.334,46.594],[-90.177,46.561],[-90.096,46.381],[-89.221,46.202],[-88.644,46.022],[-88.362,46.021],[-88.167,46.008],[-88.112,45.843],[-87.875,45.78],[-87.787,45.64],[-87.848,45.559],[-87.893,45.397],[-87.673,45.388],[-87.747,45.227],[-87.614,45.109],[-87.613,45.11],[-87.459,45.068],[-87.42,45.184],[-87.245,45.29],[-87.116,45.452],[-86.942,45.452],[-86.774,45.452],[-86.482,45.323],[-86.265,45.227]]]}}, 42 | {"type":"Feature","properties":{"name":"West Virginia","postal":"WV"},"geometry":{"type":"Polygon","coordinates":[[[-81.973,37.536],[-82.167,37.554],[-82.267,37.676],[-82.413,37.805],[-82.461,37.957],[-82.581,38.113],[-82.57,38.32],[-82.589,38.415],[-82.341,38.441],[-82.211,38.579],[-82.195,38.801],[-82.054,39.018],[-81.919,38.994],[-81.906,38.882],[-81.817,38.922],[-81.786,39.019],[-81.745,39.2],[-81.522,39.372],[-81.401,39.349],[-81.266,39.377],[-81.151,39.426],[-80.879,39.654],[-80.862,39.757],[-80.765,39.973],[-80.662,40.234],[-80.615,40.464],[-80.658,40.591],[-80.519,40.641],[-80.519,39.721],[-79.478,39.721],[-79.486,39.213],[-79.333,39.303],[-79.161,39.418],[-78.963,39.458],[-78.829,39.563],[-78.534,39.522],[-78.425,39.597],[-78.232,39.672],[-77.923,39.593],[-77.802,39.45],[-77.723,39.322],[-77.835,39.135],[-78.346,39.406],[-78.424,39.139],[-78.549,39.04],[-78.745,38.909],[-78.893,38.78],[-78.965,38.822],[-79.076,38.68],[-79.175,38.556],[-79.223,38.465],[-79.366,38.426],[-79.515,38.497],[-79.648,38.575],[-79.744,38.357],[-79.915,38.179],[-79.964,38.032],[-80.158,37.901],[-80.293,37.728],[-80.277,37.611],[-80.298,37.519],[-80.457,37.442],[-80.596,37.456],[-80.72,37.383],[-80.833,37.418],[-80.855,37.329],[-81.228,37.245],[-81.348,37.316],[-81.664,37.195],[-81.815,37.276],[-81.928,37.366],[-81.973,37.536]]]}}, 43 | {"type":"Feature","properties":{"name":"Delaware","postal":"DE"},"geometry":{"type":"Polygon","coordinates":[[[-75.048,38.449],[-75.715,38.449],[-75.788,39.724],[-75.711,39.802],[-75.621,39.847],[-75.406,39.796],[-75.554,39.691],[-75.528,39.499],[-75.32,38.96],[-75.072,38.782],[-75.048,38.449]]]}}, 44 | {"type":"Feature","properties":{"name":"District of Columbia","postal":"DC"},"geometry":{"type":"Polygon","coordinates":[[[-77.041,38.79],[-77.036,38.848],[-77.12,38.934],[-77.039,38.982],[-76.912,38.878],[-77.041,38.79]]]}}, 45 | {"type":"Feature","properties":{"name":"Maryland","postal":"MD"},"geometry":{"type":"Polygon","coordinates":[[[-75.048,38.449],[-75.057,38.404],[-75.378,38.015],[-75.61,38],[-75.722,37.937],[-76.233,38.319],[-76.35,39.15],[-76.543,38.718],[-76.329,38.083],[-76.99,38.24],[-77.048,38.38],[-77.211,38.337],[-77.343,38.392],[-77.23,38.614],[-77.059,38.709],[-77.041,38.79],[-76.912,38.878],[-77.039,38.982],[-77.12,38.934],[-77.306,39.046],[-77.517,39.106],[-77.443,39.213],[-77.576,39.289],[-77.723,39.322],[-77.802,39.45],[-77.923,39.593],[-78.232,39.672],[-78.425,39.597],[-78.534,39.522],[-78.829,39.563],[-78.963,39.458],[-79.161,39.418],[-79.333,39.303],[-79.486,39.213],[-79.478,39.721],[-78.55,39.72],[-78.233,39.721],[-77.523,39.726],[-76.668,39.721],[-75.788,39.724],[-75.715,38.449],[-75.048,38.449]]]}}, 46 | {"type":"Feature","properties":{"name":"New Jersey","postal":"NJ"},"geometry":{"type":"Polygon","coordinates":[[[-73.952,40.751],[-74.257,40.473],[-73.962,40.428],[-74.178,39.709],[-74.906,38.939],[-74.981,39.196],[-75.2,39.248],[-75.528,39.499],[-75.554,39.691],[-75.406,39.796],[-75.201,39.887],[-75.129,39.949],[-74.892,40.082],[-74.763,40.191],[-75.078,40.45],[-75.095,40.555],[-75.204,40.587],[-75.199,40.747],[-75.082,40.87],[-75.136,41],[-74.976,41.088],[-74.801,41.312],[-74.679,41.355],[-73.913,40.96],[-73.952,40.751]]]}}, 47 | {"type":"Feature","properties":{"name":"New York","postal":"NY"},"geometry":{"type":"Polygon","coordinates":[[[-74.679,41.355],[-74.84,41.426],[-75.011,41.496],[-75.075,41.641],[-75.049,41.751],[-75.168,41.842],[-75.385,41.999],[-76.744,42.001],[-78.201,42],[-79.76,42],[-79.76,42.5],[-79.773,42.547],[-78.939,42.864],[-78.92,42.965],[-79.01,43.27],[-79.172,43.466],[-79.002,43.527],[-78.846,43.583],[-78.72,43.625],[-76.82,43.629],[-76.697,43.785],[-76.586,43.924],[-76.5,44.018],[-76.375,44.096],[-75.318,44.816],[-74.867,45],[-73.348,45.007],[-73.368,44.805],[-73.408,44.676],[-73.384,44.379],[-73.329,44.227],[-73.43,44.02],[-73.338,43.758],[-73.402,43.613],[-73.383,43.575],[-73.24,43.568],[-73.282,42.743],[-73.498,42.055],[-73.553,41.29],[-73.475,41.205],[-73.693,41.107],[-73.657,40.985],[-73.648,40.955],[-73.64850727272727,40.954803636363636],[-73.71,40.931],[-72.241,41.119],[-71.945,40.93],[-73.345,40.63],[-73.982,40.628],[-73.952,40.751],[-73.913,40.96],[-74.679,41.355]]]}}, 48 | {"type":"Feature","properties":{"name":"Pennsylvania","postal":"PA"},"geometry":{"type":"Polygon","coordinates":[[[-75.406,39.796],[-75.621,39.847],[-75.711,39.802],[-75.788,39.724],[-76.668,39.721],[-77.523,39.726],[-78.233,39.721],[-78.55,39.72],[-79.478,39.721],[-80.519,39.721],[-80.519,40.641],[-80.516,42.324],[-80.516,42.325],[-80.247,42.366],[-79.773,42.546],[-79.76,42.5],[-79.76,42],[-78.201,42],[-76.744,42.001],[-75.385,41.999],[-75.168,41.842],[-75.049,41.751],[-75.075,41.641],[-75.011,41.496],[-74.84,41.426],[-74.679,41.355],[-74.801,41.312],[-74.976,41.088],[-75.136,41],[-75.082,40.87],[-75.199,40.747],[-75.204,40.587],[-75.095,40.555],[-75.078,40.45],[-74.763,40.191],[-74.892,40.082],[-75.129,39.949],[-75.201,39.887],[-75.406,39.796]]]}}, 49 | {"type":"Feature","properties":{"name":"Maine","postal":"ME"},"geometry":{"type":"Polygon","coordinates":[[[-70.646,43.09],[-70.751,43.08],[-70.798,43.22],[-70.982,43.368],[-70.944,43.466],[-71.085,45.305],[-70.66,45.46],[-70.305,45.915],[-70,46.693],[-69.237,47.448],[-68.905,47.185],[-68.234,47.355],[-67.79,47.066],[-67.791,45.703],[-67.137,45.137],[-66.965,44.81],[-68.033,44.325],[-69.06,43.98],[-70.116,43.684],[-70.646,43.09]]]}}, 50 | {"type":"Feature","properties":{"name":"Michigan","postal":"MI"},"geometry":{"type":"Polygon","coordinates":[[[-89.958,47.287],[-89.843,47.465],[-89.728,47.642],[-89.614,47.819],[-89.523,47.961],[-89.49028126070573,48.012965056526205],[-89.489,48.015],[-89.273,48.02],[-89.186,48.047],[-88.378,48.303],[-87.494,47.962],[-87.208,47.848],[-86.922,47.735],[-86.672,47.636],[-86.495,47.567],[-86.429,47.54],[-86.234,47.46],[-86.04,47.38],[-85.847,47.3],[-85.652,47.22],[-85.458,47.14],[-85.264,47.06],[-85.07,46.98],[-84.876,46.9],[-84.827,46.767],[-84.779,46.637],[-84.544,46.539],[-84.543,46.539],[-84.605,46.44],[-84.337,46.409],[-84.142,46.512],[-84.128,46.484],[-84.115,46.371],[-84.092,46.275],[-83.891,46.117],[-83.763,46.109],[-83.669,46.123],[-83.616,46.117],[-83.47,45.995],[-83.593,45.817],[-83.397,45.729],[-83.179,45.633],[-82.919,45.518],[-82.76,45.448],[-82.551,45.348],[-82.515,45.204],[-82.485,45.084],[-82.447,44.916],[-82.408,44.744],[-82.368,44.573],[-82.327,44.391],[-82.281,44.192],[-82.241,44.016],[-82.196,43.822],[-82.138,43.571],[-82.191,43.474],[-82.417,43.018],[-82.43,42.98],[-82.9,42.43],[-83.12,42.08],[-83.129,42.069],[-83.142,41.976],[-83.463,41.694],[-83.84,41.685],[-84.295,41.685],[-84.807,41.678],[-84.807,41.756],[-85.748,41.751],[-86.824,41.756],[-86.824,41.761],[-86.918,41.761],[-87.221,41.761],[-87.176,41.944],[-87.13,42.127],[-87.085,42.31],[-87.039,42.493],[-87.08,42.782],[-87.114,43.03],[-87.155,43.327],[-87.145,43.571],[-87.142,43.658],[-87.083,43.89],[-87.032,44.091],[-86.919,44.362],[-86.822,44.591],[-86.715,44.846],[-86.536,45.042],[-86.402,45.133],[-86.265,45.227],[-86.482,45.323],[-86.774,45.452],[-86.942,45.452],[-87.116,45.452],[-87.245,45.29],[-87.42,45.184],[-87.459,45.068],[-87.613,45.11],[-87.614,45.109],[-87.747,45.227],[-87.673,45.388],[-87.893,45.397],[-87.848,45.559],[-87.787,45.64],[-87.875,45.78],[-88.112,45.843],[-88.167,46.008],[-88.362,46.021],[-88.644,46.022],[-89.221,46.202],[-90.096,46.381],[-90.177,46.561],[-90.334,46.594],[-90.335,46.597],[-90.397,46.576],[-90.411,46.584],[-90.395,46.612],[-90.356,46.673],[-90.299,46.761],[-90.242,46.848],[-90.185,46.936],[-90.128,47.024],[-90.071,47.112],[-90.015,47.2],[-89.958,47.287]]]}}, 51 | {"type":"Feature","properties":{"name":"Alaska","postal":"AK"},"geometry":{"type":"MultiPolygon","coordinates":[[[[-153.229,57.969],[-152.565,57.901],[-152.141,57.591],[-153.006,57.116],[-154.005,56.735],[-154.516,56.993],[-154.671,57.461],[-153.763,57.817],[-153.229,57.969]]],[[[-166.468,60.384],[-165.674,60.294],[-165.579,59.91],[-166.193,59.754],[-166.848,59.941],[-167.455,60.213],[-166.468,60.384]]],[[[-171.732,63.783],[-171.114,63.592],[-170.491,63.695],[-169.683,63.431],[-168.689,63.298],[-168.772,63.189],[-169.529,62.977],[-170.291,63.194],[-170.671,63.376],[-171.553,63.318],[-171.791,63.406],[-171.732,63.783]]],[[[-140.986,69.712],[-140.992,66],[-140.998,60.306],[-140.013,60.277],[-139.039,60],[-138.341,59.562],[-137.453,58.905],[-136.48,59.464],[-135.476,59.788],[-134.945,59.271],[-134.271,58.861],[-133.356,58.41],[-132.73,57.693],[-131.708,56.552],[-130.008,55.916],[-129.98,55.285],[-130.536,54.803],[-131.086,55.179],[-131.967,55.498],[-132.25,56.37],[-133.539,57.179],[-134.078,58.123],[-135.038,58.188],[-136.628,58.212],[-137.8,58.5],[-139.868,59.538],[-140.825,59.728],[-142.574,60.084],[-143.959,59.999],[-145.926,60.459],[-147.114,60.885],[-148.224,60.673],[-148.018,59.978],[-148.571,59.914],[-149.728,59.706],[-150.608,59.368],[-151.716,59.156],[-151.859,59.745],[-151.41,60.726],[-150.347,61.034],[-150.621,61.284],[-151.896,60.727],[-152.578,60.062],[-154.019,59.35],[-153.288,58.865],[-154.232,58.146],[-155.307,57.728],[-156.308,57.423],[-156.556,56.98],[-158.117,56.464],[-158.433,55.994],[-159.603,55.567],[-160.29,55.644],[-161.223,55.365],[-162.238,55.024],[-163.069,54.69],[-164.786,54.404],[-164.942,54.572],[-163.848,55.039],[-162.87,55.348],[-161.804,55.895],[-160.564,56.008],[-160.071,56.418],[-158.684,57.017],[-158.461,57.217],[-157.723,57.57],[-157.55,58.328],[-157.042,58.919],[-158.195,58.616],[-158.517,58.788],[-159.059,58.424],[-159.712,58.931],[-159.981,58.573],[-160.355,59.071],[-161.355,58.671],[-161.969,58.672],[-162.055,59.267],[-161.874,59.634],[-162.518,59.99],[-163.818,59.798],[-164.662,60.267],[-165.346,60.507],[-165.351,61.074],[-166.121,61.5],[-165.734,62.075],[-164.919,62.633],[-164.563,63.146],[-163.753,63.219],[-163.067,63.059],[-162.261,63.542],[-161.534,63.456],[-160.773,63.766],[-160.958,64.223],[-161.518,64.403],[-160.778,64.789],[-161.392,64.777],[-162.453,64.559],[-162.758,64.339],[-163.546,64.559],[-164.961,64.447],[-166.425,64.687],[-166.845,65.089],[-168.111,65.67],[-166.705,66.088],[-164.475,66.577],[-163.653,66.577],[-163.789,66.077],[-161.678,66.116],[-162.49,66.736],[-163.72,67.116],[-164.431,67.616],[-165.39,68.043],[-166.764,68.359],[-166.205,68.883],[-164.431,68.916],[-163.169,69.371],[-162.931,69.858],[-161.909,70.333],[-160.935,70.448],[-159.039,70.892],[-158.12,70.825],[-156.581,71.358],[-155.068,71.148],[-154.344,70.696],[-153.9,70.89],[-152.21,70.83],[-152.27,70.6],[-150.74,70.43],[-149.72,70.53],[-147.613,70.214],[-145.69,70.12],[-144.92,69.99],[-143.589,70.153],[-142.073,69.852],[-140.986,69.712]]]]}}]}; --------------------------------------------------------------------------------