Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# -*- coding: utf-8 -*- 

2""" 

3@file 

4@brief Data related to a challenge, streets in Seattle 

5""" 

6import os 

7import pandas 

8from pyensae.datasource import download_data 

9from pyensae.notebookhelper import folium_html_map 

10 

11 

12def get_fields_description(): 

13 """ 

14 Retrieves a :epkg:`dataframe` which describes 

15 the meaning of the metadata. 

16 

17 @return dataframe 

18 """ 

19 from .seattle_streets import __file__ as local_dir 

20 this = os.path.join(os.path.dirname(local_dir), "street_seattle.desc.xlsx") 

21 return pandas.read_excel(this) 

22 

23 

24def get_seattle_streets(filename=None, folder="."): 

25 """ 

26 Retrieves processed data from 

27 `Seattle Streets <https://data.seattle.gov/dataset/Street-Network-Database/ 

28 afip-2mzr/data)>`_. 

29 

30 @param filename local filename 

31 @param folder temporary folder where to download files 

32 @return shapes, records 

33 

34 The function returns a filename. 

35 """ 

36 if filename is None: 

37 names = download_data("WGS84_seattle_street.zip", whereTo=folder) 

38 shp = [n for n in names if n.endswith('.shp')] 

39 if len(shp) != 1: 

40 from pyquickhelper.loghelper import BufferedPrint 

41 buf = BufferedPrint() 

42 names = download_data("WGS84_seattle_street.zip", 

43 whereTo=folder, fLOG=buf.fprint) 

44 raise FileNotFoundError( 

45 "Unable to download data 'WGS84_seattle_street.zip' to '{0}', log={1}\nnames={2}.".format( 

46 filename, str(buf), "\n".join(names))) 

47 filename = shp[0] 

48 elif not os.path.exists(filename): 

49 raise FileNotFoundError(filename) 

50 return filename 

51 

52 

53def shapely_records(filename, **kwargs): 

54 """ 

55 Uses `pyshp <https://pypi.python.org/pypi/pyshp/>`_ to return 

56 shapes and records from shapefiles. 

57 

58 @param filename filename 

59 @param kwargs addition parameter for the shapefile reader, 

60 useful options car *encoding* and *encodingErrors* 

61 @return shapes, records, fields 

62 

63 .. faqref:: 

64 :title: Fields in shapefiles 

65 

66 The following codes is usually used to retrieve 

67 shapefiles: 

68 

69 :: 

70 

71 rshp = shapefile.Reader(filename) 

72 shapes = rshp.shapes() 

73 records = rshp.records() 

74 

75 *records* contains metadata about each shape. 

76 Fields and values are not stored in a dictionary. 

77 Here is a snippet of code to do so: 

78 

79 :: 

80 

81 {k[0]: v for k, v in zip(rshp.fields[1:], records[0])} 

82 

83 Here is an example of the results: 

84 

85 :: 

86 

87 {'ORD_PRE_DI': 'SW', 

88 'ORD_STNAME': 'SW 149TH ST', 

89 'ORD_STREET': '149TH', 

90 'ORD_STRE_1': 'ST', 

91 'ORD_SUF_DI': b' ', 

92 'R_ADRS_FRO': 976, 

93 ... 

94 

95 """ 

96 import shapefile 

97 rshp = shapefile.Reader(filename, **kwargs) 

98 shapes = rshp.shapes() 

99 records = rshp.records() 

100 try: 

101 rshp.shp.close() 

102 except IOError: 

103 pass 

104 try: 

105 rshp.shx.close() 

106 except IOError: 

107 pass 

108 try: 

109 rshp.dbf.close() 

110 except IOError: 

111 pass 

112 return shapes, records, rshp.fields 

113 

114 

115def folium_html_street_map(subset, shapes, html_width=None, html_height=None, 

116 color_vertices=None, **kwargs): 

117 """ 

118 Returns a :epkg:`folium` map which represents the streets. 

119 

120 @param subset list of streets index 

121 @param shapes list of shapes 

122 @param kwargs extra parameters for `Map <https://github.com/python-visualization/folium/blob/master/folium/folium.py#L19>`_ 

123 @param html_width sent to function 

124 `folium_html_map <http://www.xavierdupre.fr/app/pyensae/helpsphinx/pyensae/notebookhelper/folium_helper.html 

125 #pyensae.notebookhelper.folium_helper.folium_html_map>`_ 

126 @param html_height sent to function 

127 `folium_html_map <http://www.xavierdupre.fr/app/pyensae/helpsphinx/pyensae/notebookhelper/folium_helper.html 

128 #pyensae.notebookhelper.folium_helper.folium_html_map>`_ 

129 @param color_vertices see below 

130 @return see function 

131 `folium_html_map <http://www.xavierdupre.fr/app/pyensae/helpsphinx/pyensae/notebookhelper/folium_helper.html 

132 #pyensae.notebookhelper.folium_helper.folium_html_map>`_ 

133 

134 if *color_vertices* is equal to `'odd'`, the function computes the degree 

135 of each vertex and choose a different color for odd (yellow) 

136 and even degrees (black). 

137 """ 

138 if color_vertices == "odd": 

139 count = {} 

140 for edge in subset: 

141 shape = shapes[edge] 

142 a = (shape.points[0][0], shape.points[0][1]) 

143 b = (shape.points[-1][0], shape.points[-1][1]) 

144 count[a] = count.get(a, 0) + 1 

145 count[b] = count.get(b, 0) + 1 

146 color_vertices = {k: ('yellow' if v % 2 == 1 else 'black') 

147 for k, v in count.items()} 

148 import folium 

149 map_osm = None 

150 for i, index in enumerate(subset): 

151 shape = shapes[index] 

152 if map_osm is None: 

153 if "location" not in kwargs: 

154 x, y = shape.points[0] 

155 map_osm = folium.Map(location=[y, x], **kwargs) 

156 else: 

157 map_osm = folium.Map(kwargs["location"], **kwargs) 

158 map_osm.add_child(folium.PolyLine( 

159 locations=[(_[1], _[0]) for _ in shape.points], weight=10)) 

160 if color_vertices: 

161 a = (shape.points[0][0], shape.points[0][1]) 

162 b = (shape.points[-1][0], shape.points[-1][1]) 

163 c1 = color_vertices[a] 

164 c2 = color_vertices[b] 

165 map_osm.add_child(folium.CircleMarker( 

166 [shape.points[0][1], shape.points[0][0]], popup=str((i, index)), radius=8, fill_color=c1)) 

167 map_osm.add_child(folium.CircleMarker( 

168 [shape.points[-1][1], shape.points[-1][0]], popup=str((i, index)), radius=8, fill_color=c2)) 

169 else: 

170 map_osm.add_child(folium.CircleMarker( 

171 [shape.points[0][1], shape.points[0][0]], popup=str((i, index)), radius=8, fill_color="yellow")) 

172 map_osm.add_child(folium.CircleMarker( 

173 [shape.points[-1][1], shape.points[-1][0]], popup=str((i, index)), radius=8, fill_color="yellow")) 

174 return folium_html_map(map_osm, width=html_width, height=html_height) 

175 

176 

177def build_streets_vertices(edges, shapes): 

178 """ 

179 Returns vertices and edges based on the subset of edges. 

180 

181 @param edges indexes 

182 @param shapes streets 

183 @return vertices, edges 

184 

185 *vertices* is a list of points. 

186 *edges* is a list of `tuple(a, b)` where `a`, `b` are 

187 indices refering to the array of vertices 

188 """ 

189 points = [] 

190 for i in edges: 

191 p = shapes[i].points 

192 a, b = (p[0][0], p[0][1]), (p[-1][0], p[-1][1]) 

193 points.append(a) 

194 points.append(b) 

195 vertices = list(sorted(set(points))) 

196 positions = {p: i for i, p in enumerate(vertices)} 

197 new_edges = [] 

198 for i in edges: 

199 points = shapes[i].points 

200 a, b = (points[0][0], points[0][1]), (points[-1][0], points[-1][1]) 

201 new_edges.append((positions[a], positions[b])) 

202 return vertices, new_edges 

203 

204 

205def plot_streets_network_projection(central_longitude=0.0, min_latitude=-80.0, 

206 max_latitude=84.0, globe=None, 

207 latitude_true_scale=0.0): 

208 """ 

209 Returns the default projection for @see fn plot_streets_network. 

210 See `projection <https://scitools.org.uk/cartopy/docs/v0.15/crs/projections.html>`_. 

211 """ 

212 import cartopy.crs as ccrs # pylint: disable=E0401 

213 return ccrs.PlateCarree() 

214 

215 

216def plot_streets_network(edges_index, edges, vertices, shapes, order=None, 

217 color_vertices=None, color_edges=None, ax=None, **kwargs): 

218 """ 

219 Plots the network based on :epkg:`cartopy`. 

220 

221 @param edges_index index of the edges in shapes 

222 @param edges list of tuple(v1, v2) in array of vertices 

223 @param vertices list of vertices coordinates 

224 @param shapes streets 

225 @param order list of edges composing a path (eulerian path) 

226 @param color_vertices dictionary ``{ vertex_index: color }``, 

227 changes the color associated to each vertex (black by default) 

228 @param color_edges dictionary ``{ edges_index: color }``, 

229 changes the color associated to each edge (black by default) 

230 @param ax axis or None 

231 @param kwargs parameter used to create the plot is ax is None 

232 @return ax 

233 

234 *kwargs* may contain parameters: 

235 *color_v*, *color_e*, *size_v*, *size_e*, *size_c*, *size_et*. 

236 

237 If *order* is not None, the function replaces the edge index by its position in this array. 

238 

239 if *color_vertices* is equal to `'odd'`, the function computes the degree 

240 of each vertex and choose a different color for odd (yellow) 

241 and even degrees (black). 

242 If *ax* is predefined, it should contain the parameter:: 

243 

244 import cartopy.crs as ccrs 

245 import matplotlib.pyplot as plt 

246 from ensae_projects.datainc.data_geo_streets import plot_streets_network_projection 

247 fig = plt.figure(figsize=(7,7)) 

248 ax = fig.add_subplot(1, 1, 1, projection=plot_streets_network_projection()) 

249 

250 The default projection is given by @see fn plot_streets_network_projection. 

251 https://scitools.org.uk/cartopy/docs/v0.15/examples/hurricane_katrina.html 

252 """ 

253 import cartopy.feature as cfeature # pylint: disable=E0401 

254 import matplotlib.pyplot as plt 

255 from matplotlib.lines import Line2D 

256 import numpy 

257 from matplotlib.path import Path 

258 

259 params = ["color_v", "color_e", "size_v", "size_e", 

260 "size_c", "size_et", "projection"] 

261 if ax is None: 

262 options = {k: v for k, v in kwargs.items() if k not in params} 

263 fig = plt.figure(**options) 

264 projection = plot_streets_network_projection() 

265 ax = fig.add_subplot(1, 1, 1, projection=projection) 

266 else: 

267 projection = kwargs.get( 

268 'projection', plot_streets_network_projection()) 

269 

270 try: 

271 ax.add_feature(cfeature.OCEAN) 

272 ax.add_feature(cfeature.COASTLINE) 

273 ax.add_feature(cfeature.BORDERS, linestyle=':') 

274 except AttributeError as e: 

275 raise AttributeError( 

276 "cartopy is not properly set up, did you use parameter projection?") from e 

277 

278 x1, x2 = min(_[0] for _ in vertices), max(_[0] for _ in vertices) 

279 y1, y2 = min(_[1] for _ in vertices), max(_[1] for _ in vertices) 

280 dx, dy = (x2 - x1) * 0.2, (y2 - y1) * 0.2 

281 x1 -= dx 

282 x2 += dx 

283 y1 -= dy 

284 y2 += dy 

285 ax.set_extent([x1, x2, y1, y2], projection) 

286 

287 for n in edges_index: 

288 sh = shapes[n] 

289 geo_points = sh.points 

290 lons = [_[0] for _ in geo_points] 

291 lats = [_[1] for _ in geo_points] 

292 ecolor = color_edges.get( 

293 n, "black") if color_edges is not None else "black" 

294 

295 linewidth = kwargs.get('size_e', 2) 

296 

297 line = Line2D(lons, lats, lw=linewidth, color=ecolor, axes=ax) 

298 ax.add_line(line) 

299 

300 mx, my = (lons[0] + lons[-1]) / 2, (lats[0] + lats[-1]) / 2 

301 gx, gy = mx, my 

302 if order is None: 

303 ax.text(gx, gy, "e%d" % n, color=kwargs.get('color_e', "blue")) 

304 else: 

305 pos = [i + 1 for i, v in enumerate(order) if v == n] 

306 if len(pos) > 0: 

307 pos = [str(_) for _ in pos] 

308 ax.text(gx, gy, ",".join(pos), 

309 size=kwargs.get("size_et", 12), 

310 color=kwargs.get('color_e', "blue")) 

311 

312 if color_vertices == "odd": 

313 count = {} 

314 for a, b in edges: 

315 count[a] = count.get(a, 0) + 1 

316 count[b] = count.get(b, 0) + 1 

317 color_vertices = {k: ('yellow' if v % 2 == 1 else 'black') 

318 for k, v in count.items()} 

319 

320 theta = numpy.linspace(0, 2 * numpy.pi, 100) 

321 circle_verts = numpy.vstack([numpy.sin(theta), numpy.cos(theta)]).T 

322 concentric_circle = Path.make_compound_path(Path(circle_verts[::-1]), 

323 Path(circle_verts * 0.6)) 

324 for n, (a, b) in enumerate(vertices): 

325 gx, gy = a, b 

326 color = color_vertices.get(n, 'black') if color_vertices else 'black' 

327 ax.plot(gx, gy, transform=projection, 

328 marker=concentric_circle, color=color, markersize=5, 

329 linestyle='') 

330 ax.text(gx, gy, "v%d" % n, size=kwargs.get('size_v', 12), 

331 color=kwargs.get('color_v', "red")) 

332 return ax 

333 

334 

335def connect_streets(st1, st2): 

336 """ 

337 Tells if streets `st1`, `st2` are connected. 

338 

339 @param st1 street 1 

340 @param st2 street 2 

341 @return tuple or tuple (0 or 1, 0 or 1) 

342 

343 Each tuple means: 

344 

345 * 0 or 1 mean first or last extremity or the first street 

346 * 0 or 1 mean first or last extremity or the second street 

347 

348 ``((0, 1),)`` means the first point of the first street is connected 

349 to the second extremity of the second street. 

350 """ 

351 a1, b1 = st1[0], st1[-1] 

352 a2, b2 = st2[0], st2[-1] 

353 connect = [] 

354 if a1 == a2: 

355 connect.append((0, 0)) 

356 if a1 == b2: 

357 connect.append((0, 1)) 

358 if b1 == a2: 

359 connect.append((1, 0)) 

360 if b1 == b2: 

361 connect.append((1, 1)) 

362 return tuple(connect) if connect else None 

363 

364 

365def _complete_subset_streets(edges, shapes): 

366 """ 

367 Extends a set of edges to have less extermities into the graph 

368 composed by the sets of edges. 

369 

370 @param edges list of indices in shapes 

371 @param shapes streets 

372 @return added edges (indices) 

373 """ 

374 sedges = set(edges) 

375 extension = [] 

376 for i, _ in enumerate(shapes): 

377 if i in sedges: 

378 continue 

379 add = [] 

380 for s in edges: 

381 if s != i: 

382 con = connect_streets(shapes[s].points, shapes[i].points) 

383 if con is not None: 

384 add.extend([_[1] for _ in con]) # pylint: disable=E1133 

385 if len(set(add)) == 2: 

386 extension.append(i) 

387 return extension 

388 

389 

390def _enumerate_close(lon, lat, shapes, th=None): 

391 """ 

392 Enumerates close streets from a starting point. 

393 

394 @param lon longitude 

395 @param lat latitude 

396 @param shapes list of streets 

397 @param th threshold or None for all 

398 @return iterator on *tuple(distance, i)* 

399 """ 

400 from shapely.geometry import Point, LineString 

401 p = Point(lon, lat) 

402 for i, shape in enumerate(shapes): 

403 obj = LineString(shape.points) 

404 d = p.distance(obj) 

405 if th is None or d <= th: 

406 yield d, i 

407 

408 

409def seattle_streets_set_level(shapes, records, 

410 pos=(-122.3521425, 47.6219965), 

411 size=120): 

412 """ 

413 Returns a graph of streets. 

414 

415 @param shapes list of streets 

416 @param records description of each street 

417 @param pos center of the graphs 

418 @param size number of elements 

419 @return indices of edges, edges, vertices, distances 

420 

421 The function uses the fields ``SEGMENT_TY`` to filter out train, rail ways. 

422 """ 

423 from shapely.geometry import LineString 

424 lon, lat = pos 

425 closes = list(_enumerate_close(lon, lat, shapes)) 

426 closes.sort() 

427 subset = [index for dist, index in closes[:size]] 

428 newset = list(set(subset + _complete_subset_streets(subset, shapes))) 

429 # we remove trains lines 

430 newset = [n for n in newset if records[n][8] <= 7] 

431 vertices, edges = build_streets_vertices(newset, shapes) 

432 distances = [LineString(shapes[i].points).length for i in newset] 

433 return newset, edges, vertices, distances 

434 

435 

436def seattle_streets_set_small(shapes, records, size=20): 

437 """ 

438 Returns a small graph of streets. 

439 

440 @param shapes list of streets 

441 @param records description of each street 

442 @param size number of elements 

443 @return indices of edges, edges, vertices, distances 

444 """ 

445 return seattle_streets_set_level(shapes, records, pos=(-122.34651954599997, 47.46947199700003), size=size) 

446 

447 

448def seattle_streets_set_level2(shapes, records, size=120): 

449 """ 

450 Returns a small graph of streets. 

451 

452 @param shapes list of streets 

453 @param records description of each street 

454 @param size number of elements 

455 @return indices of edges, edges, vertices, distances 

456 """ 

457 return seattle_streets_set_level(shapes, records, pos=(-122.3521425, 47.6219965), size=size) 

458 

459 

460def seattle_streets_set_level3(shapes, records, size=1200): 

461 """ 

462 Returns a small graph of streets. 

463 

464 @param shapes list of streets 

465 @param records description of each street 

466 @param size number of elements 

467 @return indices of edges, edges, vertices, distances 

468 """ 

469 return seattle_streets_set_level(shapes, records, pos=(-122.400931, 47.648435), size=size)