├── .gitignore ├── setup.cfg ├── tests ├── __init__.py ├── cli_tests.py ├── polyencode_tests.py └── fixtures │ └── brooklyn.geojson ├── LICENSE ├── polyencoder ├── __init__.py ├── cli.py ├── polyencode_layer.py └── polyencoder.py ├── Makefile ├── .travis.yml ├── setup.py └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | build 3 | dist 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | from . import polyencode_tests 2 | from . import cli_tests 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Neil Freeman 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /polyencoder/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # polyencoder 5 | # Copyright 2015 Neil Freeman contact@fakeisthenewreal.org 6 | 7 | # All rights reserved. 8 | 9 | # Redistribution and use in source and binary forms, with or without 10 | # modification, are permitted provided that the following conditions are met: 11 | # * Redistributions of source code must retain the above copyright 12 | # notice, this list of conditions and the following disclaimer. 13 | # * Redistributions in binary form must reproduce the above copyright 14 | # notice, this list of conditions and the following disclaimer in the 15 | # documentation and/or other materials provided with the distribution. 16 | # * Neither the name of the nor the 17 | # names of its contributors may be used to endorse or promote products 18 | # derived from this software without specific prior written permission. 19 | 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 21 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | # DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 24 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 25 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 26 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 27 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 29 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | from .polyencoder import PolyEncoder, polyencode 32 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # polyencoder 2 | # Copyright 2015 Neil Freeman contact@fakeisthenewreal.org 3 | 4 | # All rights reserved. 5 | 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions are met: 8 | # * Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # * Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # * Neither the name of the nor the 14 | # names of its contributors may be used to endorse or promote products 15 | # derived from this software without specific prior written permission. 16 | 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | # DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 21 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 24 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | 28 | deploy: 29 | rm -rf dist build 30 | python3 setup.py bdist_wheel 31 | rm -rf build 32 | python setup.py sdist bdist_wheel 33 | twine upload dist/* 34 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # polyencoder 2 | # Copyright 2015 Neil Freeman contact@fakeisthenewreal.org 3 | 4 | # All rights reserved. 5 | 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions are met: 8 | # * Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # * Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # * Neither the name of the nor the 14 | # names of its contributors may be used to endorse or promote products 15 | # derived from this software without specific prior written permission. 16 | 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | # DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 21 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 24 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | 28 | language: python 29 | 30 | python: 31 | - 2.7 32 | - 3.4 33 | - 3.5 34 | 35 | before_install: 36 | - sudo apt-get -qq update 37 | - sudo apt-get -qq install -y libgdal1-dev 38 | 39 | install: 40 | - pip install fiona 41 | - python setup.py install 42 | 43 | script: 44 | - python setup.py test 45 | - polyencode --help 46 | - polyencode layer GEOID tests/fixtures/brooklyn.geojson 47 | - polyencode points --help 48 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # polyencoder 5 | # Copyright 2015 Neil Freeman contact@fakeisthenewreal.org 6 | 7 | # All rights reserved. 8 | 9 | # Redistribution and use in source and binary forms, with or without 10 | # modification, are permitted provided that the following conditions are met: 11 | # * Redistributions of source code must retain the above copyright 12 | # notice, this list of conditions and the following disclaimer. 13 | # * Redistributions in binary form must reproduce the above copyright 14 | # notice, this list of conditions and the following disclaimer in the 15 | # documentation and/or other materials provided with the distribution. 16 | # * Neither the name of the nor the 17 | # names of its contributors may be used to endorse or promote products 18 | # derived from this software without specific prior written permission. 19 | 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 21 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | # DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 24 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 25 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 26 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 27 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 29 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | from setuptools import setup 32 | 33 | setup( 34 | name='polyencoder', 35 | version='0.1.0', 36 | description='Encode geo features with the GPolyencoder', 37 | long_description='''Encode geo features with the GPolyencoder''', 38 | keywords='polygons gis mapping', 39 | author='fitnr', 40 | license='BSD', 41 | author_email='contact@fakeisthenewreal.org', 42 | packages=['polyencoder'], 43 | 44 | url='https://github.com/fitnr/polyencoder', 45 | 46 | include_package_data=False, 47 | 48 | extras_require={ 49 | 'layer': ['Fiona'] 50 | }, 51 | 52 | zip_safe=True, 53 | 54 | use_2to3=True, 55 | 56 | test_suite='tests', 57 | 58 | entry_points={ 59 | 'console_scripts': [ 60 | 'polyencode=polyencoder.cli:main', 61 | ], 62 | }, 63 | ) 64 | -------------------------------------------------------------------------------- /tests/cli_tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # This file is part of polyencode. 5 | # https://github.com/fitnr/polyencode 6 | 7 | # Licensed under the GNU General Public License v3 (GPLv3) license: 8 | # http://opensource.org/licenses/GPL-3.0 9 | # Copyright (c) 2015, Neil Freeman 10 | from __future__ import unicode_literals 11 | import unittest 12 | import os 13 | import subprocess 14 | 15 | 16 | class CliTestCase(unittest.TestCase): 17 | 18 | def setUp(self): 19 | geojson = os.path.join(os.path.dirname(__file__), 'fixtures', 'brooklyn.geojson') 20 | 21 | self.args = ['polyencode', 'layer', 'GEOID', geojson] 22 | self.geoid = '36047'.encode('utf8') 23 | 24 | def testPolyEncodePoints(self): 25 | args = ('polyencode', 'points', '41.87519,-87.67879', '41.86394,-87.63004') 26 | 27 | expected = 'vfzuOspo%7EF\n'.encode('utf8') 28 | 29 | p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 30 | out, _ = p.communicate() 31 | 32 | self.assertIsNotNone(out) 33 | 34 | self.assertEqual(expected, out) 35 | 36 | def testPolyEncode(self): 37 | p = subprocess.Popen(self.args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 38 | 39 | out, _ = p.communicate() 40 | 41 | self.assertIsNotNone(out) 42 | 43 | expected = 'e%7B%7EvF%7Cc_cMi%7CBqCgI%7DEyAw%40IGcGcDgVeLgHqDsDkC_h'.encode('utf8') 44 | 45 | self.assertIn(self.geoid, out) 46 | self.assertIn('\t'.encode('utf8'), out) 47 | self.assertIn(expected, out) 48 | 49 | def testPolyEncodeNoUrlEncode(self): 50 | args = self.args + ['--no-encode'] 51 | p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 52 | out, _ = p.communicate() 53 | 54 | expected = 'e{~vF|c_cMi|BqCgI}EyAw@IGcGcDgVeL'.encode('utf8') 55 | 56 | self.assertIsNotNone(out) 57 | 58 | self.assertIn(self.geoid, out) 59 | self.assertIn('\t'.encode('utf8'), out) 60 | self.assertIn(expected, out) 61 | 62 | def testPolyEncodePluralKeys(self): 63 | self.args[2] = 'GEOID,NAMELSAD' 64 | p = subprocess.Popen(self.args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 65 | out, _ = p.communicate() 66 | 67 | self.assertIsNotNone(out) 68 | 69 | self.assertIn('Kings County'.encode('utf8'), out) 70 | 71 | def testPolyEncodeDelimiter(self): 72 | args = self.args + ['--delimiter', '|'] 73 | p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 74 | out, _ = p.communicate() 75 | 76 | self.assertIsNotNone(out) 77 | 78 | self.assertIn('|'.encode('utf8'), out) 79 | 80 | if __name__ == '__main__': 81 | unittest.main() 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Polyencoder 2 | ========== 3 | 4 | Python module for Google Maps polyline encoding. Want your list of coordinates to look like `e{~vF|c_ci|BqCI}Eyw@`? You've come to the right place! 5 | 6 | Includes command line utils for encoding a list of points or an entire geodata file. 7 | 8 | ## Installing 9 | 10 | Two options: 11 | ```` 12 | pip install polyencoder 13 | pip install polyencoder[layer] 14 | ```` 15 | 16 | The first installs the basic polyencoder. The second installs Fiona, a dependencies for the layer encoder. 17 | 18 | Requirements: 19 | 20 | * Python >= 2.7 21 | * GDAL (layer encoder only) 22 | 23 | ## Command line 24 | 25 | Encode a set of points: 26 | ```` 27 | polyencode point x0,y0 x1,y1 28 | ```` 29 | 30 | Encode a geodata layer (*.geojson, *.shp, etc): 31 | ```` 32 | polyencode layer id infile.geojson 33 | ```` 34 | 35 | This will produce tab-separated output: 36 | ``` 37 | 1 e%7B%7EvF 38 | 2 kF%60%40wFDYtIea 39 | 3 ... 40 | ``` 41 | (Assuming that the 'id' field of the input is 1, 2, 3.) 42 | 43 | You can specify more than one key with a comma: 44 | ```` 45 | polyencode layer id,name infile.geojson 46 | ```` 47 | 48 | By default, results are url-encoded. Get the raw result with `--no-encode`: 49 | ```` 50 | polyencode layer --no-encode id infile.geojson 51 | 52 | 1 e{~vF|c_cMi|BqCg 53 | 2 AH~FmClIiLfEkR`AgOjCiOLYrF 54 | ```` 55 | 56 | By default, the result is tab-separated. Use another character: 57 | ```` 58 | polyencode layer --delimiter , id infile.geojson 59 | ```` 60 | 61 | ## API 62 | 63 | ````python 64 | >>> import polyencoder 65 | >>> encoder = polyencoder.PolyEncoder() 66 | # points are a sequence of (longitude,latitude) coordinate pairs 67 | >>> points = ((8.94328,52.29834), (8.93614,52.29767), (8.93301,52.29322), (8.93036,52.28938), (8.97475,52.27014),) 68 | >>> encoder.polyencode(points) 69 | 'soe~Hovqu@dCrk@xZpR~VpOfwBmtG' 70 | ```` 71 | 72 | The constructor takes several arguments: 73 | * `num_levels` specifies how many different levels of magnification the polyline will have. (default: 18) 74 | * `zoom_factor` specifies the change in magnification between those levels. (default: 2) 75 | * `threshold` indicates the length of a barely visible object at the highest zoom level. (default: 0.00001) 76 | * `force_endpoints` indicates whether or not the endpoints should be visible at all zoom levels. (default: True) 77 | 78 | See http://facstaff.unca.edu/mcmcclur/GoogleMaps/EncodePolyline/description.html for more details on what these parameters mean and how to tweak them. The defaults are sensible for most situations. 79 | 80 | ## License 81 | 82 | This module is distributed under a BSD license. 83 | 84 | The underlying polyencoder is is Python port of the Javascript Google Maps polyline encoder from Mark McClure, released under a BSD license. It is: 85 | ``` 86 | Copyright (c) 2009, Koordinates Limited 87 | All rights reserved. 88 | ``` 89 | 90 | Updates and command line utils are 91 | ``` 92 | Copyright (c) 2015, Neil Freeman 93 | All rights reserved. 94 | ``` 95 | -------------------------------------------------------------------------------- /polyencoder/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # polyencoder 5 | # Copyright 2015 Neil Freeman contact@fakeisthenewreal.org 6 | 7 | # All rights reserved. 8 | 9 | # Redistribution and use in source and binary forms, with or without 10 | # modification, are permitted provided that the following conditions are met: 11 | # * Redistributions of source code must retain the above copyright 12 | # notice, this list of conditions and the following disclaimer. 13 | # * Redistributions in binary form must reproduce the above copyright 14 | # notice, this list of conditions and the following disclaimer in the 15 | # documentation and/or other materials provided with the distribution. 16 | # * Neither the name of the nor the 17 | # names of its contributors may be used to endorse or promote products 18 | # derived from this software without specific prior written permission. 19 | 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 21 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | # DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 24 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 25 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 26 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 27 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 29 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | from __future__ import print_function 32 | import argparse 33 | import sys 34 | from urllib import quote_plus 35 | from .polyencoder import polyencode 36 | try: 37 | from .polyencode_layer import encodelayer 38 | except ImportError: 39 | encodelayer = None 40 | 41 | DESC = """ 42 | Encode coordinates with Google's encoded polyline algorithm 43 | see: https://developers.google.com/maps/documentation/utilities/polylinealgorithm 44 | """ 45 | 46 | 47 | def encodepoints(points): 48 | if len(points) == 1: 49 | in_points = sys.stdin.read().split(' ') 50 | else: 51 | in_points = points[1:] 52 | 53 | points = [(float(x), float(y)) for x, y in [point.split(',') for point in in_points]] 54 | 55 | points = polyencode(points) 56 | print(quote_plus(points)) 57 | 58 | 59 | def main(): 60 | parser = argparse.ArgumentParser('polyencode', description=DESC) 61 | sp = parser.add_subparsers() 62 | 63 | points = sp.add_parser('points', usage='%(prog)s [points ...]', help="Encode a set of points") 64 | points.add_argument('points', nargs='*') 65 | points.set_defaults(func=encodepoints) 66 | 67 | if encodelayer: 68 | layer = sp.add_parser('layer', usage='%(prog)s [options] keys [infile]', help="Encode all features in a layer") 69 | 70 | layer.add_argument('keys', type=str, nargs='?', default='id', help='comma separated list of fields from file to include in CSV') 71 | layer.add_argument('infile', type=str) 72 | layer.add_argument('--no-encode', action='store_false', dest='encode', help="Don't urlencode the output string") 73 | layer.add_argument('--delimiter', type=str, default='\t', help="delimiter (default is tab)") 74 | layer.set_defaults(func=encodelayer) 75 | 76 | args = parser.parse_args() 77 | 78 | kwargs = vars(args) 79 | 80 | kwargs.pop('func')(**kwargs) 81 | 82 | if __name__ == '__main__': 83 | main() 84 | -------------------------------------------------------------------------------- /polyencoder/polyencode_layer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # polyencoder 4 | # Copyright 2015 Neil Freeman contact@fakeisthenewreal.org 5 | 6 | # All rights reserved. 7 | 8 | # Redistribution and use in source and binary forms, with or without 9 | # modification, are permitted provided that the following conditions are met: 10 | # * Redistributions of source code must retain the above copyright 11 | # notice, this list of conditions and the following disclaimer. 12 | # * Redistributions in binary form must reproduce the above copyright 13 | # notice, this list of conditions and the following disclaimer in the 14 | # documentation and/or other materials provided with the distribution. 15 | # * Neither the name of the nor the 16 | # names of its contributors may be used to endorse or promote products 17 | # derived from this software without specific prior written permission. 18 | 19 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | # DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 23 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 26 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | from __future__ import print_function 31 | import sys 32 | import csv 33 | from urllib import quote_plus 34 | import fiona 35 | from .polyencoder import polyencode 36 | 37 | 38 | def getproperties(feature, keys): 39 | '''Return a list of properties from feature''' 40 | return [feature['properties'].get(k) for k in keys] 41 | 42 | 43 | def encodelayer(infile, keys, encode=None, delimiter=None): 44 | keys = keys.split(',') 45 | 46 | writer = csv.writer(sys.stdout, delimiter=delimiter or '\t') 47 | 48 | with fiona.drivers(): 49 | with fiona.open(infile, 'r') as layer: 50 | 51 | for feature in layer: 52 | if feature['geometry']['type'] == 'MultiPolygon': 53 | # list of list of lists of tuples 54 | coords = feature['geometry']['coordinates'][0][0] 55 | 56 | elif feature['geometry']['type'] == 'Polygon' or feature['geometry']['type'] == 'MultiLineString': 57 | # list of lists of tuples 58 | coords = feature['geometry']['coordinates'][0] 59 | 60 | elif feature['geometry']['type'] == 'Linestring': 61 | # list of tuples 62 | coords = feature['geometry']['coordinates'] 63 | 64 | else: 65 | raise NotImplementedError( 66 | "Polyencode not available for geometry type: {}".format(feature['geometry']['type'])) 67 | try: 68 | encoded = polyencode(coords) 69 | 70 | except TypeError: 71 | print("Unexpected issue with {}".format(feature['properties'].get(keys[0])), file=sys.stderr) 72 | raise 73 | 74 | if encode: 75 | encoded = quote_plus(encoded) 76 | 77 | props = getproperties(feature, keys) + [encoded] 78 | writer.writerow(props) 79 | -------------------------------------------------------------------------------- /tests/polyencode_tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2009, Koordinates Limited 4 | # All rights reserved. 5 | 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions are 8 | # met: 9 | 10 | # * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 11 | # * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 12 | # * Neither the name of the Koordinates Limited nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 13 | 14 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 15 | # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 16 | # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 17 | # PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 18 | # HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 19 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 20 | # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 21 | # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 22 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 23 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | import os 27 | import re 28 | import unittest 29 | import json 30 | from polyencoder import polyencoder 31 | 32 | 33 | class PolyEncoderTest(unittest.TestCase): 34 | 35 | def setUp(self): 36 | self.e = polyencoder.PolyEncoder() 37 | 38 | def test_glineenc(self): 39 | # use same tests & setup as glineenc 40 | e = polyencoder.PolyEncoder(num_levels=4, zoom_factor=32, threshold=0.00001) 41 | 42 | p = ((-120.2, 38.5), (-126.453, 43.252), (-120.95, 40.7)) 43 | r = e.polyencode(p) 44 | self.assertEqual(r, '_p~iF~ps|U_c_\\fhde@~lqNwxq`@') 45 | 46 | p = ((-122.1419, 37.4419), (-122.1519, 37.4519), (-122.1819, 37.4619),) 47 | r = e.polyencode(p) 48 | self.assertEqual(r, 'yzocFzynhVq}@n}@o}@nzD') 49 | 50 | p = [(-120.2, 38.5)] 51 | r = e.polyencode(p) 52 | self.assertEqual(r, '_p~iF~ps|U') 53 | 54 | def test_java(self): 55 | # use same tests & setup as Java 56 | e = polyencoder.PolyEncoder(18, 2, 0.00001, True) 57 | p = ((8.94328, 52.29834), (8.93614, 52.29767), (8.93301, 52.29322), (8.93036, 52.28938), (8.97475, 52.27014)) 58 | r = e.polyencode(p) 59 | # JS: "soe~Hovqu@dCrk@xZpR~VpOfwBmtG", "PG@IP" 60 | # Java: 'soe~Hovqu@dCrk@xZpR~VpOfwBmtG', 'PPPPP' 61 | self.assertEqual(r, 'soe~Hovqu@dCrk@xZpR~VpOfwBmtG') 62 | 63 | def test_googleutility(self): 64 | expected = '}vq~FlwcvOheAuoH' 65 | p = ((-87.67879, 41.87519), (-87.63004, 41.86394)) 66 | result = polyencoder.PolyEncoder(num_levels=3).polyencode(p) 67 | 68 | self.assertEqual(result, expected) 69 | 70 | def test_glineenc_encode_number(self): 71 | # use same tests & setup as glineenc 72 | self.assertEqual(polyencoder.encode_signed_number(int(-179.9832104 * 1E5)), '`~oia@') 73 | self.assertEqual(polyencoder.encode_signed_number(int(-120.2 * 1E5)), '~ps|U') 74 | self.assertEqual(polyencoder.encode_signed_number(int(38.5 * 1E5)), '_p~iF') 75 | 76 | def data_test(self): 77 | test_data = os.path.join(os.path.dirname(__file__), 'fixtures', 'test_data.txt') 78 | 79 | for data, expected, threshold in _load_data(test_data): 80 | e = polyencoder.PolyEncoder( 81 | zoom_factor=expected['zoomFactor'], num_levels=expected['numLevels'], threshold=threshold) 82 | 83 | r = e.polyencode(data) 84 | 85 | self.assertEqual(r, expected['points']) 86 | 87 | 88 | # more data 89 | 90 | 91 | def _load_data(data_file): 92 | f = open(data_file, 'r') 93 | s = 0 94 | for line in f: 95 | if s == 0: 96 | data = [] 97 | name = line.strip() 98 | reverse_coords = '(yx)' in name 99 | if '(t=' in name: 100 | threshold = float(re.search(r'\(t=(\d+\.\d+)\)', name).groups()[0]) 101 | else: 102 | threshold = 0.00001 103 | s += 1 104 | elif s == 1: 105 | expected = json.loads(line.strip()) 106 | s += 1 107 | elif s == 2 and len(line.strip()): 108 | coords = map(float, re.split('[, ]+', line.strip(), 2)) 109 | if reverse_coords: 110 | coords.reverse() 111 | data.append(tuple(coords)) 112 | else: 113 | s = 0 114 | yield name, data, expected, threshold 115 | data = [] 116 | 117 | if len(data): 118 | yield name, data, expected, threshold 119 | 120 | 121 | if __name__ == "__main__": 122 | import nose 123 | nose.main() 124 | -------------------------------------------------------------------------------- /polyencoder/polyencoder.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2009, Koordinates Limited 2 | # All rights reserved. 3 | 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are 6 | # met: 7 | 8 | # * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 9 | # * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | # * Neither the name of the Koordinates Limited nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 13 | # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 14 | # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 15 | # PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 16 | # HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 17 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 18 | # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 19 | # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 20 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 21 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | 24 | # Modified for Python 3 support and CLI added by Neil Freeman 25 | 26 | import math 27 | 28 | try: 29 | from cStringIO import StringIO 30 | except ImportError: 31 | from StringIO import StringIO 32 | 33 | 34 | def encode_signed_number(num): 35 | sgn_num = num << 1 36 | if num < 0: 37 | sgn_num = ~sgn_num 38 | return encode_number(sgn_num) 39 | 40 | 41 | def encode_number(num): 42 | s = StringIO() 43 | while num >= 0x20: 44 | next_val = (0x20 | (num & 0x1f)) + 63 45 | s.write(chr(next_val)) 46 | num >>= 5 47 | num += 63 48 | s.write(chr(num)) 49 | return s.getvalue() 50 | 51 | 52 | def distance(p0, p1, p2): 53 | out = 0.0 54 | 55 | if (p1[1] == p2[1] and p1[0] == p2[0]): 56 | out = math.sqrt((p2[1] - p0[1]) ** 2 + (p2[0] - p0[0]) ** 2) 57 | else: 58 | u = ((p0[1] - p1[1]) * (p2[1] - p1[1]) + (p0[0] - p1[0]) * (p2[0] - p1[0])) \ 59 | / ((p2[1] - p1[1]) ** 2 + (p2[0] - p1[0]) ** 2) 60 | 61 | if u <= 0: 62 | out = math.sqrt((p0[1] - p1[1]) ** 2 + (p0[0] - p1[0]) ** 2) 63 | elif u >= 1: 64 | out = math.sqrt((p0[1] - p2[1]) ** 2 + (p0[0] - p2[0]) ** 2) 65 | elif (0 < u) and (u < 1): 66 | out = math.sqrt((p0[1] - p1[1] - u * (p2[1] - p1[1])) ** 2 67 | + (p0[0] - p1[0] - u * (p2[0] - p1[0])) ** 2) 68 | 69 | return out 70 | 71 | 72 | class PolyEncoder(object): 73 | 74 | def __init__(self, num_levels=18, zoom_factor=2, threshold=0.00001, force_endpoints=True): 75 | self._num_levels = num_levels 76 | self._zoom_factor = zoom_factor 77 | self._threshold = threshold 78 | self._force_endpoints = force_endpoints 79 | 80 | self._zoom_level_breaks = [] 81 | for i in range(num_levels): 82 | self._zoom_level_breaks.append(threshold * (zoom_factor ** (num_levels - i - 1))) 83 | 84 | def polyencode(self, points): 85 | dists = {} 86 | # simplify using Douglas-Peucker 87 | max_dist = 0 88 | abs_max_dist = 0 89 | stack = [] 90 | if (len(points) > 2): 91 | stack.append((0, len(points)-1)) 92 | while len(stack): 93 | current = stack.pop() 94 | max_dist = 0 95 | 96 | for i in range(current[0]+1, current[1]): 97 | temp = distance(points[i], points[current[0]], points[current[1]]) 98 | if temp > max_dist: 99 | max_dist = temp 100 | max_loc = i 101 | abs_max_dist = max(abs_max_dist, max_dist) 102 | 103 | if max_dist > self._threshold: 104 | dists[max_loc] = max_dist 105 | stack.append((current[0], max_loc)) 106 | stack.append((max_loc, current[1])) 107 | 108 | enc_points, _ = self._encode(points, dists, abs_max_dist) 109 | 110 | return enc_points 111 | 112 | def _encode(self, points, dists, abs_max_dist): 113 | encoded_levels = StringIO() 114 | encoded_points = StringIO() 115 | 116 | plat = 0 117 | plng = 0 118 | 119 | if (self._force_endpoints): 120 | encoded_levels.write(encode_number(self._num_levels - 1)) 121 | else: 122 | encoded_levels.write(encode_number(self._num_levels - self._compute_level(abs_max_dist) - 1)) 123 | 124 | n_points = len(points) 125 | for i, p in enumerate(points): 126 | if (i > 0) and (i < n_points-1) and (i in dists): 127 | encoded_levels.write(encode_number(self._num_levels - self._compute_level(dists[i]) - 1)) 128 | 129 | if (i in dists) or (i == 0) or (i == n_points-1): 130 | late5 = int(math.floor(p[1] * 1E5)) 131 | lnge5 = int(math.floor(p[0] * 1E5)) 132 | dlat = late5 - plat 133 | dlng = lnge5 - plng 134 | plat = late5 135 | plng = lnge5 136 | encoded_points.write(encode_signed_number(dlat)) 137 | encoded_points.write(encode_signed_number(dlng)) 138 | 139 | if (self._force_endpoints): 140 | encoded_levels.write(encode_number(self._num_levels - 1)) 141 | else: 142 | encoded_levels.write(encode_number(self._num_levels - self._compute_level(abs_max_dist) - 1)) 143 | 144 | return ( 145 | encoded_points.getvalue(), # .replace("\\", "\\\\"), 146 | encoded_levels.getvalue() 147 | ) 148 | 149 | def _compute_level(self, abs_max_dist): 150 | lev = 0 151 | if abs_max_dist > self._threshold: 152 | while abs_max_dist < self._zoom_level_breaks[lev]: 153 | lev += 1 154 | return lev 155 | 156 | 157 | polyencode = PolyEncoder().polyencode 158 | -------------------------------------------------------------------------------- /tests/fixtures/brooklyn.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | 4 | "features": [ 5 | { "type": "Feature", "properties": { "STATEFP": "36", "COUNTYFP": "047", "COUNTYNS": "00974122", "GEOID": "36047", "NAME": "Kings", "NAMELSAD": "Kings County", "LSAD": "06", "CLASSFP": "H6", "MTFCC": "G4020", "CSAFP": "408", "CBSAFP": "35620", "METDIVFP": "35614", "FUNCSTAT": "C", "ALAND": 180813778, "AWATER": 69955570, "INTPTLAT": "+40.6350451", "INTPTLON": "-073.9506398" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -74.056468, 40.631715 ], [ -74.056261, 40.637407 ], [ -74.056233, 40.638177 ], [ -74.056193, 40.639281 ], [ -74.055739, 40.65176 ], [ -74.054622, 40.653406 ], [ -74.054547, 40.65354 ], [ -74.05435, 40.653856 ], [ -74.054332, 40.653875 ], [ -74.054302, 40.653907 ], [ -74.053484, 40.655203 ], [ -74.053229, 40.655657 ], [ -74.053016, 40.656032 ], [ -74.052802, 40.656408 ], [ -74.052292, 40.657304 ], [ -74.051783, 40.6582 ], [ -74.05137, 40.658926 ], [ -74.050818, 40.659848 ], [ -74.050495, 40.660385 ], [ -74.050485, 40.660402 ], [ -74.05015, 40.660834 ], [ -74.049786, 40.661303 ], [ -74.049594, 40.661622 ], [ -74.048937, 40.662702 ], [ -74.04822, 40.663905 ], [ -74.047663, 40.664832 ], [ -74.045951, 40.66768 ], [ -74.044669, 40.669813 ], [ -74.043507, 40.671745 ], [ -74.042759, 40.672988 ], [ -74.035057, 40.68394 ], [ -74.034905, 40.684231 ], [ -74.034573, 40.684866 ], [ -74.019399, 40.679573 ], [ -74.019347, 40.679548 ], [ -74.019317, 40.679608 ], [ -74.019077, 40.679763 ], [ -74.018925, 40.679842 ], [ -74.018983, 40.679898 ], [ -74.018739, 40.680046 ], [ -74.018673, 40.680086 ], [ -74.018579, 40.680144 ], [ -74.018482, 40.680203 ], [ -74.018263, 40.680334 ], [ -74.017797, 40.680619 ], [ -74.017713, 40.68066 ], [ -74.017644, 40.680598 ], [ -74.017574, 40.680644 ], [ -74.017559, 40.680645 ], [ -74.017463, 40.680632 ], [ -74.01744, 40.680636 ], [ -74.01729, 40.680731 ], [ -74.01652, 40.681194 ], [ -74.016309, 40.68132 ], [ -74.015345, 40.681897 ], [ -74.015237, 40.681996 ], [ -74.015323, 40.682112 ], [ -74.015275, 40.682142 ], [ -74.013167, 40.683411 ], [ -74.012835, 40.683609 ], [ -74.012768, 40.683605 ], [ -74.01263, 40.683471 ], [ -74.012484, 40.683553 ], [ -74.012126, 40.68377 ], [ -74.011924, 40.683894 ], [ -74.01094, 40.684485 ], [ -74.008595, 40.685904 ], [ -74.008513, 40.685963 ], [ -74.008425, 40.686016 ], [ -74.008164, 40.686175 ], [ -74.00795, 40.686293 ], [ -74.007929, 40.686276 ], [ -74.007899, 40.686251 ], [ -74.007843, 40.686238 ], [ -74.007778, 40.686245 ], [ -74.00773, 40.686254 ], [ -74.00767, 40.686269 ], [ -74.007623, 40.686285 ], [ -74.007536, 40.686315 ], [ -74.007497, 40.686344 ], [ -74.007411, 40.686388 ], [ -74.007243, 40.686449 ], [ -74.00715, 40.686475 ], [ -74.007064, 40.686504 ], [ -74.00702, 40.686526 ], [ -74.006989, 40.686509 ], [ -74.006875, 40.686464 ], [ -74.006755, 40.686413 ], [ -74.006594, 40.686347 ], [ -74.006506, 40.686314 ], [ -74.006444, 40.686296 ], [ -74.006406, 40.686289 ], [ -74.00635, 40.686285 ], [ -74.005874, 40.686207 ], [ -74.00563, 40.686784 ], [ -74.005038, 40.687561 ], [ -74.004593, 40.688215 ], [ -74.003919, 40.68886 ], [ -74.00382, 40.68893 ], [ -74.003409, 40.689613 ], [ -74.003024, 40.690349 ], [ -74.002993, 40.690427 ], [ -74.002763, 40.690364 ], [ -74.002616, 40.690324 ], [ -74.002427, 40.690273 ], [ -74.002178, 40.690206 ], [ -74.001963, 40.690136 ], [ -74.001853, 40.690388 ], [ -74.00096, 40.690132 ], [ -74.000699, 40.690612 ], [ -74.001332, 40.691225 ], [ -74.001969, 40.691843 ], [ -74.001818, 40.692222 ], [ -74.001744, 40.692407 ], [ -74.001542, 40.692786 ], [ -74.001317, 40.693208 ], [ -74.001277, 40.693298 ], [ -74.001194, 40.693484 ], [ -74.000959, 40.694069 ], [ -74.000701, 40.694388 ], [ -74.000277, 40.694959 ], [ -73.999882, 40.695605 ], [ -73.999604, 40.696069 ], [ -73.999461, 40.69628 ], [ -73.99942, 40.696361 ], [ -73.999428, 40.69641 ], [ -73.999478, 40.696466 ], [ -73.999535, 40.696578 ], [ -73.999474, 40.696704 ], [ -73.999295, 40.696795 ], [ -73.999182, 40.696885 ], [ -73.998991, 40.696812 ], [ -73.99894, 40.696941 ], [ -73.998857, 40.69692 ], [ -73.998763, 40.69711 ], [ -73.998847, 40.697141 ], [ -73.998376, 40.698048 ], [ -73.998032, 40.698762 ], [ -73.9975, 40.699653 ], [ -73.996698, 40.700877 ], [ -73.995138, 40.70298 ], [ -73.995069, 40.703102 ], [ -73.994806, 40.703296 ], [ -73.994663, 40.703431 ], [ -73.994858, 40.703566 ], [ -73.994992, 40.703654 ], [ -73.994882, 40.703788 ], [ -73.994881, 40.703813 ], [ -73.994816, 40.703906 ], [ -73.994714, 40.703997 ], [ -73.994797, 40.704096 ], [ -73.994618, 40.704226 ], [ -73.994565, 40.704265 ], [ -73.994498, 40.704318 ], [ -73.994341, 40.704429 ], [ -73.993861, 40.704556 ], [ -73.993807, 40.70454 ], [ -73.993775, 40.704449 ], [ -73.993693, 40.704476 ], [ -73.993713, 40.70452 ], [ -73.993853, 40.704829 ], [ -73.992722, 40.705544 ], [ -73.99148, 40.705625 ], [ -73.990882, 40.705124 ], [ -73.990696, 40.705131 ], [ -73.990549, 40.705144 ], [ -73.989861, 40.705175 ], [ -73.989608, 40.705158 ], [ -73.989495, 40.705145 ], [ -73.989424, 40.705137 ], [ -73.989381, 40.705137 ], [ -73.989224, 40.705137 ], [ -73.987957, 40.705134 ], [ -73.987577, 40.705158 ], [ -73.987449, 40.705166 ], [ -73.987271, 40.705181 ], [ -73.986866, 40.705161 ], [ -73.986762, 40.705118 ], [ -73.98669, 40.705075 ], [ -73.986641, 40.705018 ], [ -73.986623, 40.704979 ], [ -73.98653, 40.704979 ], [ -73.986429, 40.705083 ], [ -73.986434, 40.705349 ], [ -73.985852, 40.705415 ], [ -73.984189, 40.705582 ], [ -73.983442, 40.705645 ], [ -73.983426, 40.705577 ], [ -73.983398, 40.705467 ], [ -73.983351, 40.705451 ], [ -73.983227, 40.705482 ], [ -73.98308, 40.70548 ], [ -73.98308, 40.705519 ], [ -73.982825, 40.705558 ], [ -73.982741, 40.705576 ], [ -73.982748, 40.705532 ], [ -73.982572, 40.705518 ], [ -73.982455, 40.705509 ], [ -73.982449, 40.705547 ], [ -73.982329, 40.705552 ], [ -73.98221, 40.70554 ], [ -73.982033, 40.705515 ], [ -73.981856, 40.70549 ], [ -73.980987, 40.705374 ], [ -73.980907, 40.705907 ], [ -73.980332, 40.705846 ], [ -73.979479, 40.705825 ], [ -73.979233, 40.705769 ], [ -73.979069, 40.705963 ], [ -73.975476, 40.707492 ], [ -73.97502, 40.707736 ], [ -73.97482, 40.707883 ], [ -73.974554, 40.708079 ], [ -73.974431, 40.708169 ], [ -73.974264, 40.708298 ], [ -73.973243, 40.709044 ], [ -73.97306, 40.709179 ], [ -73.972789, 40.709378 ], [ -73.972309, 40.709071 ], [ -73.970573, 40.707555 ], [ -73.970318, 40.707332 ], [ -73.970236, 40.707367 ], [ -73.970141, 40.707407 ], [ -73.969613, 40.707634 ], [ -73.969845, 40.707988 ], [ -73.969888, 40.70877 ], [ -73.969941, 40.70931 ], [ -73.969612, 40.71016 ], [ -73.969128, 40.712144 ], [ -73.969072, 40.712385 ], [ -73.96905, 40.712482 ], [ -73.969021, 40.712508 ], [ -73.968993, 40.712533 ], [ -73.968945, 40.712575 ], [ -73.968895, 40.71262 ], [ -73.968861, 40.71265 ], [ -73.968819, 40.712686 ], [ -73.968469, 40.712999 ], [ -73.96837, 40.714127 ], [ -73.968041, 40.714966 ], [ -73.967999, 40.715106 ], [ -73.967716, 40.71604 ], [ -73.967578, 40.716496 ], [ -73.966836, 40.717564 ], [ -73.966671, 40.717743 ], [ -73.966613, 40.717806 ], [ -73.966511, 40.717917 ], [ -73.966477, 40.717955 ], [ -73.966249, 40.718205 ], [ -73.96613, 40.718331 ], [ -73.965704, 40.718779 ], [ -73.965572, 40.71895 ], [ -73.965086, 40.719581 ], [ -73.964038, 40.720939 ], [ -73.96373, 40.721339 ], [ -73.96355, 40.721571 ], [ -73.962787, 40.722562 ], [ -73.962645, 40.722747 ], [ -73.962014, 40.723369 ], [ -73.961543, 40.723876 ], [ -73.961539, 40.724401 ], [ -73.961538, 40.724584 ], [ -73.961537, 40.724695 ], [ -73.96155, 40.724722 ], [ -73.961606, 40.724836 ], [ -73.961618, 40.724861 ], [ -73.961611, 40.724951 ], [ -73.961531, 40.72591 ], [ -73.961501, 40.726986 ], [ -73.961436, 40.727298 ], [ -73.961389, 40.727524 ], [ -73.961283, 40.728038 ], [ -73.961172, 40.728057 ], [ -73.961157, 40.728114 ], [ -73.961215, 40.728151 ], [ -73.961107, 40.728195 ], [ -73.961063, 40.72844 ], [ -73.961055, 40.728471 ], [ -73.96104, 40.72849 ], [ -73.961028, 40.728512 ], [ -73.960992, 40.728656 ], [ -73.960986, 40.72876 ], [ -73.960994, 40.728773 ], [ -73.961044, 40.728782 ], [ -73.961054, 40.728943 ], [ -73.961075, 40.729107 ], [ -73.96108, 40.729148 ], [ -73.961076, 40.729194 ], [ -73.961088, 40.729256 ], [ -73.961115, 40.729361 ], [ -73.961137, 40.72943 ], [ -73.961197, 40.729425 ], [ -73.961214, 40.729508 ], [ -73.96126, 40.729749 ], [ -73.961308, 40.730299 ], [ -73.96145, 40.730994 ], [ -73.961843, 40.731625 ], [ -73.961997, 40.732038 ], [ -73.962148, 40.732452 ], [ -73.961897, 40.732485 ], [ -73.961932, 40.732679 ], [ -73.962229, 40.732649 ], [ -73.962252, 40.732713 ], [ -73.962261, 40.732741 ], [ -73.962268, 40.732769 ], [ -73.962278, 40.732826 ], [ -73.962288, 40.732899 ], [ -73.962307, 40.733119 ], [ -73.962338, 40.733451 ], [ -73.962348, 40.733812 ], [ -73.962605, 40.734902 ], [ -73.962665, 40.736334 ], [ -73.962668, 40.73641 ], [ -73.962672, 40.736446 ], [ -73.962629, 40.736545 ], [ -73.962478, 40.736802 ], [ -73.961859, 40.737072 ], [ -73.961525, 40.737218 ], [ -73.95706, 40.739167 ], [ -73.955087, 40.7394 ], [ -73.953907, 40.739421 ], [ -73.952749, 40.739262 ], [ -73.952723, 40.739261 ], [ -73.952669, 40.739258 ], [ -73.95254, 40.739228 ], [ -73.951886, 40.739023 ], [ -73.947064, 40.737516 ], [ -73.946832, 40.737437 ], [ -73.946367, 40.737256 ], [ -73.942163, 40.735678 ], [ -73.940368, 40.733269 ], [ -73.940318, 40.733195 ], [ -73.94037, 40.732767 ], [ -73.940073, 40.732233 ], [ -73.940003, 40.732114 ], [ -73.939977, 40.732052 ], [ -73.939913, 40.731943 ], [ -73.93966, 40.731487 ], [ -73.937525, 40.729815 ], [ -73.934425, 40.728818 ], [ -73.931825, 40.728482 ], [ -73.929213, 40.72778 ], [ -73.929179, 40.727764 ], [ -73.92914, 40.727745 ], [ -73.929086, 40.727714 ], [ -73.929058, 40.727688 ], [ -73.928908, 40.727548 ], [ -73.927801, 40.726494 ], [ -73.927225, 40.725333 ], [ -73.926539, 40.724065 ], [ -73.925344, 40.721638 ], [ -73.92418, 40.72002 ], [ -73.924278, 40.718536 ], [ -73.923127, 40.717024 ], [ -73.922711, 40.716478 ], [ -73.922739, 40.716158 ], [ -73.924327, 40.715568 ], [ -73.924241, 40.715126 ], [ -73.924059, 40.714112 ], [ -73.92404, 40.714008 ], [ -73.923137, 40.713396 ], [ -73.922233, 40.712854 ], [ -73.921687, 40.711894 ], [ -73.921546, 40.711043 ], [ -73.921221, 40.710791 ], [ -73.920745, 40.71053 ], [ -73.921892, 40.709396 ], [ -73.921435, 40.709129 ], [ -73.920701, 40.708695 ], [ -73.91996, 40.708257 ], [ -73.919222, 40.707815 ], [ -73.918477, 40.707382 ], [ -73.917737, 40.706943 ], [ -73.916995, 40.706503 ], [ -73.916253, 40.706066 ], [ -73.915513, 40.705626 ], [ -73.914771, 40.705189 ], [ -73.914031, 40.704749 ], [ -73.913292, 40.704313 ], [ -73.912549, 40.703873 ], [ -73.911807, 40.703435 ], [ -73.912904, 40.702362 ], [ -73.912162, 40.701923 ], [ -73.911422, 40.701487 ], [ -73.910679, 40.701046 ], [ -73.911808, 40.699938 ], [ -73.911036, 40.699532 ], [ -73.910143, 40.699033 ], [ -73.909533, 40.698675 ], [ -73.908774, 40.698255 ], [ -73.908022, 40.697825 ], [ -73.907771, 40.697684 ], [ -73.90757, 40.697572 ], [ -73.907523, 40.697548 ], [ -73.907372, 40.697452 ], [ -73.907275, 40.697399 ], [ -73.90712, 40.697314 ], [ -73.907036, 40.697268 ], [ -73.906927, 40.697208 ], [ -73.906846, 40.697164 ], [ -73.906524, 40.696976 ], [ -73.906263, 40.696832 ], [ -73.906156, 40.696776 ], [ -73.906117, 40.69675 ], [ -73.905771, 40.696549 ], [ -73.905704, 40.696515 ], [ -73.905583, 40.696449 ], [ -73.905401, 40.69635 ], [ -73.905235, 40.696251 ], [ -73.905083, 40.696163 ], [ -73.905011, 40.696122 ], [ -73.904952, 40.696086 ], [ -73.90489, 40.696051 ], [ -73.904803, 40.696008 ], [ -73.904664, 40.695932 ], [ -73.90426, 40.6957 ], [ -73.904804, 40.695148 ], [ -73.904914, 40.695074 ], [ -73.9058, 40.694128 ], [ -73.905043, 40.693704 ], [ -73.904288, 40.693278 ], [ -73.903541, 40.692849 ], [ -73.902788, 40.692418 ], [ -73.902073, 40.691999 ], [ -73.90183, 40.691815 ], [ -73.90159, 40.691632 ], [ -73.901241, 40.691438 ], [ -73.901805, 40.690766 ], [ -73.900425, 40.688184 ], [ -73.901162, 40.687878 ], [ -73.896466, 40.682336 ], [ -73.896462, 40.682458 ], [ -73.896386, 40.682638 ], [ -73.896309, 40.682802 ], [ -73.896219, 40.682918 ], [ -73.896095, 40.683109 ], [ -73.895812, 40.683506 ], [ -73.89493, 40.684527 ], [ -73.89459, 40.684871 ], [ -73.894403, 40.685084 ], [ -73.894175, 40.685283 ], [ -73.892523, 40.683424 ], [ -73.89166, 40.684044 ], [ -73.890878, 40.684543 ], [ -73.890201, 40.683864 ], [ -73.889747, 40.684131 ], [ -73.889607, 40.684214 ], [ -73.889089, 40.684641 ], [ -73.888294, 40.685175 ], [ -73.888083, 40.685294 ], [ -73.887284, 40.685622 ], [ -73.884517, 40.686691 ], [ -73.883867, 40.687721 ], [ -73.883777, 40.687863 ], [ -73.883757, 40.687879 ], [ -73.883654, 40.687959 ], [ -73.882649, 40.688741 ], [ -73.882267, 40.689039 ], [ -73.881341, 40.689761 ], [ -73.880723, 40.690243 ], [ -73.879616, 40.691106 ], [ -73.879512, 40.691187 ], [ -73.879456, 40.691231 ], [ -73.877764, 40.692153 ], [ -73.874021, 40.694191 ], [ -73.868917, 40.69515 ], [ -73.868717, 40.694622 ], [ -73.868685, 40.694035 ], [ -73.868672, 40.693855 ], [ -73.868685, 40.69385 ], [ -73.868059, 40.691184 ], [ -73.86797, 40.690765 ], [ -73.867909, 40.690476 ], [ -73.867768, 40.689803 ], [ -73.867623, 40.689145 ], [ -73.867461, 40.688415 ], [ -73.867277, 40.687745 ], [ -73.867018, 40.68683 ], [ -73.866871, 40.686331 ], [ -73.866598, 40.68527 ], [ -73.866504, 40.684474 ], [ -73.866295, 40.683189 ], [ -73.866199, 40.682736 ], [ -73.866074, 40.682144 ], [ -73.866027, 40.681918 ], [ -73.864994, 40.682129 ], [ -73.864449, 40.682278 ], [ -73.864101, 40.682373 ], [ -73.863787, 40.681114 ], [ -73.863508, 40.679992 ], [ -73.863349, 40.679352 ], [ -73.863316, 40.679214 ], [ -73.863282, 40.679073 ], [ -73.862346, 40.679165 ], [ -73.862028, 40.677886 ], [ -73.861915, 40.67743 ], [ -73.861712, 40.676611 ], [ -73.861554, 40.675973 ], [ -73.861397, 40.675335 ], [ -73.861373, 40.675236 ], [ -73.861318, 40.675021 ], [ -73.861064, 40.674343 ], [ -73.861077, 40.674021 ], [ -73.860775, 40.672806 ], [ -73.860389, 40.671269 ], [ -73.859474, 40.671381 ], [ -73.859391, 40.671394 ], [ -73.858555, 40.671533 ], [ -73.857633, 40.671656 ], [ -73.857309, 40.670419 ], [ -73.857267, 40.670255 ], [ -73.857226, 40.670094 ], [ -73.856847, 40.668603 ], [ -73.856459, 40.667159 ], [ -73.856425, 40.667033 ], [ -73.856115, 40.665729 ], [ -73.856063, 40.665511 ], [ -73.855685, 40.663867 ], [ -73.856117, 40.663808 ], [ -73.856591, 40.663747 ], [ -73.857536, 40.663598 ], [ -73.85843, 40.663453 ], [ -73.85837, 40.663182 ], [ -73.858025, 40.661721 ], [ -73.857922, 40.661319 ], [ -73.857615, 40.660119 ], [ -73.858514, 40.659899 ], [ -73.859387, 40.659773 ], [ -73.860138, 40.659676 ], [ -73.86036, 40.659646 ], [ -73.861222, 40.659221 ], [ -73.861508, 40.65905 ], [ -73.863171, 40.658277 ], [ -73.8627, 40.657623 ], [ -73.862438, 40.657258 ], [ -73.862193, 40.656829 ], [ -73.861658, 40.65611 ], [ -73.860988, 40.655212 ], [ -73.860964, 40.65518 ], [ -73.860676, 40.654868 ], [ -73.860635, 40.654824 ], [ -73.860594, 40.65478 ], [ -73.860567, 40.654751 ], [ -73.860483, 40.654659 ], [ -73.860422, 40.654612 ], [ -73.859907, 40.654229 ], [ -73.859755, 40.654104 ], [ -73.85898, 40.652675 ], [ -73.856962, 40.650584 ], [ -73.856652, 40.650129 ], [ -73.856537, 40.649916 ], [ -73.856148, 40.649198 ], [ -73.855021, 40.643098 ], [ -73.854938, 40.64265 ], [ -73.848338, 40.643521 ], [ -73.842955, 40.63814 ], [ -73.838298, 40.633485 ], [ -73.838111, 40.633298 ], [ -73.837652, 40.63284 ], [ -73.837379, 40.632506 ], [ -73.837091, 40.632155 ], [ -73.836595, 40.631548 ], [ -73.836238, 40.631111 ], [ -73.835769, 40.630678 ], [ -73.835376, 40.630315 ], [ -73.833041, 40.628261 ], [ -73.833393, 40.627674 ], [ -73.8334, 40.627449 ], [ -73.833422, 40.626782 ], [ -73.833452, 40.626149 ], [ -73.8337, 40.620922 ], [ -73.834008, 40.614425 ], [ -73.834054, 40.613455 ], [ -73.834283, 40.609186 ], [ -73.834463, 40.607193 ], [ -73.835876, 40.605637 ], [ -73.842017, 40.598313 ], [ -73.844437, 40.595855 ], [ -73.84668, 40.593209 ], [ -73.847386, 40.592028 ], [ -73.849852, 40.588669 ], [ -73.853522, 40.586592 ], [ -73.854937, 40.585974 ], [ -73.855106, 40.585899 ], [ -73.859492, 40.583938 ], [ -73.865607, 40.581205 ], [ -73.866005, 40.581001 ], [ -73.867374, 40.580299 ], [ -73.86751, 40.580229 ], [ -73.867661, 40.580151 ], [ -73.868425, 40.579757 ], [ -73.869232, 40.579342 ], [ -73.870239, 40.57897 ], [ -73.871212, 40.578611 ], [ -73.871418, 40.578535 ], [ -73.871613, 40.578438 ], [ -73.879222, 40.574656 ], [ -73.885069, 40.573749 ], [ -73.886292, 40.573505 ], [ -73.890518, 40.572705 ], [ -73.902874, 40.5717 ], [ -73.903871, 40.571619 ], [ -73.904908, 40.571407 ], [ -73.918487, 40.569284 ], [ -73.926236, 40.568072 ], [ -73.927424, 40.567887 ], [ -73.92807, 40.567786 ], [ -73.928717, 40.567681 ], [ -73.932716, 40.567034 ], [ -73.933363, 40.566929 ], [ -73.933448, 40.566914 ], [ -73.934894, 40.566652 ], [ -73.936525, 40.566436 ], [ -73.945004, 40.565313 ], [ -73.94988, 40.564555 ], [ -73.950703, 40.564427 ], [ -73.951009, 40.56439 ], [ -73.951195, 40.564368 ], [ -73.951409, 40.564343 ], [ -73.95167, 40.564312 ], [ -73.952309, 40.564208 ], [ -73.958844, 40.56315 ], [ -73.959281, 40.563079 ], [ -73.959715, 40.563013 ], [ -73.997087, 40.557283 ], [ -74.036293, 40.551042 ], [ -74.036285, 40.551185 ], [ -74.036216, 40.5524 ], [ -74.036209, 40.552524 ], [ -74.035978, 40.556563 ], [ -74.035153, 40.571003 ], [ -74.035053, 40.572747 ], [ -74.035047, 40.57285 ], [ -74.034547, 40.57625 ], [ -74.037877, 40.589201 ], [ -74.041393, 40.603106 ], [ -74.042036, 40.604088 ], [ -74.043722, 40.606662 ], [ -74.043741, 40.606691 ], [ -74.043795, 40.606773 ], [ -74.043832, 40.606828 ], [ -74.043889, 40.606913 ], [ -74.043945, 40.606996 ], [ -74.044074, 40.607188 ], [ -74.044114, 40.607248 ], [ -74.045197, 40.608863 ], [ -74.046451, 40.610735 ], [ -74.047514, 40.612319 ], [ -74.047568, 40.612406 ], [ -74.048101, 40.613264 ], [ -74.049075, 40.61483 ], [ -74.049624, 40.615712 ], [ -74.049963, 40.616257 ], [ -74.052352, 40.620134 ], [ -74.052477, 40.620344 ], [ -74.053522, 40.622091 ], [ -74.053838, 40.622618 ], [ -74.055324, 40.625103 ], [ -74.05663, 40.627287 ], [ -74.056468, 40.631715 ] ] ] } } 6 | ] 7 | } 8 | --------------------------------------------------------------------------------