├── .gitignore ├── LICENSE ├── README.md ├── postgis2geojson.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | __pycache__ 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | 30 | # Translations 31 | *.mo 32 | 33 | # Mr Developer 34 | .mr.developer.cfg 35 | .project 36 | .pydevproject 37 | 38 | data.geojson -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PostGIS2GeoJSON 2 | 3 | ## postgis2geojson.py 4 | 5 | A simple tool for exporting from a PostGIS table to GeoJSON and TopoJSON. Assumes [Python 2.7+](http://www.python.org/download/), 6 | [psycopg2](http://initd.org/psycopg/download/), and [TopoJSON](https://github.com/mbostock/topojson/wiki/Installation) are already installed and in your ````PATH````. 7 | 8 | Adapted from Bryan McBride's excellent [PHP implementation](https://gist.github.com/bmcbride/1913855/). 9 | 10 | ####Example usage: 11 | 12 | To export table ````boundaries```` from database ````gisdata```` as user ````user```` to both GeoJSON and TopoJSON: 13 | 14 | ```` 15 | python postgis2geojson.py -d gisdata -t boundaries -u user --topojson 16 | ```` 17 | 18 | or, also specify that the geometry column is ````the_geom````, only fields ````oid```` and ````name```` should be returned, and the output file should be called ````boundary_data````: 19 | 20 | ```` 21 | python postgis2geojson.py -d gisdata -t boundaries -u user -p password -g the_geom -f oid name -o boundary_data --topojson 22 | ```` 23 | 24 | ####Arguments: 25 | 26 | | Name | Argument | Default value | Required | Description | 27 | |--------------|:------------------:|:-------------:|:----------:|------------------| 28 | | Help | ````-h```` | | | Show friendly help message | 29 | | Database | ````-d```` | | Y | Database to use | 30 | | Host | ````-H```` | localhost | | Host to connect to | 31 | | User | ````-U```` | postgres | | Postgres user to use | 32 | | Password | ````-p```` | | | Password for Postgres user | 33 | | Port | ````-P```` | 5432 | | Database port | 34 | | Table | ````-t ```` | | Y | Table to query | 35 | | Fields | ````-f ```` | * | | Database fields to return, separated by a single space | 36 | | Geometry | ````-g```` | geom | | Name of geometry column | 37 | | Where | ````-w```` | | | Optional WHERE clause to add to the SQL query | 38 | | File | ````-o```` | data.geojson | | Name of output file | 39 | | Topojson | ````--topojson```` | | | Creates a TopoJSON file in addtion to a GeoJSON | 40 | | Pretty print | ````--pretty```` | | | Pretty print the output | 41 | 42 | A full list of options is also available via ````python postgis2geojson.py --help````. 43 | -------------------------------------------------------------------------------- /postgis2geojson.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import datetime 3 | import decimal 4 | import json 5 | import subprocess 6 | 7 | import psycopg2 8 | 9 | 10 | parser = argparse.ArgumentParser( 11 | description="Create a GeoJSON from a PostGIS query.", 12 | epilog="Example usage: python postgis2geojson.py -d awesomeData -h localhost -u user -p securePassword -t table -f id name geom -w 'columnA = columnB' -o myData --topojson") 13 | 14 | parser.add_argument("-d", "--database", dest="database", 15 | type=str, required=True, 16 | help="The database to connect to") 17 | 18 | # Python doesn't let you use -h as an option for some reason 19 | parser.add_argument("-H", "--host", dest="host", 20 | default="localhost", type=str, 21 | help="Database host. Defaults to 'localhost'") 22 | 23 | parser.add_argument("-u", "--user", dest="user", 24 | default="postgres", type=str, 25 | help="Database user. Defaults to 'postgres'") 26 | 27 | parser.add_argument("-p", "--password", dest="password", 28 | default="", type=str, 29 | help="Password for the database user") 30 | 31 | parser.add_argument("-P", "--port", dest="port", 32 | default="5432", type=str, 33 | help="Password for the database user") 34 | 35 | parser.add_argument("-t", "--table", dest="table", 36 | type=str, required=True, 37 | help="Database table to query") 38 | 39 | parser.add_argument("-f", "--fields", dest="fields", 40 | nargs="+", 41 | help="Fields to return separated by a single space. Defaults to *") 42 | 43 | parser.add_argument("-g", "--geometry", dest="geometry", 44 | default="geom", type=str, 45 | help="Name of the geometry column. Defaults to 'geom'") 46 | 47 | parser.add_argument("-w", "--where", dest="where", 48 | type=str, 49 | help="Optional WHERE clause to add to the SQL query") 50 | 51 | parser.add_argument("-o", "--output", dest="file", 52 | default="data", type=str, 53 | help="Output file name without extension. Defaults to 'data.geojson'") 54 | 55 | parser.add_argument("--topojson", dest="topojson", 56 | action="store_true", 57 | help="Use if you would also like a copy of the data as TopoJSON") 58 | 59 | parser.add_argument("--pretty", dest="pretty", 60 | action="store_true", 61 | help="Pretty print the output (indent).") 62 | 63 | arguments = parser.parse_args() 64 | 65 | # Fix to float decimals 66 | # http://stackoverflow.com/questions/16957275/python-to-json-serialization-fails-on-decimal 67 | def check_for_decimals(obj): 68 | if isinstance(obj, decimal.Decimal): 69 | return float(obj) 70 | raise TypeError 71 | 72 | def getData(): 73 | # Connect to the database 74 | try: 75 | conn = psycopg2.connect("dbname=" + arguments.database + " user=" + arguments.user + " host=" + arguments.host + " port=" + arguments.port + " password="+ arguments.password) 76 | except: 77 | print "Unable to connect to the database. Please check your options and try again." 78 | return 79 | 80 | # Create a cursor for executing queries 81 | cur = conn.cursor() 82 | 83 | # Start building the query 84 | query = "SELECT " 85 | 86 | # If a list of fields were provided, add those 87 | if isinstance(arguments.fields, list): 88 | for each in arguments.fields: 89 | query += each + ", " 90 | 91 | # Otherwise, just select everything 92 | else: 93 | query += "*, " 94 | 95 | query += "ST_AsGeoJSON(" + arguments.geometry + ") AS geometry FROM " + arguments.table 96 | 97 | # If a WHERE statement was provided, add that 98 | if arguments.where is not None: 99 | query += " WHERE " + arguments.where + ";" 100 | else: 101 | query += ";" 102 | 103 | # Execute the query 104 | try: 105 | cur.execute(query) 106 | except Exception as exc: 107 | print "Unable to execute query. Error was {0}".format(str(exc)) 108 | return 109 | 110 | # Retrieve the results of the query 111 | rows = cur.fetchall() 112 | 113 | # Get the column names returned 114 | colnames = [desc[0] for desc in cur.description] 115 | 116 | # Find the index of the column that holds the geometry 117 | geomIndex = colnames.index("geometry") 118 | 119 | feature_collection = {'type': 'FeatureCollection', 'features': []} 120 | 121 | # For each row returned... 122 | for row in rows: 123 | feature = { 124 | 'type': 'Feature', 125 | 'geometry': json.loads(row[geomIndex]), 126 | 'properties': {}, 127 | } 128 | 129 | for index, colname in enumerate(colnames): 130 | if colname not in ('geometry', arguments.geometry): 131 | if isinstance(row[index], datetime.datetime): 132 | # datetimes are not JSON.dumpable, manually stringify these. 133 | value = str(row[index]) 134 | else: 135 | value = row[index] 136 | feature['properties'][colname] = value 137 | 138 | feature_collection['features'].append(feature) 139 | 140 | indent = 2 if arguments.pretty else None 141 | jsonified = json.dumps(feature_collection, indent=indent, default=check_for_decimals) 142 | 143 | # Write it to a file 144 | with open(arguments.file + '.geojson', 'w') as outfile: 145 | outfile.write(jsonified) 146 | 147 | # If a TopoJSON conversion is requested... 148 | if arguments.topojson is True: 149 | topojson() 150 | else: 151 | print "Done!" 152 | 153 | def topojson(): 154 | command = "topojson -o " + arguments.file + ".topojson -p -- " + arguments.file + ".geojson" 155 | subprocess.call(command, shell=True) 156 | 157 | # Start the process 158 | getData() 159 | 160 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | psycopg2==2.5.1 2 | --------------------------------------------------------------------------------