Repository: fitnr/visvalingamwyatt

Issue:
first & last position not the same in coordinates after simplify_geometry
# Environment
OS: mscOS Big Sur 11.1
Python: 3.8.6
pip list:
numpy            1.19.4
pip              20.2.4
setuptools       50.3.2
visvalingamwyatt 0.1.3
wheel            0.35.1

# Reproduce

```python
import pprint
import visvalingamwyatt as vw
pp = pprint.PrettyPrinter(sort_dicts=False)

test_data={"type":"Feature","properties":{},"geometry":{"type":"MultiPolygon","coordinates":[[[[121.20803833007811,24.75431413309125],[121.1846923828125,24.746831298412058],[121.1517333984375,24.74059525872194],[121.14486694335936,24.729369599118222],[121.12152099609375,24.693191139677126],[121.13525390625,24.66449040712424],[121.10504150390625,24.66449040712424],[121.10092163085936,24.645768980151793],[121.0748291015625,24.615808859044243],[121.09405517578125,24.577099744289427],[121.12564086914062,24.533381526147682],[121.14624023437499,24.515889973088104],[121.19018554687499,24.528384188171866],[121.19430541992186,24.57959746772822],[121.23687744140624,24.587090339209634],[121.24099731445311,24.552119771544227],[121.2451171875,24.525885444592642],[121.30279541015624,24.55087064225044],[121.27258300781251,24.58958786341259],[121.26708984374999,24.623299562653035],[121.32614135742188,24.62579636412304],[121.34674072265624,24.602074737077242],[121.36871337890625,24.580846310771612],[121.40853881835936,24.653257887871963],[121.40853881835936,24.724380091871726],[121.37283325195312,24.716895455859337],[121.3604736328125,24.693191139677126],[121.343994140625,24.69942955501979],[121.32888793945312,24.728122241065808],[121.3714599609375,24.743089712134605],[121.37695312499999,24.77177232822881],[121.35635375976562,24.792968265314457],[121.32476806640625,24.807927923059236],[121.29730224609375,24.844072974931866],[121.24923706054688,24.849057671305268],[121.24786376953125,24.816653556469955],[121.27944946289062,24.79047481357294],[121.30142211914061,24.761796517185815],[121.27258300781251,24.73311159823193],[121.25335693359374,24.708162811665265],[121.20391845703125,24.703172454280217],[121.19979858398438,24.731864277701714],[121.20803833007811,24.75431413309125]]]]}}

pp.pprint(vw.simplify_geometry(test_data['geometry'], number=10))
pp.pprint(vw.simplify_geometry(test_data['geometry'], ratio=0.3))
pp.pprint(vw.simplify_geometry(test_data['geometry'], threshold=0.01))
```

# output

```
{'type': 'MultiPolygon',
 'coordinates': [[[[121.20803833007811, 24.75431413309125],
                   [121.1517333984375, 24.74059525872194],
                   [121.0748291015625, 24.615808859044243],
                   [121.14624023437499, 24.515889973088104],
                   [121.26708984374999, 24.623299562653035],
                   [121.36871337890625, 24.580846310771612],
                   [121.40853881835936, 24.724380091871726],
                   [121.29730224609375, 24.844072974931866],
                   [121.30142211914061, 24.761796517185815],
                   [121.20391845703125, 24.703172454280217]]]]}
{'type': 'MultiPolygon',
 'coordinates': [[[[121.20803833007811, 24.75431413309125],
                   [121.1517333984375, 24.74059525872194],
                   [121.0748291015625, 24.615808859044243],
                   [121.14624023437499, 24.515889973088104],
                   [121.23687744140624, 24.587090339209634],
                   [121.2451171875, 24.525885444592642],
                   [121.30279541015624, 24.55087064225044],
                   [121.26708984374999, 24.623299562653035],
                   [121.36871337890625, 24.580846310771612],
                   [121.40853881835936, 24.724380091871726],
                   [121.29730224609375, 24.844072974931866],
                   [121.24786376953125, 24.816653556469955]]]]}
{'type': 'MultiPolygon',
 'coordinates': [[[[121.20803833007811, 24.75431413309125],
                   [121.0748291015625, 24.615808859044243],
                   [121.14624023437499, 24.515889973088104],
                   [121.36871337890625, 24.580846310771612],
                   [121.40853881835936, 24.724380091871726],
                   [121.29730224609375, 24.844072974931866],
                   [121.20803833007811, 24.75431413309125]]]]}
```

From the result above, simplify_geometry works perfectly with threshold (3rd json).
But for number and ratio, the last position is not the same as first positon.
So... it's a bug or I do this wrong?


Comments:

Patch:
diff --git a/.travis.yml b/.travis.yml
index 1bfad4d..1c406f5 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -8,12 +8,9 @@
 language: python
 
 python:
-    - 2.7
-    - 3.4
-    - 3.5
-    - 3.6
     - 3.7
     - 3.8
+    - 3.9
 
 matrix:
     fast_finish: true
diff --git a/Makefile b/Makefile
index 8bced23..ce32e95 100644
--- a/Makefile
+++ b/Makefile
@@ -21,8 +21,8 @@ cov:
 	coverage report
 	coverage html
 
-test: README.rst
-	python setup.py test	
+test:
+	tox
 	
 upload: README.rst | clean build
 	twine upload dist/*
diff --git a/setup.py b/setup.py
index ff9369f..0920fb2 100644
--- a/setup.py
+++ b/setup.py
@@ -43,11 +43,10 @@ setup(
         'License :: OSI Approved :: MIT License',
         'Natural Language :: English',
         'Operating System :: Unix',
-        'Programming Language :: Python :: 3.4',
-        'Programming Language :: Python :: 3.5',
         'Programming Language :: Python :: 3.6',
         'Programming Language :: Python :: 3.7',
         'Programming Language :: Python :: 3.8',
+        'Programming Language :: Python :: 3.9',
         'Programming Language :: Python :: Implementation :: PyPy',
         'Operating System :: OS Independent',
     ],
diff --git a/tox.ini b/tox.ini
index a9ef38f..7bbadc8 100644
--- a/tox.ini
+++ b/tox.ini
@@ -6,11 +6,7 @@
 # Copyright (c) 2015, fitnr <contact@fakeisthenewreal.org>
 
 [tox]
-envlist = py37, py38, pypy
+envlist = py37, py38, py39, pypy
 
 [testenv]
-whitelist_externals = make
-
-commands =
-    make install
-    make test
+commands = python -m unittest
diff --git a/visvalingamwyatt/visvalingamwyatt.py b/visvalingamwyatt/visvalingamwyatt.py
index 518480a..565b530 100644
--- a/visvalingamwyatt/visvalingamwyatt.py
+++ b/visvalingamwyatt/visvalingamwyatt.py
@@ -226,9 +226,12 @@ def simplify_geometry(geom, **kwargs):
         pass
 
     elif geom['type'] == 'MultiPolygon':
-        g['coordinates'] = [simplify_rings(rings, **kwargs) for rings in geom['coordinates']]
+        g['coordinates'] = [simplify_rings(rings, closed=True, **kwargs) for rings in geom['coordinates']]
 
-    elif geom['type'] in ('Polygon', 'MultiLineString'):
+    elif geom['type'] == 'Polygon':
+        g['coordinates'] = simplify_rings(geom['coordinates'], closed=True, **kwargs)
+
+    elif geom['type'] == 'MultiLineString':
         g['coordinates'] = simplify_rings(geom['coordinates'], **kwargs)
 
     elif geom['type'] == 'LineString':
@@ -247,9 +250,12 @@ def simplify_rings(rings, **kwargs):
     return [simplify(ring, **kwargs) for ring in rings]
 
 
-def simplify(coordinates, number=None, ratio=None, threshold=None):
+def simplify(coordinates, number=None, ratio=None, threshold=None, closed=False):
     '''Simplify a list of coordinates'''
-    return Simplifier(coordinates).simplify(number=number, ratio=ratio, threshold=threshold).tolist()
+    result = Simplifier(coordinates).simplify(number=number, ratio=ratio, threshold=threshold).tolist()
+    if closed:
+        result[-1] = result[0]
+    return result 
 
 
 def simplify_feature(feat, number=None, ratio=None, threshold=None):


Test patch:
diff --git a/tests/test_vw.py b/tests/test_vw.py
index 51053fd..fb31d7a 100644
--- a/tests/test_vw.py
+++ b/tests/test_vw.py
@@ -15,10 +15,12 @@ import numpy as np
 import visvalingamwyatt as vw
 from visvalingamwyatt import __main__ as cli
 
+
 class TestCase(unittest.TestCase):
 
     def setUp(self):
-        with open('tests/data/sample.json') as f:
+        self.samplefile = os.path.join(os.path.dirname(__file__), 'data', 'sample.json')
+        with open(self.samplefile) as f:
             self.fixture = json.load(f).get('features')[0]
 
     def standard(self, **kwargs):
@@ -70,7 +72,6 @@ class TestCase(unittest.TestCase):
         #  a    e
         #
         # so b and d are eliminated
-
         a, b, c, d, e = Point(0, 0), Point(1, 1), Point(2, 2), Point(3, 1), Point(4, 0)
         inp = [a, b, c, d, e]
         expected_output = np.array([a, c, e])
@@ -86,7 +87,6 @@ class TestCase(unittest.TestCase):
         #  a    e
         #
         # so b and d are eliminated
-
         a, b, c, d, e = (0, 0), (1, 1), (2, 2), (3, 1), (4, 0)
         inp = [a, b, c, d, e]
         expected_output = np.array([a, c, e])
@@ -94,13 +94,44 @@ class TestCase(unittest.TestCase):
         actual_output = vw.simplify(inp, threshold=0.001)
         self.assertTrue(np.array_equal(actual_output, expected_output))
 
+    def testSimplifyClosedFeature(self):
+        '''When simplifying geometries with closed rings (Polygons and MultiPolygons),
+        the first and last points in each ring should remain the same'''
+        test_ring = [[121.20803833007811,24.75431413309125],[121.1846923828125,24.746831298412058],[121.1517333984375,24.74059525872194],[121.14486694335936,24.729369599118222],[121.12152099609375,24.693191139677126],[121.13525390625,24.66449040712424],[121.10504150390625,24.66449040712424],[121.10092163085936,24.645768980151793],[121.0748291015625,24.615808859044243],[121.09405517578125,24.577099744289427],[121.12564086914062,24.533381526147682],[121.14624023437499,24.515889973088104],[121.19018554687499,24.528384188171866],[121.19430541992186,24.57959746772822],[121.23687744140624,24.587090339209634],[121.24099731445311,24.552119771544227],[121.2451171875,24.525885444592642],[121.30279541015624,24.55087064225044],[121.27258300781251,24.58958786341259],[121.26708984374999,24.623299562653035],[121.32614135742188,24.62579636412304],[121.34674072265624,24.602074737077242],[121.36871337890625,24.580846310771612],[121.40853881835936,24.653257887871963],[121.40853881835936,24.724380091871726],[121.37283325195312,24.716895455859337],[121.3604736328125,24.693191139677126],[121.343994140625,24.69942955501979],[121.32888793945312,24.728122241065808],[121.3714599609375,24.743089712134605],[121.37695312499999,24.77177232822881],[121.35635375976562,24.792968265314457],[121.32476806640625,24.807927923059236],[121.29730224609375,24.844072974931866],[121.24923706054688,24.849057671305268],[121.24786376953125,24.816653556469955],[121.27944946289062,24.79047481357294],[121.30142211914061,24.761796517185815],[121.27258300781251,24.73311159823193],[121.25335693359374,24.708162811665265],[121.20391845703125,24.703172454280217],[121.19979858398438,24.731864277701714],[121.20803833007811,24.75431413309125]]
+        multipolygon = {
+            "type": "MultiPolygon",
+            "coordinates": [[test_ring]]
+        }
+        number = vw.simplify_geometry(multipolygon, number=10)
+        self.assertEqual(number['coordinates'][0][0][0], number['coordinates'][0][0][-1])
+
+        ratio = vw.simplify_geometry(multipolygon, ratio=0.3)
+        self.assertEqual(ratio['coordinates'][0][0][0], ratio['coordinates'][0][0][-1])
+
+        thres =  vw.simplify_geometry(multipolygon, threshold=0.01)
+        self.assertEqual(thres['coordinates'][0][0][0], thres['coordinates'][0][0][-1])
+
+        polygon = {
+            "type": "Polygon",
+            "coordinates": [test_ring]
+        }
+        number = vw.simplify_geometry(multipolygon, number=10)
+        self.assertEqual(number['coordinates'][0][0][0], number['coordinates'][0][0][-1])
+
+        ratio = vw.simplify_geometry(multipolygon, ratio=0.3)
+        self.assertEqual(ratio['coordinates'][0][0][0], ratio['coordinates'][0][0][-1])
+
+        thres =  vw.simplify_geometry(multipolygon, threshold=0.01)
+        self.assertEqual(thres['coordinates'][0][0][0], thres['coordinates'][0][0][-1])
+
     def testCli(self):
         pass
 
     def testSimplify(self):
+        '''Use the command-line function to simplify the sample data.'''
         try:
             output = 'tmp.json'
-            cli.simplify('tests/data/sample.json', output, number=9)
+            cli.simplify(self.samplefile, output, number=9)
 
             self.assertTrue(os.path.exists(output))