Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ports lineToPolygon, polygonToLine, and their tests #104

Merged
merged 12 commits into from
Jul 4, 2022
3 changes: 3 additions & 0 deletions lib/line_to_polygon.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
library turf_line_to_polygon.dart;

export 'src/line_to_polygon.dart';
3 changes: 3 additions & 0 deletions lib/polygon_to_line.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
library turf_polygon_to_line;

export 'src/polygon_to_line.dart';
186 changes: 186 additions & 0 deletions lib/src/line_to_polygon.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import 'package:turf/bbox.dart';
import 'package:turf/helpers.dart';
import 'package:turf/meta.dart';
import 'package:turf/src/invariant.dart';

/// Converts [LineString]s & [MultiLineString](s) to [Polygon] or [MultiPolygon].
/// Takes an optional bool autoComplete=true that auto complete [Linestring]s (matches first & last coordinates)
/// Takes an optional orderCoords=true that sorts [Linestring]s to place outer ring at the first position of the coordinates
/// Takes an optional mutate=false that mutates the original [Linestring] using autoComplete (matches first & last coordinates)
/// Returns [Feature<Polygon>] or [Feature<MultiPolygon>] converted to Polygons.
/// example:
/// ```dart
/// var line = LineString(coordinates: [
/// Position.of([125, -30]),
/// Position.of([145, -30]),
/// Position.of([145, -20]),
/// Position.of([125, -20]),
/// Position.of([125, -30])]);
/// var polygon = lineToPolygon(line);
/// //addToMap
/// var addToMap = [polygon];
/// ```
Feature lineToPolygon(
GeoJSONObject lines, {
Map<String, dynamic>? properties,
bool autoComplete = true,
bool orderCoords = true,
bool mutate = false,
}) {
Exception exc = Exception(
"""allowed types are Feature<LineString||MultiLineString>, LineString,
MultiLineString, FeatureCollection<LineString || MultiLineString>""");
if (lines is FeatureCollection) {
featureEach(
lines,
(currentFeature, index) {
if (currentFeature.geometry is! LineString &&
currentFeature.geometry is! MultiLineString) {
throw exc;
}
},
);
List<List<Position>> list = [];
geomEach(
lines,
(
GeometryType? currentGeometry,
int? featureIndex,
Map<String, dynamic>? featureProperties,
BBox? featureBBox,
dynamic featureId,
) {
if (currentGeometry is LineString) {
list.add(currentGeometry.coordinates.map((e) => e.clone()).toList());
} else if (currentGeometry is MultiLineString) {
list = [
...list,
...currentGeometry.coordinates
.map((e) => e.map((p) => p.clone()).toList())
.toList()
];
} else {
throw Exception("$currentGeometry type is not supperted");
}
},
);

lines = FeatureCollection<MultiLineString>(features: [])
..features.add(Feature(geometry: MultiLineString(coordinates: list)));
} else if (lines is Feature) {
if (lines.geometry is LineString) {
lines = Feature<LineString>(
geometry: lines.geometry as LineString,
properties: lines.properties,
id: lines.id,
);
} else if (lines.geometry is MultiLineString) {
lines = Feature<MultiLineString>(
geometry: lines.geometry as MultiLineString,
properties: lines.properties,
id: lines.id,
);
} else {
throw exc;
}
} else if (lines is LineString) {
lines = Feature<LineString>(geometry: lines);
} else if (lines is MultiLineString) {
lines = Feature<MultiLineString>(geometry: lines);
} else {
throw exc;
}
if (!mutate) {
lines = lines.clone();
}

if (lines is FeatureCollection) {
List<List<List<Position>>> coords = [];
featureEach(
lines,
((line, featureIndex) => coords.add(getCoords(lineStringToPolygon(
line, autoComplete, orderCoords, properties: {}))
as List<List<Position>>)),
);
return Feature(
geometry: MultiPolygon(coordinates: coords), properties: properties);
} else {
return lineStringToPolygon(lines, autoComplete, orderCoords,
properties: properties);
}
}

/// Converts LineString to Polygon
/// Takes a optional bool autoComplete=true that auto completes linestrings
/// Takes an optional orderCoords=true that sorts linestrings to place outer
/// ring at the first position of the coordinates.
Feature<Polygon> lineStringToPolygon(
GeoJSONObject line,
bool autoComplete,
bool orderCoords, {
Map<String, dynamic>? properties,
}) {
properties = properties ?? (line is Feature ? line.properties ?? {} : {});

if (line is Feature && line.geometry != null) {
return lineStringToPolygon(line.geometry!, autoComplete, orderCoords);
}

if (line is GeometryType && line.coordinates.isEmpty) {
throw Exception("line must contain coordinates");
}

if (line is LineString) {
var coords =
autoComplete ? _autoCompleteCoords(line.coordinates) : line.coordinates;

return Feature(
geometry: Polygon(coordinates: [coords]), properties: properties);
} else if (line is MultiLineString) {
List<List<Position>> multiCoords = [];
num largestArea = 0;

line.coordinates.forEach(
(coord) {
if (autoComplete) {
coord = _autoCompleteCoords(coord);
}

// Largest LineString to be placed in the first position of the coordinates array
if (orderCoords) {
var area = _calculateArea(bbox(LineString(coordinates: coord)));
if (area > largestArea) {
multiCoords.insert(0, coord);
largestArea = area;
} else {
multiCoords.add(coord);
}
} else {
multiCoords.add(coord);
}
},
);
return Feature(
geometry: Polygon(coordinates: multiCoords), properties: properties);
} else {
throw Exception("Geometry type ${line.type} is not supported");
}
}

/// Auto Completes Coords - matches first & last coordinates
List<Position> _autoCompleteCoords(List<Position> coords) {
var newCoords = coords.map((c) => c.clone()).toList();
if (newCoords.first != newCoords.last) {
newCoords.add(newCoords.first.clone());
}
return newCoords;
}

/// Quick calculates approximate area (used to sort)
num _calculateArea(BBox bbox) {
var west = bbox[0];
var south = bbox[1];
var east = bbox[2];
var north = bbox[3];
return (west! - east!).abs() * (south! - north!).abs();
}
65 changes: 65 additions & 0 deletions lib/src/polygon_to_line.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import '../helpers.dart';

armantorkzaban marked this conversation as resolved.
Show resolved Hide resolved
/// Converts a [Polygon] to [LineString] or [MultiLineString] or a [MultiPolygon] to a
/// [FeatureCollection] of [LineString] or [MultiLineString].
/// Returns [FeatureCollection] or [Feature<LineString>] or [Feature<MultiLinestring>]
/// example:
/// ```dart
/// var poly = Polygon(coordinates:
/// [
/// [
/// Position.of([125, -30]),
/// Position.of([145, -30]),
/// Position.of([145, -20]),
/// Position.of([125, -20]),
/// Position.of([125, -30])
/// ]
/// ]);
/// var line = polygonToLine(poly);
/// //addToMap
/// var addToMap = [line];
/// ```
GeoJSONObject polygonToLine(GeoJSONObject poly,
armantorkzaban marked this conversation as resolved.
Show resolved Hide resolved
{Map<String, dynamic>? properties}) {
var geom = poly is Feature ? poly.geometry : poly;

properties =
properties ?? ((poly is Feature) ? poly.properties : <String, dynamic>{});

if (geom is Polygon) {
return _polygonToLine(geom, properties: properties);
} else if (geom is MultiPolygon) {
return _multiPolygonToLine(geom, properties: properties);
} else {
throw Exception("invalid poly");
}
}

Feature _polygonToLine(Polygon geom, {Map<String, dynamic>? properties}) {
var coords = geom.coordinates;
properties = properties ?? <String, dynamic>{};

return _coordsToLine(coords, properties);
}

FeatureCollection _multiPolygonToLine(MultiPolygon geom,
{Map<String, dynamic>? properties}) {
var coords = geom.coordinates;
properties = properties ?? <String, dynamic>{};

var lines = <Feature>[];
coords.forEach((coord) => {lines.add(_coordsToLine(coord, properties))});
return FeatureCollection(features: lines);
}

Feature _coordsToLine(
List<List<Position>> coords, Map<String, dynamic>? properties) {
if (coords.isEmpty) {
throw RangeError("coordinates is empty");
} else if (coords.length > 1) {
return Feature(
geometry: MultiLineString(coordinates: coords), properties: properties);
}
return Feature(
geometry: LineString(coordinates: coords[0]), properties: properties);
}
127 changes: 127 additions & 0 deletions test/components/line_to_polygon_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import 'dart:convert';
import 'dart:io';

import 'package:test/test.dart';
import 'package:turf/helpers.dart';
import 'package:turf/src/line_to_polygon.dart';

void main() {
group(
'line_to_polygon:',
() {
var inDir = Directory('./test/examples/lineToPolygon/in');
for (var file in inDir.listSync(recursive: true)) {
if (file is File && file.path.endsWith('.geojson')) {
test(file.path, () {
var inSource = file.readAsStringSync();
var inGeom = GeoJSONObject.fromJson(jsonDecode(inSource));
var properties = (inGeom is Feature)
? inGeom.properties ?? {"stroke": "#F0F", "stroke-width": 6}
: {"stroke": "#F0F", "stroke-width": 6};
//print(inGeom);
var results = lineToPolygon(
inGeom,
properties: properties,
);

var outPath = './' +
file.uri.pathSegments
.sublist(0, file.uri.pathSegments.length - 2)
.join('/') +
'/out/${file.uri.pathSegments.last}';

var outSource = File(outPath).readAsStringSync();
var outGeom = GeoJSONObject.fromJson(jsonDecode(outSource));

if (outGeom is Feature) {
if (outGeom.geometry is Polygon) {
outGeom =
Feature<Polygon>(geometry: outGeom.geometry as Polygon);
} else {
outGeom = Feature<MultiPolygon>(
geometry: outGeom.geometry as MultiPolygon);
}
}
expect(results, equals(outGeom));
});
}
test(
'Handles Errors',
() {
expect(
() => lineToPolygon(Point(coordinates: Position.of([10, 5]))),
throwsA(isA<Exception>()));
expect(() => lineToPolygon(LineString(coordinates: [])),
throwsA(isA<Exception>()));
expect(
lineToPolygon(
LineString(coordinates: [
Position.of([10, 5]),
Position.of([20, 10]),
Position.of([30, 20]),
]),
autoComplete: false) is Feature<Polygon>,
true);
},
);
}
},
);
}

/*
const fs = require("fs");
const test = require("tape");
const path = require("path");
const load = require("load-json-file");
const write = require("write-json-file");
const { point, lineString } = require("@turf/helpers");
const clone = require("@turf/clone").default;
const lineToPolygon = require("./index").default;

const directories = {
in: path.join(__dirname, "test", "in") + path.sep,
out: path.join(__dirname, "test", "out") + path.sep,
};

let fixtures = fs.readdirSync(directories.in).map((filename) => {
return {
filename,
name: path.parse(filename).name,
geojson: load.sync(directories.in + filename),
};
});
// fixtures = fixtures.filter(fixture => fixture.name === 'multi-outGeomtrings-with-holes');

test("turf-outGeomtring-to-polygon", (t) => {
for (const { name, filename, geojson } of fixtures) {
const originalInput = clone(geojson);
let { autoComplete, properties, orderCoords } = geojson.properties || {};
properties = properties || { stroke: "#F0F", "stroke-width": 6 };
const results = lineToPolygon(geojson, {
properties: properties,
autoComplete: autoComplete,
orderCoords: orderCoords,
});

if (process.env.REGEN) write.sync(directories.out + filename, results);
t.deepEqual(load.sync(directories.out + filename), results, name);
t.deepEqual(originalInput, geojson);
}
// Handle Errors
t.throws(() => lineToPolygon(point([10, 5])), "throws - invalid geometry");
t.throws(() => lineToPolygon(lineString([])), "throws - empty coordinates");
t.assert(
lineToPolygon(
lineString([
[10, 5],
[20, 10],
[30, 20],
]),
{ autocomplete: false }
),
"is valid - autoComplete=false"
);
t.end();
});
*/
Loading