├── README.md ├── cgi-bin └── isochrone.py ├── contour.py └── www ├── DrawPoints.js └── index.html /README.md: -------------------------------------------------------------------------------- 1 | pysochrone 2 | ========== 3 | pysochrone is a simple Python script for creating isochrone maps. Isochrones are curves of equal travel time from a certain point of origin. Really this can be used for any type of isoline (aka contour) map. For more information: 4 | * http://en.wikipedia.org/wiki/Contour_line 5 | * http://wiki.openstreetmap.org/wiki/Isochrone 6 | 7 | Code is heavily borrowed from the [Quantum GIS](http://www.qgis.org/) Contour plugin. 8 | 9 | Requirements 10 | ------------ 11 | * [Python](http://www.python.org/) 12 | * [numpy](http://numpy.scipy.org/) 13 | * [matplotlib](http://matplotlib.sourceforge.net/) 14 | * [Shapely](http://pypi.python.org/pypi/Shapely) 15 | * [OGR](http://www.gdal.org/ogr/) 16 | * [pgRouting](http://www.pgrouting.org/) (optional) 17 | 18 | Use 19 | --- 20 | The contour module has a class Contours which creates contour lines or filled contour polygons from an OGR compatible datasource. A cgi script, isochrone.py, uses this class to provide a RESTful service 21 | 22 | Todo 23 | ---- 24 | * Include simple Dijkstra algorithm to compute shortest path tree to remove 25 | the need for a PostGIS/pgRouting database 26 | * Allow download of OSM data on demand (for small areas) 27 | * Provide more options, such as changing grid size 28 | * Provide more methods, such as alpha shapes or buffered roads 29 | -------------------------------------------------------------------------------- /cgi-bin/isochrone.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import os 4 | import sys 5 | import cgi 6 | import cgitb 7 | cgitb.enable() 8 | 9 | # set HOME env var to a directory the httpd server can write to (matplotlib) 10 | os.environ['HOME'] = '/tmp/' 11 | 12 | from contour import Contours, ContourError 13 | 14 | form = cgi.FieldStorage() 15 | if "lon" not in form or "lat" not in form: 16 | print "Content-Type: text/plain\n" 17 | print "Error, no lon/lat specified" 18 | 19 | # TODO: should make sure float values were passed 20 | pt = form["lon"].value + " " + form["lat"].value 21 | sql = """SELECT * 22 | FROM vertices_tmp 23 | JOIN 24 | (SELECT * FROM driving_distance(' 25 | SELECT gid AS id, 26 | source::int4, 27 | target::int4, 28 | length::float8 AS cost 29 | FROM ways', 30 | (SELECT id FROM vertices_tmp ORDER BY distance(the_geom, 31 | ST_GeomFromText('POINT(""" + pt + """)',4326)) LIMIT 1), 32 | 3, 33 | false, 34 | false)) AS route 35 | ON 36 | vertices_tmp.id = route.vertex_id;""" 37 | 38 | c = Contours() 39 | c.getDataFromOGR(dataSrcName='PG: host=localhost dbname=virginia ' 40 | 'user=postgres password=postgres', 41 | fieldName='cost', sql=sql) 42 | c.computeGrid() 43 | c.setLevels(0.0, 1.6, 5) 44 | 45 | try: 46 | # on one system had trouble letting OGR print to stdout, so do so manually 47 | tempname = '/tmp/walk.json' 48 | if os.path.exists(tempname): 49 | os.remove(tempname) 50 | c.createFilledContourLayer(driverName="GeoJSON", fileName=tempname) 51 | json = open(tempname, 'r') 52 | print "Content-Type: application/json\n" 53 | print json.read() 54 | except ContourError as detail: 55 | print "Content-Type: text/plain\n", detail 56 | -------------------------------------------------------------------------------- /contour.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # contour.py 5 | # 6 | # Copyright 2009 Lionel Roubeyrie 7 | # Copyright 2011 Josh Doe 8 | # 9 | # This program is free software; you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by 11 | # the Free Software Foundation; either version 2 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # This program is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU General Public License 20 | # along with this program; if not, write to the Free Software 21 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 22 | # MA 02110-1301, USA. 23 | 24 | # Modified by Chris Crook to contour irregular data 25 | 26 | 27 | import sys 28 | import os.path 29 | import string 30 | import math 31 | import inspect 32 | 33 | import numpy as np 34 | import matplotlib 35 | matplotlib.use("Agg") # use non-graphical backend 36 | import matplotlib.pyplot as plt 37 | from matplotlib.mlab import griddata 38 | from shapely.geometry import MultiLineString, MultiPolygon 39 | import shapely 40 | import ogr 41 | 42 | 43 | # global constants 44 | EPSILON = 1.e-27 45 | 46 | 47 | class ContourError(Exception): 48 | """Used for all exceptions created by this module.""" 49 | def __init__(self, message): 50 | self._message = message 51 | 52 | def __str__(self): 53 | return self._message 54 | 55 | 56 | class Contours(): 57 | """This class allows the creation of contour lines and/or filled contour 58 | polygons from irregular point data 59 | """ 60 | def __init__(self): 61 | self._data = None 62 | self._gridData = None 63 | self._levels = None 64 | 65 | def getLevels(self): 66 | return self._levels 67 | 68 | def setLevels(self, start, stop, num): 69 | self._levels = np.linspace(float(start), float(stop), num) 70 | 71 | # generate grid and interpolate 72 | def computeGrid(self): 73 | x, y, z = self._data 74 | 75 | self.gridSpacing = math.sqrt((max(x) - min(x)) * 76 | (max(y) - min(y)) / len(x)) / 2.0 77 | gridSpacing = self.gridSpacing 78 | 79 | if gridSpacing <= 0.0: 80 | raise ContourError("Grid spacing must be greater than 0") 81 | 82 | # make grid 83 | x0 = math.floor(min(x) / gridSpacing) * gridSpacing 84 | nx = int(math.floor((max(x) - x0) / gridSpacing)) + 1 85 | gx = np.linspace(x0, x0 + gridSpacing * nx, nx) 86 | 87 | y0 = math.floor(min(y) / gridSpacing) * gridSpacing 88 | ny = int(math.floor((max(y) - y0) / gridSpacing)) + 1 89 | gy = np.linspace(y0, y0 + gridSpacing * ny, ny) 90 | 91 | try: 92 | # interpolate values on grid 93 | gz = griddata(x, y, z, gx, gy) 94 | except: 95 | raise ContourError("Unable to generate a grid for this data set") 96 | 97 | self._gridData = (gx, gy, gz) 98 | 99 | def getDataFromOGR(self, dataSrcName, fieldName, layerName=None, sql=None): 100 | self._gridData = None 101 | self._data = None 102 | 103 | datasrc = ogr.Open(dataSrcName) 104 | if datasrc == None: 105 | raise ContourError("Failed to open data source '%s'" % dataSrcName) 106 | 107 | if layerName: 108 | layer = datasrc.GetLayerByName(layerName) 109 | elif sql: 110 | layer = datasrc.ExecuteSQL(sql) 111 | else: 112 | layer = datasrc.GetLayer(0) 113 | if layer is None: 114 | raise ContourError("Failed to read layer") 115 | 116 | x = list() 117 | y = list() 118 | z = list() 119 | # read points 120 | fieldIndex = None 121 | while 1: 122 | feat = layer.GetNextFeature() 123 | if not feat: 124 | break 125 | 126 | if fieldIndex is None and fieldName is not None: 127 | fieldIndex = feat.GetFieldIndex(fieldName) 128 | 129 | inGeom = shapely.wkb.loads(feat.GetGeometryRef().ExportToWkb()) 130 | x.append(inGeom.x) 131 | y.append(inGeom.y) 132 | if fieldIndex: 133 | z.append(feat.GetFieldAsDouble(fieldIndex)) 134 | else: 135 | z.append(inGeom.z) 136 | 137 | # convert to numpy arrays 138 | x = np.array(x) 139 | y = np.array(y) 140 | z = np.array(z) 141 | self._data = [x, y, z] 142 | return x, y, z 143 | 144 | def computeContours(self): 145 | gx, gy, gz = self._gridData 146 | levels = self.getLevels() 147 | # cs = plt.tricontour(x, y, z, levels, extend=extend) 148 | CS = plt.contour(gx, gy, gz, levels, extend='neither') 149 | lines = list() 150 | for i, line in enumerate(CS.collections): 151 | lines.append([i, levels[i], 152 | [path.vertices for path in line.get_paths()]]) 153 | self._lines = lines 154 | 155 | def computeFilledContours(self): 156 | gx, gy, gz = self._gridData 157 | levels = self.getLevels() 158 | #cs = plt.tricontourf(x, y, z, levels, extend=extend) 159 | CS = plt.contourf(gx, gy, gz, levels, extend='neither') 160 | polygons = list() 161 | for i, polygon in enumerate(CS.collections): 162 | mpoly = [] 163 | for path in polygon.get_paths(): 164 | path.should_simplify = False 165 | poly = path.to_polygons() 166 | exterior = [] 167 | holes = [] 168 | if len(poly) > 0: 169 | exterior = poly[0] # and interiors (holes) are in poly[1:] 170 | # Crazy correction of one vertice polygon, 171 | # mpl doesn't care about it 172 | if len(exterior) == 1: 173 | p0 = exterior[0] 174 | exterior = np.vstack(exterior, self.epsi_point(p0), 175 | self.epsi_point(p0)) 176 | if len(poly) > 1: # There's some holes 177 | for h in poly[1:]: 178 | if len(h) > 2: 179 | holes.append(h) 180 | 181 | mpoly.append([exterior, holes]) 182 | polygons.append([i, levels[i], levels[i + 1], mpoly]) 183 | self._polygons = polygons 184 | 185 | def epsi_point(self, point): 186 | x = point[0] + EPSILON * np.random.random() 187 | y = point[1] + EPSILON * np.random.random() 188 | return [x, y] 189 | 190 | # TODO: use OGR instead of QGIS 191 | # def createContourLayer(self, lines): 192 | # dec = self.uPrecision.value() 193 | # name = "%s" % str(self.uOutputName.text()) 194 | # vl = self.createVectorLayer("MultiLineString", name) 195 | # pr = vl.dataProvider() 196 | # pr.addAttributes( [QgsField("index", QVariant.Int, "Int"), 197 | # QgsField(self._zField, QVariant.Double, "Double"), 198 | # QgsField("label", QVariant.String, "String")] ) 199 | # msg = list() 200 | # for i, level, line in lines: 201 | # try: 202 | # fet = QgsFeature() 203 | # fet.setGeometry(QgsGeometry.fromWkt(QString(MultiLineString(line).to_wkt()))) 204 | # fet.setAttributeMap( { 0 : QVariant(i), 1 : QVariant(level), 205 | # 2 : QVariant( str("%s"%np.round(level, dec)) ) 206 | # 207 | # } ) 208 | # pr.addFeatures( [ fet ] ) 209 | # except: 210 | # msg.append("%s"%level) 211 | # if len(msg) > 0: 212 | # self.message("Levels not represented : %s"%", ".join(msg),"Contour issue") 213 | # vl.updateExtents() 214 | # vl.commitChanges() 215 | # return vl 216 | 217 | def createFilledContourLayer(self, driverName, fileName="/vsistdout"): 218 | self.computeFilledContours() 219 | 220 | drv = ogr.GetDriverByName(driverName) 221 | if drv is None: 222 | raise ContourError("%s driver not available." % driverName) 223 | 224 | #name = "%s"%str(self.uOutputName.text()) 225 | #pr.addAttributes( [QgsField("index", QVariant.Int, "Int"), 226 | # QgsField(self._zField+"_min", QVariant.Double, "Double"), 227 | # QgsField(self._zField+"_max", QVariant.Double, "Double"), 228 | # QgsField("label", QVariant.String, "String")] ) 229 | 230 | ds = drv.CreateDataSource(fileName) 231 | if ds is None: 232 | raise ContourError("Creation of output file %s failed" % fileName) 233 | 234 | lyr = ds.CreateLayer("filled_contours", None, ogr.wkbMultiPolygon) 235 | if lyr is None: 236 | raise ContourError("Layer creation failed.") 237 | 238 | field_defn = ogr.FieldDefn("level_min", ogr.OFTReal) 239 | if lyr.CreateField(field_defn) != 0: 240 | raise ContourError("Creating level_min field failed.") 241 | 242 | msg = list() 243 | for i, level_min, level_max, polygon in self._polygons: 244 | try: 245 | feat = ogr.Feature(lyr.GetLayerDefn()) 246 | 247 | feat.SetField("level_min", level_min) 248 | 249 | wkb = MultiPolygon(polygon).to_wkb() 250 | pt = ogr.CreateGeometryFromWkb(wkb) 251 | 252 | feat.SetGeometry(pt) 253 | 254 | if lyr.CreateFeature(feat) != 0: 255 | raise ContourError("Failed to create feature in shapefile.") 256 | 257 | feat.Destroy() 258 | except: 259 | msg.append(str(sys.exc_info()[1])) 260 | msg.append("%s" % level_min) 261 | 262 | ds = None 263 | 264 | if __name__ == "__main__": 265 | # Testing Contours using points.shp with 'cost' attribute 266 | c = Contours(dataSrcName='points.shp', fieldName='cost') 267 | c.setLevels(0.0, 2.0, 5) 268 | # Create filled contour layer and write to stdout as GeoJSON 269 | c.createFilledContourLayer(driverName="GeoJSON") 270 | -------------------------------------------------------------------------------- /www/DrawPoints.js: -------------------------------------------------------------------------------- 1 | DrawPoints = OpenLayers.Class(OpenLayers.Control.DrawFeature, { 2 | 3 | // this control is active by default 4 | autoActivate: true, 5 | 6 | initialize: function(layer, options) { 7 | // only points can be drawn 8 | var handler = OpenLayers.Handler.Point; 9 | OpenLayers.Control.DrawFeature.prototype.initialize.apply( 10 | this, [layer, handler, options] 11 | ); 12 | }, 13 | 14 | drawFeature: function(geometry) { 15 | OpenLayers.Control.DrawFeature.prototype.drawFeature.apply( 16 | this, arguments 17 | ); 18 | if (this.layer.features.length == 1) { 19 | this.deactivate(); 20 | } 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | A Basic GeoExt Page 5 | 6 | 7 | 8 | 9 | 10 | 12 | 13 | 14 | 15 | 16 | 17 | 145 | 146 | 147 |
148 |
149 | 150 | 151 | --------------------------------------------------------------------------------