diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bccba6..f0bb1b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 1.1.0 + +- Add bounding box search option +- Read files inside isolate +- Correctly dispose and cleanup objects + ## 1.0.0 Migration to null safety diff --git a/lib/src/deserializers.dart b/lib/src/deserializers.dart index cecb9a7..248edbd 100644 --- a/lib/src/deserializers.dart +++ b/lib/src/deserializers.dart @@ -1,12 +1,12 @@ -import 'package:geopoint/geopoint.dart'; - +import 'geopoint/geopoint.dart'; +import 'geopoint/geoserie.dart'; import 'models.dart'; /// Get a collection inside another collection GeoJsonGeometryCollection getGeometryCollection( - {List?>? geometries, - GeoJsonFeature? feature, - String? nameProperty}) { + {final List?>? geometries, + final GeoJsonFeature? feature, + final String? nameProperty}) { final name = _getName(feature: feature, nameProperty: nameProperty); final collection = GeoJsonGeometryCollection(geometries: geometries, name: name); @@ -15,9 +15,9 @@ GeoJsonGeometryCollection getGeometryCollection( /// Get a point from coordinates and feature GeoJsonPoint getPoint( - {List? coordinates, - GeoJsonFeature? feature, - String? nameProperty}) { + {final List? coordinates, + final GeoJsonFeature? feature, + final String? nameProperty}) { final name = _getName(feature: feature, nameProperty: nameProperty); final geoPoint = _getGeoPoints([coordinates])[0]..name = name; return GeoJsonPoint(geoPoint: geoPoint, name: name); @@ -25,9 +25,9 @@ GeoJsonPoint getPoint( /// Get multi points from coordinates and feature GeoJsonMultiPoint getMultiPoint( - {required List coordinates, - GeoJsonFeature? feature, - String? nameProperty}) { + {required final List coordinates, + final GeoJsonFeature? feature, + final String? nameProperty}) { final multiPoint = GeoJsonMultiPoint(); final name = _getName(feature: feature, nameProperty: nameProperty); multiPoint.name = name; @@ -41,9 +41,9 @@ GeoJsonMultiPoint getMultiPoint( /// Get a line from coordinates and feature GeoJsonLine getLine( - {required List coordinates, - GeoJsonFeature? feature, - String? nameProperty}) { + {required final List coordinates, + final GeoJsonFeature? feature, + final String? nameProperty}) { final line = GeoJsonLine(); final name = _getName(feature: feature, nameProperty: nameProperty); line.name = name; @@ -57,9 +57,9 @@ GeoJsonLine getLine( /// Get a multi line from coordinates and feature GeoJsonMultiLine getMultiLine( - {required List coordinates, - GeoJsonFeature? feature, - String? nameProperty}) { + {required final List coordinates, + final GeoJsonFeature? feature, + final String? nameProperty}) { final name = _getName(feature: feature, nameProperty: nameProperty); final multiLine = GeoJsonMultiLine(name: name); var i = 1; @@ -79,9 +79,9 @@ GeoJsonMultiLine getMultiLine( /// Get a polygon from coordinates and feature GeoJsonPolygon getPolygon( - {required List coordinates, - GeoJsonFeature? feature, - String? nameProperty}) { + {required final List coordinates, + final GeoJsonFeature? feature, + final String? nameProperty}) { final polygon = GeoJsonPolygon(); final name = _getName(feature: feature, nameProperty: nameProperty); polygon.name = name; @@ -97,9 +97,9 @@ GeoJsonPolygon getPolygon( /// Get a multiPolygon from coordinates and feature GeoJsonMultiPolygon getMultiPolygon( - {required List coordinates, - GeoJsonFeature? feature, - String? nameProperty}) { + {required final List coordinates, + final GeoJsonFeature? feature, + final String? nameProperty}) { final multiPolygon = GeoJsonMultiPolygon(); var i = 1; multiPolygon.name = _getName(feature: feature, nameProperty: nameProperty); @@ -108,7 +108,7 @@ GeoJsonMultiPolygon getMultiPolygon( final name = _getName(feature: feature, nameProperty: nameProperty, index: i); polygon.name = name; - for (final coords in coordsL2) { + for (final coords in coordsL2 as Iterable) { final geoSerie = GeoSerie( name: _getName(feature: feature, nameProperty: nameProperty), type: GeoSerieType.polygon) @@ -122,7 +122,9 @@ GeoJsonMultiPolygon getMultiPolygon( } String _getName( - {GeoJsonFeature? feature, String? nameProperty, int? index}) { + {final GeoJsonFeature? feature, + final String? nameProperty, + final int? index}) { String? name; if (nameProperty != null) { name = feature?.properties?[nameProperty]?.toString(); @@ -140,7 +142,7 @@ String _getName( return name; } -List _getGeoPoints(List coordsList) { +List _getGeoPoints(final List coordsList) { final geoPoints = []; for (final coordinates in coordsList) { if (!(coordinates is List)) continue; // skip diff --git a/lib/src/geojson.dart b/lib/src/geojson.dart index adec570..712b95e 100644 --- a/lib/src/geojson.dart +++ b/lib/src/geojson.dart @@ -3,11 +3,12 @@ import 'dart:convert'; import 'dart:io'; import 'package:geodesy/geodesy.dart'; -import 'package:iso/iso.dart'; import 'package:pedantic/pedantic.dart'; import 'deserializers.dart'; import 'exceptions.dart'; +import 'iso/iso.dart'; +import 'iso/runner.dart'; import 'models.dart'; /// The main geojson class @@ -79,6 +80,9 @@ class GeoJson { /// The stream indicating that the parsing is finished /// Use it to dispose the class if not needed anymore after parsing Stream get endSignal => _endSignalController.stream; + + bool _disposed = false; + // internal method Stream _getGeoStream() => _processedFeaturesController.stream .asBroadcastStream() @@ -91,20 +95,8 @@ class GeoJson { bool verbose = false, GeoJsonQuery? query, bool disableStream = false}) async { - final file = File(path); - if (!file.existsSync()) { - throw FileSystemException("The file ${file.path} does not exist"); - } - String data; - try { - data = await file.readAsString(); - } catch (e) { - throw FileSystemException("Can not read file $e"); - } - if (verbose) { - print("Parsing file ${file.path}"); - } - await _parse(data, + await _parse( + fileName: path, nameProperty: nameProperty, verbose: verbose, query: query, @@ -116,13 +108,18 @@ class GeoJson { {String? nameProperty, bool verbose = false, bool disableStream = false}) => - _parse(data, + _parse( + data: data, nameProperty: nameProperty, verbose: verbose, disableStream: disableStream); void _pipeFeature(GeoJsonFeature data, {required bool disableStream}) { + if (_disposed) { + return; + } + dynamic item; switch (data.type) { case GeoJsonFeatureType.point: @@ -151,8 +148,10 @@ class GeoJson { break; case GeoJsonFeatureType.geometryCollection: } - if (!disableStream) { - if (item != null) _processedFeaturesController.sink.add(item); + if (!disableStream && !_processedFeaturesController.isClosed) { + if (item != null) { + _processedFeaturesController.sink.add(item); + } _processedFeaturesController.sink.add(data); } features.add(data); @@ -179,8 +178,9 @@ class GeoJson { unawaited(_feats.close()); } - Future _parse( - String data, { + Future _parse({ + String? data, + String? fileName, required bool disableStream, required bool verbose, String? nameProperty, @@ -200,9 +200,14 @@ class GeoJson { throw ParseErrorException("Can not parse geojson"); }); final dataToProcess = _DataToProcess( - data: data, nameProperty: nameProperty, verbose: verbose, query: query); + data: data, + fileName: fileName, + nameProperty: nameProperty, + verbose: verbose, + query: query); unawaited(iso.run([dataToProcess])); await finished.future; + iso.dispose(); _endSignalController.sink.add(true); } @@ -228,7 +233,8 @@ class GeoJson { throw ArgumentError("Provide data or parse some to run a search"); } if (data != null) { - await _parse(data, + await _parse( + data: data, nameProperty: nameProperty, verbose: verbose, query: query, @@ -251,7 +257,7 @@ class GeoJson { if (data is GeoJsonPoint) { final point = data; foundPoints.add(point); - if (!disableStream) { + if (!disableStream && !_processedFeaturesController.isClosed) { _processedFeaturesController.sink.add(point); } } else { @@ -265,6 +271,7 @@ class GeoJson { points: points, point: point, distance: distance, verbose: verbose); unawaited(iso.run([dataToProcess])); await finished.future; + iso.dispose(); return foundPoints; } @@ -303,7 +310,7 @@ class GeoJson { if (data is GeoJsonPoint) { final point = data; foundPoints.add(point); - if (!disableStream) { + if (!disableStream && !_processedFeaturesController.isClosed) { _processedFeaturesController.sink.add(point); } } else { @@ -317,6 +324,7 @@ class GeoJson { _GeoFenceToProcess(points: points, polygon: polygon, verbose: verbose); unawaited(iso.run([dataToProcess])); await finished.future; + iso.dispose(); return foundPoints; } @@ -346,10 +354,73 @@ class GeoJson { /// Dispose the class when finished using it void dispose() { + _disposed = true; _processedFeaturesController.close(); _endSignalController.close(); } + static bool _isOverlapping( + GeoBoundingBox boundingBox, GeoJsonFeature? feature) { + if (feature == null) { + return false; + } + + final dynamic geometry = feature.geometry; + switch (feature.type) { + case GeoJsonFeatureType.geometryCollection: + final collection = geometry as GeoJsonGeometryCollection; + for (final geometry + in collection.geometries ?? ?>[]) { + if (_isOverlapping(boundingBox, geometry)) { + return true; + } + } + return false; + case GeoJsonFeatureType.multipolygon: + final multiPolygon = geometry as GeoJsonMultiPolygon; + for (final polygon in multiPolygon.polygons) { + if (boundingBox + .isOverlapping(polygon.geoSeries.expand((e) => e.geoPoints))) { + return true; + } + } + return false; + case GeoJsonFeatureType.polygon: + final polygon = geometry as GeoJsonPolygon; + if (boundingBox + .isOverlapping(polygon.geoSeries.expand((e) => e.geoPoints))) { + return true; + } + return false; + case GeoJsonFeatureType.multiline: + final multiLine = geometry as GeoJsonMultiLine; + if (boundingBox.isOverlapping( + multiLine.lines.expand((e) => e.geoSerie?.geoPoints ?? []))) { + return true; + } + return false; + case GeoJsonFeatureType.line: + final line = geometry as GeoJsonLine; + if (boundingBox.isOverlapping(line.geoSerie?.geoPoints ?? [])) { + return true; + } + return false; + case GeoJsonFeatureType.multipoint: + final multiPoint = geometry as GeoJsonMultiPoint; + if (boundingBox.isOverlapping(multiPoint.geoSerie?.geoPoints ?? [])) { + return true; + } + return false; + case GeoJsonFeatureType.point: + final point = geometry as GeoJsonPoint; + if (boundingBox.isOverlapping([point.geoPoint])) { + return true; + } + return false; + } + return false; + } + static GeoJsonFeature? _processGeometry( Map geometry, Map? properties, @@ -446,10 +517,26 @@ class GeoJson { throw ArgumentError.notNull(); } } - final data = dataToProcess.data; + final nameProperty = dataToProcess.nameProperty; final verbose = dataToProcess.verbose; final query = dataToProcess.query; + + var data = dataToProcess.data; + if (data == null) { + final file = File(dataToProcess.fileName!); + if (!file.existsSync()) { + throw FileSystemException("The file ${file.path} does not exist"); + } + try { + data = file.readAsStringSync(); + } catch (e) { + throw FileSystemException("Can not read file $e"); + } + if (verbose) { + print("Parsing file ${file.path}"); + } + } final decoded = json.decode(data) as Map; final feats = decoded["features"] as List; for (final dFeature in feats) { @@ -470,7 +557,7 @@ class GeoJson { if (nameProperty != null) { feature.geometry.name = properties![nameProperty]; } - for (final geom in geometry["geometries"]) { + for (final geom in geometry["geometries"] as Iterable) { feature.geometry.add(_processGeometry( geom as Map, properties, nameProperty)); } @@ -544,10 +631,21 @@ class GeoJson { continue; } } + if (query?.boundingBox != null) { + if (!_isOverlapping(query!.boundingBox!, feature)) { + if (verbose == true) { + print( + "Skipping out of bounds feature ${feature?.type} ${feature?.geometry?.name}"); + } + continue; + } + } if (iso != null) { iso.send(feature); } else { - print("FEAT SINK $feature / ${feature?.type}"); + if (verbose == true) { + print("FEAT SINK $feature / ${feature?.type}"); + } sink?.add(feature); } if (verbose == true) { @@ -596,12 +694,18 @@ class GeoJson { class _DataToProcess { _DataToProcess( - {required this.data, + {this.data, + this.fileName, required this.nameProperty, required this.verbose, - required this.query}); + required this.query}) { + if (this.data == null && this.fileName == null) { + throw ArgumentError.notNull("Either data or filename should be provided"); + } + } - final String data; + final String? fileName; + final String? data; final String? nameProperty; final bool verbose; final GeoJsonQuery? query; diff --git a/lib/src/geopoint/geopoint.dart b/lib/src/geopoint/geopoint.dart new file mode 100644 index 0000000..5fb8d1f --- /dev/null +++ b/lib/src/geopoint/geopoint.dart @@ -0,0 +1,251 @@ +import 'dart:io'; + +import 'package:latlong2/latlong.dart'; + +import '../slug/slugify.dart'; + +/// inValidLocation +double _inValidLocation = -181.0; + +/// A class to hold geo point data structure +class GeoPoint { + /// Default constructor: needs [latitude] and [longitude] + GeoPoint( + {required this.latitude, + required this.longitude, + this.name, + this.id, + this.slug, + this.timestamp, + this.altitude, + this.speed, + this.accuracy, + this.heading, + this.country, + this.locality, + this.sublocality, + this.number, + this.postalCode, + this.region, + this.speedAccuracy, + this.street, + this.subregion, + this.images}) { + if (slug == null && name != null) slug = slugify(name!); + } + + /// The name of the geoPoint + String? name; + + /// A latitude coordinate + final double latitude; + + /// A longitude coordinate + final double longitude; + + /// A string without spaces nor special characters. Can be used + /// to define file paths + String? slug; + + /// The id of the geoPoint + int? id; + + /// The timestamp + int? timestamp; + + /// The altitude of the geoPoint + double? altitude; + + /// The speed + double? speed; + + /// The accuracy of the measurement + double? accuracy; + + /// The accuracy of the speed + double? speedAccuracy; + + /// The heading + double? heading; + + /// Number in the street + String? number; + + /// Street name + String? street; + + /// Locality name + String? locality; + + /// Sub locality name + String? sublocality; + + /// Local postal code + String? postalCode; + + /// Subregion + String? subregion; + + /// Region + String? region; + + /// Country + String? country; + + /// A list of images can be attached to the geo point + List? images; + + /// the formatted address of the [GeoPoint] + String get address => _getAddress(); + + /// the [LatLng] of the [GeoPoint] + LatLng get point => LatLng(latitude, longitude); + + /// Build this geo point from json data + /// Default constructor: needs [latitude] and [longitude] + GeoPoint.fromJson(Map json) + : id = int.tryParse("${json["id"]}"), + name = "${json["name"]}", + timestamp = int.tryParse("${json["timestamp"]}"), + latitude = double.tryParse("${json["latitude"]}") ?? _inValidLocation, + longitude = double.tryParse("${json["longitude"]}") ?? _inValidLocation, + altitude = double.tryParse("${json["altitude"]}"), + speed = double.tryParse("${json["speed"]}"), + accuracy = double.tryParse("${json["accuracy"]}"), + speedAccuracy = double.tryParse("${json["speed_accuracy"]}"), + heading = double.tryParse("${json["heading"]}"), + number = "${json["number"]}", + street = "${json["street"]}", + locality = "${json["locality"]}", + sublocality = "${json["sublocality"]}", + postalCode = "${json["postal_code"]}", + subregion = "${json["subregion"]}", + region = "${json["region"]}", + country = "${json["country"]}" { + if (slug == null && name != null) { + slug = slugify(name!); + } + } + + /// Get a GeoPoint from [LatLng] coordinates + /// + /// [name] is the name of this [GeoPoint] and + /// [point] is a [LatLng] coordinate + GeoPoint.fromLatLng({required LatLng point, this.name}) + : latitude = point.latitude, + longitude = point.longitude { + if (name != null) slug = slugify(name!); + } + + /// Get a json map from this geo point + /// + /// [withId] include the id of the geo point or not + Map toMap({bool withId = true}) { + final json = { + "name": name, + "timestamp": timestamp, + "latitude": latitude, + "longitude": longitude, + "altitude": altitude, + "speed": speed, + "heading": heading, + "accuracy": accuracy, + "speed_accuracy": speedAccuracy, + "number": number, + "street": street, + "locality": locality, + "sublocality": sublocality, + "postal_code": postalCode, + "subregion": subregion, + "region": region, + "country": country, + }; + if (withId) json["id"] = id; + return json; + } + + /// Get a strings map from this geo point + /// + /// [withId] include the id of the geo point or not + Map toStringsMap({bool withId = true}) { + final json = { + "name": "$name", + "timestamp": "$timestamp", + "latitude": "$latitude", + "longitude": "$longitude", + "altitude": "$altitude", + "speed": "$speed", + "heading": "$heading", + "accuracy": "$accuracy", + "speed_accuracy": "$speedAccuracy", + "number": "$number", + "street": "$street", + "locality": "$locality", + "sublocality": "$sublocality", + "postal_code": "$postalCode", + "subregion": "$subregion", + "region": "$region", + "country": "$country", + }; + if (withId) json["id"] = "$id"; + return json; + } + + /// Convert this [GeoPoint] to a [LatLng] object + LatLng? toLatLng({bool ignoreErrors = false}) { + LatLng? latLng; + try { + latLng = LatLng(latitude, longitude); + } catch (e) { + if (!ignoreErrors) { + rethrow; + } + } + return latLng; + } + + /// Convert to a geojson feature string + String toGeoJsonFeatureString() => _toGeoJsonFeatureString("Point"); + + String _toGeoJsonFeatureString(String type) { + return '{"type":"Feature","properties":{"name":"$name"},' + '"geometry":{"type":"$type",' + '"coordinates":' + + toGeoJsonCoordinatesString() + + '}}'; + } + + /// Convert to a geojson coordinates string + String toGeoJsonCoordinatesString() { + return '[$longitude,$latitude]'; + } + + /// Get a formatted address from this geo point + String _getAddress() { + return "$number $street $locality " + "$postalCode $subregion $region $country"; + } + + /// Convert this geo point to string + @override + String toString() { + String? n; + if (name != null) { + n = name; + } else { + n = "$latitude/$longitude"; + } + return "Geopoint $n"; + } + + /// Convert this geo point to detailed string + String details() { + var str = "Geopoint: $name\n"; + str += "Lat: $latitude\n"; + str += "Lon: $longitude\n"; + str += "Altitude: $altitude\n"; + str += "Speed: $speed\n"; + str += "Heading: $heading\n"; + return str; + } +} diff --git a/lib/src/geopoint/geoserie.dart b/lib/src/geopoint/geoserie.dart new file mode 100644 index 0000000..27bc532 --- /dev/null +++ b/lib/src/geopoint/geoserie.dart @@ -0,0 +1,177 @@ +import 'dart:convert'; + +import 'package:latlong2/latlong.dart'; + +import 'geopoint.dart'; + +/// The type of the geoserie: group of points, line or polygon +enum GeoSerieType { + /// A group of geo points + group, + + /// A group of geo points forming a line + line, + + /// A group of geo points forming a polygon + polygon, +} + +/// [GeoSerieType] Extension +extension GeoSerieTypeExtension on GeoSerieType { + /// String value of [GeoSerieType] Enum. + String stringValue() { + switch (this) { + case GeoSerieType.group: + return "group"; + case GeoSerieType.line: + return "line"; + case GeoSerieType.polygon: + return "polygon"; + } + } + + /// feature String value of [GeoSerieType] Enum. + String featureString() { + switch (this) { + case GeoSerieType.group: + return "MultiPoint"; + case GeoSerieType.line: + return "LineString"; + case GeoSerieType.polygon: + return "Polygon"; + } + } +} + +/// static method to convert string to [GeoSerieType]. +GeoSerieType _typeFromString(String? typeStr) { + switch (typeStr?.toLowerCase()) { + case "group": + return GeoSerieType.group; + case "line": + return GeoSerieType.line; + case "polygon": + return GeoSerieType.polygon; + case "multipoint": + return GeoSerieType.group; + case "linestring": + return GeoSerieType.line; + } + throw Exception("Invalid Type"); +} + +/// A class to hold information about a serie of [GeoPoint] +class GeoSerie { + /// Default constructor: requires a [name] and a [type] + GeoSerie( + {required this.name, + required this.type, + this.id, + this.surface, + this.boundary, + this.centroid, + List? geoPoints}) + : this.geoPoints = geoPoints ?? []; + + /// Name if the geoserie + String name; + + /// Id of the geoserie + int? id; + + /// Type of the geoserie + GeoSerieType type; + + /// The list of [GeoPoint] in the serie + List geoPoints; + + /// The surface of a geometry + num? surface; + + /// Boundaries of a geometry + GeoSerie? boundary; + + /// The centroid of a geometry + GeoPoint? centroid; + + /// The type of the serie as a string + String get typeStr => type.stringValue(); + + /// Make a [GeoSerie] from json data + GeoSerie.fromJson(Map json) + : name = "${json["name"]}", + id = int.parse("${json["id"]}"), + surface = double.tryParse("${json["surface"]}"), + type = _typeFromString("${json["type"]}"), + this.geoPoints = []; + + /// Make a [GeoSerie] from name and serie type + GeoSerie.fromNameAndType( + {required this.name, required String typeStr, this.id}) + : type = _typeFromString(typeStr), + this.geoPoints = []; + + /// [name] the name of the [GeoSerie] + /// [typeStr] the type of the serie: group, line or polygon + /// [id] the id of the serie + + /// Get a json map from this [GeoSerie] + Map toMap({bool withId = true}) { + /// [withId] include the id in the result + final json = { + "name": name, + "type": typeStr, + "surface": surface + }; + if (withId) { + json["id"] = id; + } + return json; + } + + /// Get a list of [LatLng] from this [GeoSerie] + List toLatLng({bool ignoreErrors = false}) { + final points = []; + for (final geoPoint in geoPoints) { + try { + points.add(geoPoint.point); + } catch (_) { + if (!ignoreErrors) { + rethrow; + } + } + } + return points; + } + + /// Convert to a geojson coordinates string + String toGeoJsonCoordinatesString() { + final coords = []; + + for (final geoPoint in geoPoints) { + coords.add(geoPoint.toGeoJsonCoordinatesString()); + } + return "[" + coords.join(",") + "]"; + } + + /// Convert to a geojson feature string + String toGeoJsonFeatureString(Map? properties) => + _buildGeoJsonFeature(type, properties ?? {"name": name}); + + String _buildGeoJsonFeature( + GeoSerieType type, Map properties) { + var extra1 = ""; + var extra2 = ""; + if (type == GeoSerieType.polygon) { + extra1 = "["; + extra2 = "]"; + } + return '{"type":"Feature","properties":${jsonEncode(properties)},' + '"geometry":{"type":"${type.featureString()}",' + '"coordinates":' + + extra1 + + toGeoJsonCoordinatesString() + + extra2 + + '}}'; + } +} diff --git a/lib/src/iso/exceptions.dart b/lib/src/iso/exceptions.dart new file mode 100644 index 0000000..23e731b --- /dev/null +++ b/lib/src/iso/exceptions.dart @@ -0,0 +1,8 @@ +/// An exception for code running in an isolate +class IsolateRuntimeError implements Exception { + /// Provide a message + IsolateRuntimeError(this.message); + + /// The error message + final String message; +} diff --git a/lib/src/iso/iso.dart b/lib/src/iso/iso.dart new file mode 100644 index 0000000..548882a --- /dev/null +++ b/lib/src/iso/iso.dart @@ -0,0 +1,107 @@ +import 'dart:async'; +import "dart:isolate"; + +import 'exceptions.dart'; +import 'runner.dart'; + +/// Data processing function type +typedef IsoOnData = void Function(dynamic data); + +/// The isolate runner class +class Iso { + /// If [onDataOut] is not provided the data coming from the isolate + /// will print to the screen by default + Iso(this.runFunction, {this.onDataOut, this.onError}) + : _fromIsolateReceivePort = ReceivePort(), + _fromIsolateErrorPort = ReceivePort() { + onDataOut ??= (dynamic data) => null; + onError ??= + (dynamic err) => throw IsolateRuntimeError("Error in isolate:\n $err"); + } + + /// The function to run in the isolate + final void Function(IsoRunner) runFunction; + + /// The handler for the data coming from the isolate + IsoOnData? onDataOut; + + /// The handler for the errors coming from the isolate + IsoOnData? onError; + + Isolate? _isolate; + final ReceivePort _fromIsolateReceivePort; + final ReceivePort _fromIsolateErrorPort; + SendPort? _toIsolateSendPort; + final StreamController _dataOutIsolate = StreamController(); + final Completer _isolateReadyToListenCompleter = Completer(); + bool _canReceive = false; + + /// A stream with the data coming out from the isolate + Stream get dataOut => _dataOutIsolate.stream; + + /// Working state callback + Future get onCanReceive => _isolateReadyToListenCompleter.future; + + /// The state of the isolate + bool get canReceive => _canReceive; + + /// Send data to the isolate + void send(dynamic data) { + assert(_toIsolateSendPort != null); + _toIsolateSendPort!.send(data); + } + + /// Run the isolate + Future run([List args = const []]) async { + //print("I > run"); + final _comChanCompleter = Completer(); + // set runner config + final runner = IsoRunner(chanOut: _fromIsolateReceivePort.sendPort); + if (args.isNotEmpty) runner.args = args; + // run + await Isolate.spawn(runFunction, runner, + onError: _fromIsolateErrorPort.sendPort) + .then((Isolate _is) { + _isolate = _is; + _fromIsolateReceivePort.listen((dynamic data) { + if (_toIsolateSendPort == null && data is SendPort) { + _toIsolateSendPort = data; + //print("I > com port received $data"); + _comChanCompleter.complete(); + } else { + //print("I > DATA OUT $data"); + _dataOutIsolate.sink.add(data); + onDataOut!(data); + } + }, onError: (dynamic err) { + _fromIsolateErrorPort.sendPort.send(err); + }); + _fromIsolateErrorPort.listen((dynamic err) { + onError!(err); + }); + //print("I > init data in"); + //runner.initDataIn(); + return; + }); + await _comChanCompleter.future; + _isolateReadyToListenCompleter.complete(); + _canReceive = true; + } + + /// Kill the isolate + void _kill() { + //print("Killing $_isolate"); + if (_isolate != null) { + _fromIsolateReceivePort.close(); + _fromIsolateErrorPort.close(); + _isolate!.kill(priority: Isolate.immediate); + _isolate = null; + } + } + + /// Cleanup + void dispose() { + _kill(); + _dataOutIsolate.close(); + } +} diff --git a/lib/src/iso/runner.dart b/lib/src/iso/runner.dart new file mode 100644 index 0000000..031c9d9 --- /dev/null +++ b/lib/src/iso/runner.dart @@ -0,0 +1,34 @@ +import 'dart:isolate'; + +/// The isolate runner +class IsoRunner { + /// A [chanOut] has to be provided + IsoRunner({required this.chanOut, this.dataIn, this.args}) + : assert(chanOut != null); + + /// The [SendPort] to send data into the isolate + final SendPort chanOut; + + /// The [ReceivePort] to reveive data in the isolate + ReceivePort? dataIn; + + /// The arguments for the run function + List? args; + + /// Does the run function has arguments + bool get hasArgs => args!.isNotEmpty; + + /// Send data to the main thread + void send(dynamic data) => chanOut.send(data); + + /// Initialize the receive channel + /// + /// This must be done before sending messages into the isolate + /// after this the [Iso.onCanReceive] future will be completed + ReceivePort receive() { + final listener = ReceivePort(); + send(listener.sendPort); + dataIn = listener; + return listener; + } +} diff --git a/lib/src/models.dart b/lib/src/models.dart index 2ef00de..6927877 100644 --- a/lib/src/models.dart +++ b/lib/src/models.dart @@ -1,6 +1,8 @@ import 'dart:convert'; +import 'dart:math'; -import 'package:geopoint/geopoint.dart'; +import 'geopoint/geopoint.dart'; +import 'geopoint/geoserie.dart'; /// Geojson feature types enum GeoJsonFeatureType { @@ -298,13 +300,15 @@ enum GeoSearchType { /// A geojson query for search class GeoJsonQuery { /// Provide a [geometryType] and/or a [property] and [value] - GeoJsonQuery( - {this.property, - this.value, - this.geometryType, - this.matchCase = true, - this.searchType = GeoSearchType.exact}) { - if (geometryType == null) { + GeoJsonQuery({ + this.property, + this.value, + this.geometryType, + this.matchCase = true, + this.searchType = GeoSearchType.exact, + this.boundingBox, + }) { + if (geometryType == null && boundingBox == null) { if (property == null || value == null) { throw ArgumentError.notNull( "Property and value must not be null if no geometry " @@ -327,6 +331,53 @@ class GeoJsonQuery { /// Match the case of string or not final bool matchCase; + + /// Bounding box to search for features that overlap + final GeoBoundingBox? boundingBox; +} + +/// A Geo Bounding Box used for search +class GeoBoundingBox { + /// Creates a new GeoBoundingBox instance with the supplied min/max coordinates + GeoBoundingBox({required this.coords}) { + if (coords[0] > coords[2]) { + throw ArgumentError.value( + coords[0], "Min longitude larger than max longitude"); + } + if (coords[1] > coords[3]) { + throw ArgumentError.value( + coords[0], "Min latitude larger than max latitude"); + } + } + + /// Coordinates of the bounding box + /// [min longitude, min latitude, max longitude, max latitude] + final List coords; + + /// Checks if any of the points are withing the bounds defined by the bounding box + bool isOverlapping(Iterable points) { + // check if bounding box rectangle contains any of the provided points + final minLon = coords[0]; + final maxLon = coords[2]; + final minLat = coords[1]; + final maxLat = coords[3]; + + final pMinLon = points.map((e) => e.longitude).reduce(min); + final pMaxLon = points.map((e) => e.longitude).reduce(max); + final pMinLat = points.map((e) => e.latitude).reduce(min); + final pMaxLat = points.map((e) => e.latitude).reduce(max); + + // check if bounding box rectangle is outside the other, if it is then it's + // considered not overlapping + if (minLat > pMaxLat || + maxLat < pMinLat || + minLon > pMaxLon || + maxLon < pMinLon) { + return false; + } + + return true; + } } String _buildGeoJsonFeature( diff --git a/lib/src/slug/replacements.dart b/lib/src/slug/replacements.dart new file mode 100644 index 0000000..2e0745f --- /dev/null +++ b/lib/src/slug/replacements.dart @@ -0,0 +1,458 @@ +/// List of common character replacements. +const replacements = { + '¹': '1', + '²': '2', + '³': '3', + 'º': 'o', + '°': '0', + 'æ': 'ae', + 'ǽ': 'ae', + 'À': 'A', + 'Á': 'A', + 'Â': 'A', + 'Ã': 'A', + 'Å': 'A', + 'Ǻ': 'A', + 'Ă': 'A', + 'Ǎ': 'A', + 'Ạ': 'A', + 'Ả': 'A', + 'ả': 'a', + 'ạ': 'a', + 'Ầ': 'A', + 'ầ': 'a', + 'Ẩ': 'A', + 'ẩ': 'a', + 'Ẫ': 'A', + 'ẫ': 'a', + 'Ậ': 'A', + 'ậ': 'a', + 'Ắ': 'A', + 'ắ': 'a', + 'Ằ': 'Ằ', + 'ằ': 'ằ', + 'Ẳ': 'A', + 'ẳ': 'a', + 'Ẵ': 'A', + 'ẵ': 'a', + 'Ặ': 'A', + 'ặ': 'a', + 'Æ': 'AE', + 'Ǽ': 'AE', + 'à': 'a', + 'á': 'a', + 'â': 'a', + 'ã': 'a', + 'å': 'a', + 'ǻ': 'a', + 'ă': 'a', + 'ǎ': 'a', + 'ª': 'a', + '@': 'at', + '&': 'and', + 'Ĉ': 'CX', + 'Ċ': 'C', + 'ĉ': 'cx', + 'ċ': 'c', + '©': 'c', + 'Ð': 'Dj', + 'Đ': 'Dj', + 'ð': 'dj', + 'đ': 'dj', + 'È': 'E', + 'É': 'E', + 'Ê': 'E', + 'Ë': 'E', + 'Ĕ': 'E', + 'Ė': 'E', + 'è': 'e', + 'é': 'e', + 'ê': 'e', + 'ë': 'e', + 'ĕ': 'e', + 'ė': 'e', + 'Ệ': 'E', + 'ệ': 'e', + 'Ể': 'E', + 'ể': 'e', + 'Ẹ': 'E', + 'ẻ': 'e', + 'Ẻ': 'E', + 'ẽ': 'e', + 'Ẽ': 'E', + 'ễ': 'e', + 'Ễ': 'E', + 'ẹ': 'e', + 'ƒ': 'f', + 'Ĝ': 'GX', + 'Ġ': 'G', + 'ĝ': 'gx', + 'ġ': 'g', + 'Ĥ': 'HX', + 'Ħ': 'H', + 'ĥ': 'hx', + 'ħ': 'h', + 'Ì': 'I', + 'Í': 'I', + 'Î': 'I', + 'Ï': 'I', + 'Ĩ': 'I', + 'Ĭ': 'I', + 'Ǐ': 'I', + 'Į': 'I', + 'IJ': 'IJ', + 'ì': 'i', + 'í': 'i', + 'î': 'i', + 'ï': 'i', + 'ĩ': 'i', + 'ĭ': 'i', + 'ǐ': 'i', + 'į': 'i', + 'Ỉ': 'I', + 'ỉ': 'i', + 'Ị': 'I', + 'ị': 'i', + 'ij': 'ij', + 'Ĵ': 'JX', + 'ĵ': 'jx', + 'Ĺ': 'L', + 'Ľ': 'L', + 'Ŀ': 'L', + 'ĺ': 'l', + 'ľ': 'l', + 'ŀ': 'l', + 'Ñ': 'N', + 'ñ': 'n', + 'ʼn': 'n', + 'Ò': 'O', + 'Ô': 'O', + 'Õ': 'O', + 'Ō': 'O', + 'Ŏ': 'O', + 'Ǒ': 'O', + 'Ő': 'O', + 'Ơ': 'O', + 'Ø': 'O', + 'Ǿ': 'O', + 'Œ': 'OE', + 'ò': 'o', + 'ô': 'o', + 'õ': 'o', + 'ō': 'o', + 'ŏ': 'o', + 'ǒ': 'o', + 'ő': 'o', + 'ơ': 'o', + 'ø': 'o', + 'ǿ': 'o', + 'Ọ': 'O', + 'ọ': 'o', + 'Ổ': 'O', + 'ỗ': 'o', + 'Ỗ': 'o', + 'ổ': 'o', + 'Ộ': 'O', + 'ộ': 'o', + 'ợ': 'o', + 'Ợ': 'o', + 'Ở': 'O', + 'ở': 'o', + 'Ỡ': 'O', + 'ỡ': 'o', + 'œ': 'oe', + 'Ŕ': 'R', + 'Ŗ': 'R', + 'ŕ': 'r', + 'ŗ': 'r', + 'Ŝ': 'SX', + 'Ș': 'S', + 'ŝ': 'sx', + 'ș': 's', + 'ſ': 's', + 'Ţ': 'T', + 'Ț': 'T', + 'Ŧ': 'T', + 'Þ': 'TH', + 'ţ': 't', + 'ț': 't', + 'ŧ': 't', + 'þ': 'th', + 'Ù': 'U', + 'Ú': 'U', + 'Û': 'U', + 'Ũ': 'U', + 'Ŭ': 'UX', + 'Ű': 'U', + 'Ų': 'U', + 'Ư': 'U', + 'Ǔ': 'U', + 'Ǖ': 'U', + 'Ǘ': 'U', + 'Ǚ': 'U', + 'Ǜ': 'U', + 'ù': 'u', + 'ú': 'u', + 'û': 'u', + 'ũ': 'u', + 'ŭ': 'ux', + 'ű': 'u', + 'ų': 'u', + 'ư': 'u', + 'ǔ': 'u', + 'ǖ': 'u', + 'ǘ': 'u', + 'ǚ': 'u', + 'ǜ': 'u', + 'Ụ': 'U', + 'Ủ': 'U', + 'ủ': 'u', + 'ụ': 'u', + 'Ŵ': 'W', + 'ŵ': 'w', + 'Ý': 'Y', + 'Ÿ': 'Y', + 'Ŷ': 'Y', + 'ý': 'y', + 'ÿ': 'y', + 'ŷ': 'y', + 'Ъ': '', + 'Ь': '', + 'А': 'A', + 'Б': 'B', + 'Ц': 'C', + 'Ч': 'Ch', + 'Д': 'D', + 'Е': 'E', + 'Ё': 'E', + 'Э': 'E', + 'Ф': 'F', + 'Г': 'G', + 'Х': 'H', + 'И': 'I', + 'Й': 'J', + 'Я': 'Ja', + 'Ю': 'Ju', + 'К': 'K', + 'Л': 'L', + 'М': 'M', + 'Н': 'N', + 'О': 'O', + 'П': 'P', + 'Р': 'R', + 'С': 'S', + 'Ш': 'Sh', + 'Щ': 'Shch', + 'Т': 'T', + 'У': 'U', + 'В': 'V', + 'Ы': 'Y', + 'З': 'Z', + 'Ж': 'Zh', + 'ъ': '', + 'ь': '', + 'а': 'a', + 'б': 'b', + 'ц': 'c', + 'ч': 'ch', + 'д': 'd', + 'е': 'e', + 'ё': 'e', + 'э': 'e', + 'ф': 'f', + 'г': 'g', + 'х': 'h', + 'и': 'i', + 'й': 'j', + 'я': 'ja', + 'ю': 'ju', + 'к': 'k', + 'л': 'l', + 'м': 'm', + 'н': 'n', + 'о': 'o', + 'п': 'p', + 'р': 'r', + 'с': 's', + 'ш': 'sh', + 'щ': 'shch', + 'т': 't', + 'у': 'u', + 'в': 'v', + 'ы': 'y', + 'з': 'z', + 'ж': 'zh', + 'Ä': 'AE', + 'Ö': 'OE', + 'Ü': 'UE', + 'ß': 'ss', + 'ä': 'ae', + 'ö': 'oe', + 'ü': 'ue', + 'Ç': 'C', + 'Ğ': 'G', + 'İ': 'I', + 'Ş': 'S', + 'ç': 'c', + 'ğ': 'g', + 'ı': 'i', + 'ş': 's', + 'Ā': 'A', + 'Ē': 'E', + 'Ģ': 'G', + 'Ī': 'I', + 'Ķ': 'K', + 'Ļ': 'L', + 'Ņ': 'N', + 'Ū': 'U', + 'ā': 'a', + 'ē': 'e', + 'ģ': 'g', + 'ī': 'i', + 'ķ': 'k', + 'ļ': 'l', + 'ņ': 'n', + 'ū': 'u', + 'Ґ': 'G', + 'І': 'I', + 'Ї': 'Ji', + 'Є': 'Ye', + 'ґ': 'g', + 'і': 'i', + 'ї': 'ji', + 'є': 'ye', + 'Č': 'C', + 'Ď': 'Dj', + 'Ě': 'E', + 'Ň': 'N', + 'Ř': 'R', + 'Š': 'S', + 'Ť': 'T', + 'Ů': 'U', + 'Ž': 'Z', + 'č': 'c', + 'ď': 'dj', + 'ě': 'e', + 'ň': 'n', + 'ř': 'r', + 'š': 's', + 'ť': 't', + 'ů': 'u', + 'ž': 'z', + 'Ą': 'A', + 'Ć': 'C', + 'Ę': 'E', + 'Ł': 'L', + 'Ń': 'N', + 'Ó': 'O', + 'Ś': 'S', + 'Ź': 'Z', + 'Ż': 'Z', + 'ą': 'a', + 'ć': 'c', + 'ę': 'e', + 'ł': 'l', + 'ń': 'n', + 'ó': 'o', + 'ś': 's', + 'ź': 'z', + 'ż': 'z', + 'Α': 'A', + 'Β': 'B', + 'Γ': 'G', + 'Δ': 'D', + 'Ε': 'E', + 'Ζ': 'Z', + 'Η': 'E', + 'Θ': 'Th', + 'Ι': 'I', + 'Κ': 'K', + 'Λ': 'L', + 'Μ': 'M', + 'Ν': 'N', + 'Ξ': 'X', + 'Ο': 'O', + 'Π': 'P', + 'Ρ': 'R', + 'Σ': 'S', + 'Τ': 'T', + 'Υ': 'Y', + 'Ỷ': 'Y', + 'ỷ': 'y', + 'Ỹ': 'Y', + 'ỹ': 'y', + 'Ỵ': 'Y', + 'ỵ': 'y', + 'Φ': 'Ph', + 'Χ': 'Ch', + 'Ψ': 'Ps', + 'Ω': 'O', + 'Ϊ': 'I', + 'Ϋ': 'Y', + 'ά': 'a', + 'έ': 'e', + 'ή': 'e', + 'ί': 'i', + 'ΰ': 'Y', + 'α': 'a', + 'β': 'b', + 'γ': 'g', + 'δ': 'd', + 'ε': 'e', + 'ζ': 'z', + 'η': 'e', + 'θ': 'th', + 'ι': 'i', + 'κ': 'k', + 'λ': 'l', + 'μ': 'm', + 'ν': 'n', + 'ξ': 'x', + 'ο': 'o', + 'π': 'p', + 'ρ': 'r', + 'ς': 's', + 'σ': 's', + 'τ': 't', + 'υ': 'y', + 'φ': 'ph', + 'χ': 'ch', + 'ψ': 'ps', + 'ω': 'o', + 'ϊ': 'i', + 'ϋ': 'y', + 'ό': 'o', + 'ύ': 'y', + 'ώ': 'o', + 'ϐ': 'b', + 'ϑ': 'th', + 'ϒ': 'Y', + 'أ': 'a', + 'ب': 'b', + 'ت': 't', + 'ث': 'th', + 'ج': 'g', + 'ح': 'h', + 'خ': 'kh', + 'د': 'd', + 'ذ': 'th', + 'ر': 'r', + 'ز': 'z', + 'س': 's', + 'ش': 'sh', + 'ص': 's', + 'ض': 'd', + 'ط': 't', + 'ظ': 'th', + 'ع': 'aa', + 'غ': 'gh', + 'ف': 'f', + 'ق': 'k', + 'ك': 'k', + 'ل': 'l', + 'م': 'm', + 'ن': 'n', + 'ه': 'h', + 'و': 'o', + 'ي': 'y' +}; diff --git a/lib/src/slug/slugify.dart b/lib/src/slug/slugify.dart new file mode 100644 index 0000000..4a631cd --- /dev/null +++ b/lib/src/slug/slugify.dart @@ -0,0 +1,28 @@ +import 'replacements.dart'; + +final _dupeSpaceRegExp = RegExp(r'\s{2,}'); +final _punctuationRegExp = RegExp(r'[^\w\s-]'); + +/// Converts [text] to a slug [String] separated by the [delimiter]. +String slugify(String text, {String delimiter = '-', bool lowercase = true}) { + // Trim leading and trailing whitespace. + var slug = text.trim(); + + // Make the text lowercase (optional). + if (lowercase) { + slug = slug.toLowerCase(); + } + + // Substitute characters for their latin equivalent. + replacements.forEach((k, v) => slug = slug.replaceAll(k, v)); + + slug = slug + // Condense whitespaces to 1 space. + .replaceAll(_dupeSpaceRegExp, ' ') + // Remove punctuation. + .replaceAll(_punctuationRegExp, '') + // Replace space with the delimiter. + .replaceAll(' ', delimiter); + + return slug; +} diff --git a/pubspec.yaml b/pubspec.yaml index b69a814..56f0fec 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,17 +2,15 @@ name: geojson description: Utilities to work with geojson data. Parser with a reactive api, search and geofencing homepage: https://github.com/synw/geojson -version: 1.0.0 +version: 1.1.0 environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=2.12.0 <4.0.0" dependencies: - geodesy: ^0.4.0-nullsafety.0 - geopoint: ^1.0.0 - iso: ^1.0.0 - pedantic: ^1.11.1 + geodesy: ^0.4.0 + pedantic: ^1.11.0 dev_dependencies: - extra_pedantic: ^1.4.0 - test: ^1.17.4 + extra_pedantic: ^3.0.0 + test: ^1.24.0