├── .gitignore ├── LICENCE ├── README.md ├── gpd_lite_toolbox ├── __init__.py ├── core.py ├── cycartogram.c ├── cycartogram.h ├── cycartogram.pyx ├── utils.py └── utils_carto.py ├── misc ├── output_11_1.png ├── output_13_1.png ├── output_16_1.png ├── output_19_1.png ├── output_21_1.png ├── output_22_1.png ├── output_26_1.png ├── output_6_1.png └── output_9_1.png └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 mthh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### gpd_lite_toolbox 2 | Mini toolbox using geopandas/GeoDataFrame and providing some convenience functions. 3 | (read from spatialite, snap points on lines/polygons, match linestring segments, make continous cartogram or non-contiguous cartogram, dissolve or explode a layer, compute weighted mean coordinates, etc.) 4 | 5 | **More a testing repo than a real package repository** 6 | 7 | Installation 8 | ------------ 9 | ```shell 10 | git clone https://github.com/mthh/gpd_lite_toolbox.git 11 | cd gpd_lite_toolbox/ 12 | python setup.py install 13 | 14 | ``` 15 | 16 | Requierments 17 | ------------ 18 | - geopandas 19 | - cython 20 | 21 | 22 | Demo 23 | ---- 24 | 25 | ```python 26 | 27 | In [1]: # Import some basics : 28 | ...: import geopandas as gpd 29 | ...: import numpy as np 30 | ...: import matplotlib.pyplot as plt 31 | ...: %matplotlib inline 32 | ...: 33 | ...: # Import the functions presented here : 34 | ...: import gpd_lite_toolbox as glt 35 | ...: 36 | ...: # and also some slightly modified functions from geopandas.plotting : 37 | ...: from gpd_lite_toolbox.utils_carto import m_plot_dataframe, m_plot_multipolygon 38 | ...: 39 | ...: path_points = 'pts_umz_3600s.shp' 40 | ...: path_grid = 'grid_pop.shp' 41 | ...: path_country = 'eu32_poids.shp' 42 | ...: 43 | ...: gdf_points = gpd.read_file(path_points).to_crs(epsg=3035) 44 | ...: gdf_grid_pop = gpd.read_file(path_grid) 45 | ...: gdf_country = gpd.read_file(path_country) 46 | ...: 47 | ``` 48 | #### Gridify your (points) data : 49 | 50 | ```python 51 | 52 | In [3]: grid = glt.gridify_data(gdf_points, 8000, 53 | ...: 'time', method=np.min) # Choose a method to aggregate point values in each grid cell 54 | ...: 55 | ...: m_plot_dataframe(grid, column='time', contour_poly_width=0.5, edgecolor="grey") 56 | ...: 57 | Out[3]: 58 | ``` 59 | ![png](misc/output_6_1.png) 60 | 61 | 62 | 63 | 64 | #### Compute the weighted mean coordinates of a collection of categorized points 65 | 66 | ```python 67 | In [4]: gdf_grid_pop.geometry = gdf_grid_pop.geometry.centroid 68 | ...: gdf_grid_pop.plot() 69 | ...: 70 | Out[4]: 71 | ``` 72 | ![png](misc/output_9_1.png) 73 | 74 | ```python 75 | In [5]: mean_coord = glt.mean_coordinates(gdf_grid_pop, id_field='ID_N_sup', weight_field='TOT_P') 76 | ...: mean_coord.plot() 77 | ...: 78 | Out[5]: 79 | ``` 80 | ![png](misc/output_11_1.png) 81 | 82 | 83 | #### Make a continous cartogram (Dougenik & al. algorithm) 84 | 85 | ```python 86 | In [6]: cartogram = glt.transform_cartogram(gdf_country, 'SIZE(MB)', 5, inplace=False) 87 | ...: cartogram.plot() 88 | ...: 89 | Out[6]: 90 | ``` 91 | ![png](misc/output_13_1.png) 92 | 93 | 94 | #### Display some random points on the surface of polygons (based, or not, on a numerical field) 95 | 96 | ``` 97 | In [7]: rand_pts = glt.random_pts_on_surface(gdf_country, coef=1, nb_field='SIZE(MB)') 98 | ...: # Coef define a fixed number of points per polygon, which can be multiplied by a value for each polygon. 99 | ...: rand_pts.plot() 100 | ...: 101 | Out[7]: 102 | ``` 103 | ![png](misc/output_16_1.png) 104 | 105 | 106 | #### Compute accessibility isocrones from an OSRM local instance 107 | 108 | ``` 109 | In [11]: %%capture 110 | ...: iso_poly, grid, pt = glt.access_isocrone((21.8, 41.77), 111 | ...: precision=0.024, size=0.45, host='http://localhost:5000') 112 | ...: 113 | 114 | In [12]: iso_poly.plot() 115 | Out[12]: 116 | 117 | ``` 118 | ![png](misc/output_19_1.png) 119 | 120 | #### Dissolve a layer according to a field 121 | 122 | ```python 123 | In [13]: gdf_n23 = gpd.read_file('eu32_n23G.shp') 124 | ...: gdf_n23.plot() 125 | ...: 126 | Out[13]: 127 | ``` 128 | ![png](misc/output_21_1.png) 129 | 130 | ```python 131 | In [14]: gdf_cntr = glt.dissolve(gdf_n23, colname='country') 132 | ...: gdf_cntr.plot() 133 | ...: 134 | Out[14]: 135 | ``` 136 | ![png](misc/output_22_1.png) 137 | 138 | 139 | #### Extract the border line between polygons 140 | **_(and get nearby polygons attributes on the extracted border polylines)_** 141 | 142 | ```python 143 | In [15]: gdf_border = glt.find_borders(gdf_cntr, tol=5, col_name='country') 144 | ``` 145 | ##### And plot them, according to a numerical field (for mapping borders discontinuity for example) : 146 | 147 | ```python 148 | In [16]: # Fisrt preparing some other layer to plot with : 149 | ...: gdf_n23.transfront = gdf_n23.transfront.astype(float) 150 | ...: gdf_country_selec = glt.dissolve(gdf_n23[gdf_n23.transfront == 1], 'country') 151 | ...: 152 | ...: # Generate some fake values to use as border discontinuity values : 153 | ...: from random import randint 154 | ...: vals = [randint(1,6) for i in range(len(gdf_border))] 155 | ...: gdf_border['val'] = vals 156 | ...: fig, ax = plt.subplots(figsize=(10, 10)) 157 | ...: 158 | ...: # Use the modified plotting functions from geopandas : 159 | ...: from shapely.ops import unary_union 160 | ...: m_plot_multipolygon(ax, unary_union(gdf_cntr.geometry.values), 161 | ...: linewidth=0.1, facecolor='lightgrey', 162 | ...: edgecolor='grey', alpha=0.65) 163 | ...: m_plot_dataframe(gdf_country_selec, alpha=0.95, edgecolor='grey', 164 | ...: contour_poly_width=0.2, column='country', 165 | ...: colormap="Pastel2", axes=ax) 166 | ...: 167 | ...: # Make proportional lines : 168 | ...: glt.make_prop_lines(gdf_border, 'val', axes=ax, normalize_values=False) 169 | ...: 170 | ...: # And draw a very basic legend : 171 | ...: ax.patches[0]._label = "Ouf of study area" 172 | ...: ax.lines[164]._label = "Prop. line discontinuity" 173 | ...: ax.legend(handles=[ax.lines[164], ax.patches[0]], loc=1) 174 | ...: fig.suptitle("Map the extracted borders with proportional lines", 175 | ...: size=14) 176 | ...: 177 | Out[16]: 178 | ``` 179 | ![png](misc/output_26_1.png) 180 | 181 | -------------------------------------------------------------------------------- /gpd_lite_toolbox/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | """ 5 | __version__ = '0.0.0-r27' 6 | 7 | from gpd_lite_toolbox.core import ( 8 | get_borders, find_borders, transform_cartogram, dissolve, intersects_byid, 9 | multi_to_single, dumb_multi_to_single, snap_to_nearest, read_spatialite, 10 | match_lines, mean_coordinates, non_contiguous_cartogram, make_grid, 11 | gridify_data, random_pts_on_surface, access_isocrone 12 | ) 13 | -------------------------------------------------------------------------------- /gpd_lite_toolbox/core.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | gpd_lite_toolboox 4 | @author: mthh 5 | """ 6 | import shapely.ops 7 | import numpy as np 8 | import pandas as pd 9 | from sklearn.cluster import KMeans 10 | from shapely.geometry import Point, Polygon, MultiPolygon 11 | from geopandas import GeoDataFrame 12 | from sklearn.metrics.pairwise import pairwise_distances 13 | 14 | from .utils import ( 15 | db_connect, Borderiz, dbl_range, ftouches_byid, l_shared_border, 16 | make_index, nrepeat, mparams, dorling_radius, dorling_radius2, 17 | ) 18 | 19 | __all__ = ('get_borders', 'find_borders', 'transform_cartogram', 'dissolve', 20 | 'intersects_byid', 'multi_to_single', 'dumb_multi_to_single', 21 | 'snap_to_nearest', 'read_spatialite', 'match_lines', 22 | 'mean_coordinates', 'non_contiguous_cartogram', 'make_grid', 23 | 'gridify_data', 'random_pts_on_surface', 'access_isocrone') 24 | 25 | 26 | def match_lines(gdf1, gdf2, method='cheap_hausdorff', limit=None): 27 | """ 28 | Return a pandas.Series (with the length of *gdf1*) with each row containing 29 | the id of the matching feature in *gdf2* (i.e the closest based on the 30 | computation of a "hausdorff-distance-like" between the two lines or 31 | the most similar based on some geometry properties) or nothing if nothing 32 | is found according to the *limit* argument. 33 | 34 | If a *limit* is given, features situed far from this distance 35 | will not be taken into account (in order to avoid retrieving the id of 36 | too far located segments, even if the closest when no one seems to 37 | be matching). 38 | 39 | Parameters 40 | ---------- 41 | gdf1: GeoDataFrame of LineStrings (the reference dataset). 42 | gdf2: GeoDataFrame of LineStrings (the dataset to match). 43 | limit: Integer 44 | The maximum distance, where it is sure that segments 45 | couldn't match. 46 | 47 | Returns 48 | ------- 49 | match_table: pandas.Series containing the matching table (with index 50 | based on *gdf1*) 51 | """ 52 | if 'cheap_hausdorff' in method: 53 | if limit: 54 | return (gdf1.geometry.apply( 55 | lambda x: [fh_dist_lines(x, gdf2.geometry[i]) for i in range(len(gdf2))] 56 | )).apply(lambda x: [nb for nb, i in enumerate(x) if i == min(x) and i < limit]) 57 | else: 58 | return (gdf1.geometry.apply( 59 | lambda x: [fh_dist_lines(x, gdf2.geometry[i]) for i in range(len(gdf2))] 60 | )).apply(lambda x: [nb for nb, i in enumerate(x) if i == min(x)]) 61 | 62 | elif 'cluster' in method: 63 | return match_line_cluster(gdf1, gdf2) 64 | 65 | else: 66 | raise ValueError('Incorrect matching method\nMethod should ' 67 | 'be \'cheap_hausdorff\' or \'cluster\'.') 68 | 69 | 70 | def match_line_cluster(gdf1, gdf2): 71 | """ 72 | Try to match two layers of linestrings with KMeans cluster analysis based 73 | on a triplet of descriptive attributes : 74 | (centroid coords., rounded length, approximate bearing) 75 | 76 | Parameters 77 | ---------- 78 | gdf1: GeoDataFrame 79 | The reference dataset. 80 | gdf2: GeoDataFrame 81 | The collection of LineStrings to match. 82 | 83 | Returns 84 | ------- 85 | matching_table: pandas.Series 86 | A table (index-based on *gdf1*) containing the id of the matching 87 | feature found in *gdf2*. 88 | """ 89 | param1, param2 = list(map(mparams, [gdf1, gdf2])) 90 | k_means = KMeans(init='k-means++', n_clusters=len(gdf1), 91 | n_init=10, max_iter=1000) 92 | k_means.fit(np.array((param1+param2))) 93 | df1 = pd.Series(k_means.labels_[len(gdf1):]) 94 | df2 = pd.Series(k_means.labels_[len(gdf1):]) 95 | # gdf1['fid_layer2'] = \ 96 | # df1.apply(lambda x: df2.where(gdf2['key'] == x).notnull().nonzero()[0][0]) 97 | return pd.DataFrame( 98 | index=list(range(len(gdf1))), 99 | data=df1.apply( 100 | lambda x: df2.where(df2 == x).notnull().nonzero()) 101 | ) 102 | 103 | 104 | def fh_dist_lines(li1, li2): 105 | """ 106 | Compute a cheap distance (based on hausdorff-distance) between 107 | *li1* and *li2*, two LineString. 108 | 109 | Parameters 110 | ---------- 111 | li1: shapely.geometry.LineString 112 | li2: shapely.geometry.LineString 113 | 114 | Returns 115 | ------- 116 | max_dist: Float of the distance between li1 and li2. 117 | 118 | """ 119 | coord_li1 = np.array([i for i in zip(li1.coords.xy[0], li1.coords.xy[1])]) 120 | coord_li2 = np.array([i for i in zip(li2.coords.xy[0], li2.coords.xy[1])]) 121 | if len(coord_li2) > len(coord_li2): 122 | coord_li1, coord_li2 = coord_li2, coord_li1 123 | dist_mat = pairwise_distances( 124 | coord_li1, coord_li2, metric='euclidean', n_jobs=2 125 | ) 126 | chkl = round(len(coord_li1)/len(coord_li2)) 127 | return max( 128 | [dist_mat[i, j] for i, j in zip( 129 | list(range(len(coord_li1))), 130 | list(nrepeat(range(len(coord_li2)), chkl))[:len(coord_li1)])] 131 | ) 132 | 133 | 134 | def get_borders(gdf, tol=1, col_name='id'): 135 | """ 136 | Get the lines corresponding to the border between each 137 | polygon from the dataset, each line containing the *col_name* of the 138 | two polygons around (quicker computation than :py:func:`find_borders`). 139 | Likely a minimalist python port of cartography::getBorders R function from 140 | https://github.com/Groupe-ElementR/cartography/blob/master/R/getBorders.R 141 | 142 | Parameters 143 | ---------- 144 | gdf: :py:class: `geopandas.GeoDataFrame` 145 | Input collection of polygons. 146 | tol: int, default=1 147 | The tolerance (in units of :py:obj:`gdf`). 148 | col_name: str, default='id' 149 | The field name of the polygon to yield. 150 | 151 | Returns 152 | ------- 153 | borders: GeoDataFrame 154 | A GeoDataFrame of linestrings corresponding to the border between each 155 | polygon from the dataset, each line containing the *col_name* of the 156 | two polygon around. 157 | """ 158 | buff = gdf.geometry.buffer(tol) 159 | intersect_table = intersects_byid(buff, buff) 160 | attr, new_geoms = [], [] 161 | for i in range(len(gdf)): 162 | tmp1 = gdf.iloc[i] 163 | buff_geom1 = buff[i] 164 | for j in intersect_table[i]: 165 | if not i == j: 166 | tmp2 = gdf.iloc[j] 167 | buff_geom2 = buff[j] 168 | new_geoms.append( 169 | (buff_geom1.intersection(buff_geom2)).boundary 170 | ) 171 | attr.append(tmp1[col_name] + '-' + tmp2[col_name]) 172 | return GeoDataFrame(attr, geometry=new_geoms, columns=[col_name]) 173 | 174 | 175 | def find_borders(gdf, tol=1, col_name='id'): 176 | """ 177 | Parameters 178 | ---------- 179 | gdf: :py:class::`geopandas.GeoDataFrame` 180 | Input collection of polygons. 181 | tol: int, default=1 182 | The tolerance (in units of :py:obj:`gdf`). 183 | col_name: str, default='id' 184 | The field name of the polygon to yield. 185 | 186 | Returns 187 | ------- 188 | borders: GeoDataFrame 189 | Return lines corresponding to the border between each polygon of the 190 | dataset, each line containing the id of the two polygon around it. 191 | This function is slower/more costly than :py:func:`get_borders`. 192 | """ 193 | if col_name not in gdf.columns: 194 | raise ValueError("Column name error : can't find {}".format(col_name)) 195 | bor = Borderiz(gdf) 196 | return bor.run(tol, col_name) 197 | 198 | 199 | def transform_cartogram(gdf, field_name, iterations=5, inplace=False): 200 | """ 201 | Make a continuous cartogram on a geopandas.GeoDataFrame collection 202 | of Polygon/MultiPolygon (wrapper to call the core functions 203 | written in cython). 204 | Based on the transformation of Dougenik and al.(1985). 205 | 206 | Parameters 207 | ---------- 208 | gdf: geopandas.GeoDataFrame 209 | The GeoDataFrame containing the geometry and a field to use for the 210 | transformation. 211 | field_name: String 212 | The label of the field containing the value to use. 213 | iterations: Integer, default 5 214 | The number of iteration to make. 215 | inplace, Boolean, default False 216 | Append in place if True. Otherwhise return a new :py:obj:GeoDataFrame 217 | with transformed geometry. 218 | 219 | Returns 220 | ------- 221 | GeoDataFrame: A new GeoDataFrame (or None if inplace=True) 222 | 223 | References 224 | ---------- 225 | ``Dougenik, J. A, N. R. Chrisman, and D. R. Niemeyer. 1985. 226 | "An algorithm to construct continuous cartograms." 227 | Professional Geographer 37:75-81`` 228 | """ 229 | from gpd_lite_toolbox.cycartogram import make_cartogram 230 | return make_cartogram(gdf, field_name, iterations, inplace) 231 | 232 | 233 | def intersects_byid(geoms1, geoms2): 234 | """ 235 | Return a table with a row for each features of *geoms1*, containing the id 236 | of each *geoms2* intersecting features (almost like an intersecting matrix). 237 | 238 | Parameters 239 | ---------- 240 | geoms1: GeoSeries or GeoDataFrame 241 | Collection on which the intersecting table will be based. 242 | geoms2: GeoSeries or GeoDataFrame 243 | Collection to test on intersects. 244 | 245 | Returns 246 | ------- 247 | intersect_table: pandas.Series 248 | A Series with the same index id as geoms1, each row containg the ids of 249 | the features of geoms2 intersecting it. 250 | """ 251 | return geoms1.geometry.apply( 252 | lambda x: [i for i in range(len(geoms2.geometry)) 253 | if x.intersects(geoms2.geometry[i])] 254 | ) 255 | 256 | 257 | def dissolve(gdf, colname, inplace=False): 258 | """ 259 | Parameters 260 | ---------- 261 | gdf: GeoDataFrame 262 | The geodataframe to dissolve 263 | colname: String 264 | The label of the column containg the common values to use to dissolve 265 | the collection. 266 | 267 | Returns 268 | ------- 269 | Return a new :py:obj:`geodataframe` with 270 | dissolved features around the selected columns. 271 | """ 272 | if not inplace: 273 | gdf = gdf.copy() 274 | df2 = gdf.groupby(colname) 275 | gdf.set_index(colname, inplace=True) 276 | gdf['geometry'] = df2.geometry.apply(shapely.ops.unary_union) 277 | gdf.reset_index(inplace=True) 278 | gdf.drop_duplicates(colname, inplace=True) 279 | gdf.set_index(pd.Int64Index([i for i in range(len(gdf))]), 280 | inplace=True) 281 | if not inplace: 282 | return gdf 283 | 284 | 285 | def multi_to_single(gdf): 286 | """ 287 | Return a new geodataframe with exploded geometries (where each feature 288 | has a single-part geometry). 289 | 290 | Parameters 291 | ---------- 292 | gdf: GeoDataFrame 293 | The input GeoDataFrame to explode to single part geometries. 294 | 295 | Returns 296 | ------- 297 | gdf: GeoDataFrame 298 | The exploded result. 299 | 300 | See-also 301 | -------- 302 | The method GeoDataFrame.explode() in recent versions of **geopandas**. 303 | """ 304 | values = gdf[[i for i in gdf.columns if i != 'geometry']] 305 | geom = gdf.geometry 306 | geoms, attrs = [], [] 307 | for i in range(len(gdf)): 308 | try: 309 | for single_geom in geom.iloc[i]: 310 | geoms.append(single_geom) 311 | attrs.append(values.iloc[i]) 312 | except: 313 | geoms.append(geom.iloc[i]) 314 | attrs.append(values.iloc[i]) 315 | return GeoDataFrame(attrs, index=[i for i in range(len(geoms))], 316 | geometry=geoms, 317 | columns=[i for i in gdf.columns if i != 'geometry']) 318 | 319 | 320 | def snap_to_nearest(pts_ref, target_layer, inplace=False, 321 | searchframe=50, max_searchframe=500): 322 | """ 323 | Snap each point from :py:obj:`pts_ref` on the nearest 324 | line-segment/polygon-vertex of :py:obj:`target_layer` according to a 325 | *searchframe* defined in units of both two input layers. 326 | Append inplace or return a new object. 327 | (A larger search frame can be set in *max_searchframe* : the search frame 328 | will be progressivly increased from *searchframe* to *max_searchframe* in 329 | order to snap the maximum of points without using a large orginal search 330 | frame) 331 | 332 | Parameters 333 | ---------- 334 | pts_ref: GeoDataFrame 335 | The collection of points to snap on *target_layer*. 336 | target_layer: GeoDataFrame 337 | The collection of LineString or Polygon on which *pts_ref* will be 338 | snapped, according to the *max_searchframe*. 339 | inplace: Boolean, default=False 340 | Append inplace or return a new GeoDataFrame containing moved points. 341 | searchframe: Integer or float, default=50 342 | The original searchframe (in unit of the two inputs GeoDataFrame), 343 | which will be raised to *max_searchframe* if there is no objects to 344 | snap on. 345 | max_searchframe: Integer or float, default=500 346 | The maximum searchframe around each features of *pts_ref* to search in. 347 | 348 | Returns 349 | ------- 350 | snapped_pts: GeoDataFrame 351 | The snapped collection of points (or None if inplace=True, where 352 | points are moved in the original geodataframe). 353 | """ 354 | new_geoms = pts_ref.geometry.values.copy() 355 | target_geoms = target_layer.geometry.values 356 | start_buff = searchframe 357 | index = make_index([i.bounds for i in target_geoms]) 358 | 359 | for id_pts_ref in range(len(new_geoms)): 360 | while True: 361 | try: 362 | tmp = { 363 | (new_geoms[id_pts_ref].distance(target_geoms[fid])): fid 364 | for fid in list(index.intersection( 365 | new_geoms[id_pts_ref].buffer(searchframe).bounds, 366 | objects='raw')) 367 | } 368 | road_ref = tmp[min(tmp.keys())] 369 | break 370 | except ValueError as err: 371 | searchframe += (max_searchframe-start_buff)/3 372 | if searchframe > max_searchframe: 373 | break 374 | try: 375 | res = {new_geoms[id_pts_ref].distance(Point(x, y)): Point(x, y) 376 | for x, y in zip(*target_geoms[road_ref].coords.xy)} 377 | new_geoms[id_pts_ref] = res[min(res.keys())] 378 | except NameError as err: 379 | print(err, 'No value for {}'.format(id_pts_ref)) 380 | 381 | if inplace: 382 | pts_ref.set_geometry(new_geoms, drop=True, inplace=True) 383 | else: 384 | result = pts_ref.copy() 385 | result.set_geometry(new_geoms, drop=True, inplace=True) 386 | return result 387 | 388 | 389 | def dumb_multi_to_single(gdf): 390 | """ 391 | A "dumb" (but sometimes usefull) multi-to-single function, returning a 392 | GeoDataFrame with the first single geometry of each multi-part geometry 393 | (and also return single geometry features untouched), so the returned 394 | GeoDataFrame will have the same number of features. 395 | 396 | Parameters 397 | ---------- 398 | gdf: GeoDataFrame 399 | The input collection of features. 400 | 401 | Returns 402 | ------- 403 | gdf: GeoDataFrame 404 | The exploded result. 405 | """ 406 | values = gdf[[i for i in gdf.columns if i != 'geometry']] 407 | geom = gdf.geometry 408 | geoms, attrs = [], [] 409 | for i in range(len(gdf)): 410 | try: 411 | for single_geom in geom.iloc[i]: 412 | geoms.append(single_geom) 413 | attrs.append(values.iloc[i]) 414 | break 415 | except: 416 | geoms.append(geom.iloc[i]) 417 | attrs.append(values.iloc[i]) 418 | return GeoDataFrame(attrs, index=[i for i in range(len(geoms))], 419 | geometry=geoms, 420 | columns=[i for i in gdf.columns if i != 'geometry']) 421 | 422 | 423 | def read_spatialite(sql, conn, geom_col='geometry', crs=None, 424 | index_col=None, coerce_float=True, params=None, 425 | db_path=None): 426 | """ 427 | Wrap :py:func:`geopandas.read_postgis()` and allow to read from spatialite. 428 | 429 | Returns 430 | ------- 431 | gdf: GeoDataframe 432 | 433 | Example 434 | ------- 435 | >>> # With a connection object (conn) already instancied : 436 | >>> gdf = read_spatialite("SELECT PK_UID, pop_t, gdp FROM countries", conn, 437 | geom_col="GEOM") 438 | >>> # Without being already connected to the database : 439 | >>> gdf = read_spatialite("SELECT PK_UID, pop_t, gdp FROM countries", None, 440 | geom_col="GEOM", 441 | db_path='/home/mthh/tmp/db.sqlite') 442 | """ 443 | from geopandas import read_postgis 444 | if '*' in sql: 445 | raise ValueError('Column names have to be specified') 446 | 447 | if not conn and db_path: 448 | conn = db_connect(db_path) 449 | elif not conn: 450 | raise ValueError( 451 | 'A connection object or a path to the DB have to be provided') 452 | 453 | if sql.lower().find('select') == 0 and sql.find(' ') == 6: 454 | sql = sql[:7] \ 455 | + "HEX(ST_AsBinary({0})) as {0}, ".format(geom_col) + sql[7:] 456 | else: 457 | raise ValueError( 458 | 'Unable to understand the query') 459 | 460 | return read_postgis( 461 | sql, conn, geom_col=geom_col, crs=crs, index_col=index_col, 462 | coerce_float=coerce_float, params=params 463 | ) 464 | 465 | 466 | def mean_coordinates(gdf, id_field=None, weight_field=None): 467 | """ 468 | Compute the (weighted) mean coordinate(s) of a set of points. If provided 469 | the point(s) will be located according to *weight_field* (numérical field). 470 | If an *id_field* is given, a mean coordinate pt will be calculated for each 471 | subset of points differencied by this *id_field*. 472 | 473 | Parameters 474 | ---------- 475 | gdf: GeoDataFrame 476 | The input collection of Points. 477 | id_field: String, optional 478 | The label of the field containing a value to weight each point. 479 | weight_field: String, optional 480 | The label of the field which differenciate features of *gdf* in subsets 481 | in order to get multiples mean points returned. 482 | 483 | Returns 484 | ------- 485 | mean_points: GeoDataFrame 486 | A new GeoDataFrame with the location of the computed point(s). 487 | """ 488 | assert 'Multi' not in gdf.geometry.geom_type, \ 489 | "Multipart geometries aren't allowed" 490 | fields = ['geometry'] 491 | if id_field: 492 | assert id_field in gdf.columns 493 | fields.append(id_field) 494 | if weight_field: 495 | assert weight_field in gdf.columns 496 | fields.append(weight_field) 497 | else: 498 | weight_field = 'count' 499 | tmp = gdf[fields].copy() 500 | tmp['x'] = tmp.geometry.apply(lambda x: x.coords.xy[0][0]) 501 | tmp['y'] = tmp.geometry.apply(lambda x: x.coords.xy[1][0]) 502 | tmp.x = tmp.x * tmp[weight_field] 503 | tmp.y = tmp.y * tmp[weight_field] 504 | tmp['count'] = 1 505 | if id_field: 506 | tmp = tmp.groupby(id_field).sum() 507 | else: 508 | tmp = tmp.sum() 509 | tmp = tmp.T 510 | tmp.x = tmp.x / tmp[weight_field] 511 | tmp.y = tmp.y / tmp[weight_field] 512 | tmp['geometry'] = [Point(i[0], i[1]) for i in tmp[['x', 'y']].values] 513 | return GeoDataFrame(tmp[weight_field], geometry=tmp['geometry'], 514 | index=tmp.index).reset_index() 515 | 516 | 517 | def random_pts_on_surface(gdf, coef=1, nb_field=None): 518 | """ 519 | For each polygon, return a point (or a set of points, according to 520 | *nb_field* and *coef*), lying on the polygon surface. 521 | 522 | Parameters 523 | ---------- 524 | gdf: GeoDataFrame 525 | A collection of polygons on which generate points. 526 | coef: Integer, default 1 527 | The multiplicant, which applies to each feature of *gdf*, 528 | If used, the values contained on *nb_field* will also be multiplicated. 529 | nb_field: String, optional 530 | The name of the field to read, containing an integer 531 | which will be used as the number of points to create. 532 | 533 | Returns 534 | ------- 535 | rand_points: GeoDataFrame 536 | A collection of points, located on *gdf*, accordingly to *coef* and 537 | values contained in *nb_field* if used. 538 | 539 | """ 540 | nb_ft = len(gdf) 541 | if nb_field: 542 | nb_pts = gdf[nb_field].values * coef 543 | else: 544 | nb_pts = np.array([coef for i in range(nb_ft)]) 545 | res = [] 546 | for i in range(nb_ft): 547 | pts_to_create = round(nb_pts[i]) 548 | (minx, miny, maxx, maxy) = gdf.geometry[i].bounds 549 | while True: 550 | xpt = \ 551 | (maxx-minx) * np.random.random_sample((pts_to_create,)) + minx 552 | ypt = \ 553 | (maxy-miny) * np.random.random_sample((pts_to_create,)) + miny 554 | points = np.array([xpt, ypt]).T 555 | for pt_ in points: 556 | pt_geom = Point((pt_[0], pt_[1])) 557 | if gdf.geometry[i].contains(pt_geom): 558 | res.append(pt_geom) 559 | pts_to_create -= 1 560 | if pts_to_create == 0: 561 | break 562 | return GeoDataFrame(geometry=res, crs=gdf.crs) 563 | 564 | 565 | def make_grid(gdf, height, cut=True): 566 | """ 567 | Return a grid, based on the shape of *gdf* and on a *height* value (in 568 | units of *gdf*). If cut=False, the grid will not be intersected with *gdf* 569 | (i.e it makes a grid on the bounding-box of *gdf*). 570 | 571 | Parameters 572 | ---------- 573 | gdf: GeoDataFrame 574 | The collection of polygons to be covered by the grid. 575 | height: Integer 576 | The dimension (will be used as height and width) of the ceils to create, 577 | in units of *gdf*. 578 | cut: Boolean, default True 579 | Cut the grid to fit the shape of *gdf* (ceil partially covering it will 580 | be truncated). If False, the returned grid will fit the bounding box 581 | of *gdf*. 582 | 583 | Returns 584 | ------- 585 | grid: GeoDataFrame 586 | A collection of polygons. 587 | """ 588 | from math import ceil 589 | from shapely.ops import unary_union 590 | xmin, ymin = [i.min() for i in gdf.bounds.T.values[:2]] 591 | xmax, ymax = [i.max() for i in gdf.bounds.T.values[2:]] 592 | rows = int(ceil((ymax-ymin) / height)) 593 | cols = int(ceil((xmax-xmin) / height)) 594 | 595 | x_left_origin = xmin 596 | x_right_origin = xmin + height 597 | y_top_origin = ymax 598 | y_bottom_origin = ymax - height 599 | 600 | res_geoms = [] 601 | for countcols in range(cols): 602 | y_top = y_top_origin 603 | y_bottom = y_bottom_origin 604 | for countrows in range(rows): 605 | res_geoms.append(( 606 | (x_left_origin, y_top), (x_right_origin, y_top), 607 | (x_right_origin, y_bottom), (x_left_origin, y_bottom) 608 | )) 609 | y_top = y_top - height 610 | y_bottom = y_bottom - height 611 | x_left_origin = x_left_origin + height 612 | x_right_origin = x_right_origin + height 613 | if cut: 614 | if all(gdf.eval( 615 | "geometry.type =='Polygon' or geometry.type =='MultiPolygon'")): 616 | res = GeoDataFrame( 617 | geometry=pd.Series(res_geoms).apply(lambda x: Polygon(x)), 618 | crs=gdf.crs 619 | ).intersection(unary_union(gdf.geometry)) 620 | else: 621 | res = GeoDataFrame( 622 | geometry=pd.Series(res_geoms).apply(lambda x: Polygon(x)), 623 | crs=gdf.crs 624 | ).intersection(unary_union(gdf.geometry).convex_hull) 625 | res = res[res.geometry.type == 'Polygon'] 626 | res.index = [i for i in range(len(res))] 627 | return GeoDataFrame(geometry=res) 628 | 629 | else: 630 | return GeoDataFrame( 631 | index=[i for i in range(len(res_geoms))], 632 | geometry=pd.Series(res_geoms).apply(lambda x: Polygon(x)), 633 | crs=gdf.crs 634 | ) 635 | 636 | 637 | def gridify_data(gdf, height, col_name, cut=True, method=np.mean): 638 | """ 639 | Gridify a collection of point observations. 640 | 641 | Parameters 642 | ---------- 643 | gdf: GeoDataFrame 644 | The collection of polygons to be covered by the grid. 645 | height: Integer 646 | The dimension (will be used as height and width) of the ceils to create, 647 | in units of *gdf*. 648 | col_name: String 649 | The name of the column containing the value to use for the grid cells. 650 | cut: Boolean, default True 651 | Cut the grid to fit the shape of *gdf* (ceil partially covering it will 652 | be truncated). If False, the returned grid fit the bounding box of gdf. 653 | method: Numpy/Pandas function 654 | The method to aggregate values of points for each cell. 655 | (like numpy.max, numpy.mean, numpy.mean, numpy.std or numpy.sum) 656 | 657 | Returns 658 | ------- 659 | grid: GeoDataFrame 660 | A collection of polygons. 661 | 662 | Example 663 | ------- 664 | >>> all(gdf.geometry.type == 'Point') # The function only act on Points 665 | True 666 | >>> gdf.time.dtype # And the value to aggreagate have to be numerical 667 | dtype('int64') 668 | >>> grid_data = gridify_data(gdf, 7500, 'time', method=np.min) 669 | >>> plot_dataframe(grid_data, column='time') 670 | 671 | ... 672 | """ 673 | if not all(gdf.geometry.type == 'Point'): 674 | raise ValueError("Can only gridify scattered data (Point)") 675 | if not gdf[col_name].dtype.kind in {'i', 'f'}: 676 | raise ValueError("Target column have to be a numerical field") 677 | 678 | grid = make_grid(gdf, height, cut) 679 | grid[col_name] = -1 680 | index = make_index([i.bounds for i in gdf.geometry]) 681 | for id_cell in range(len(grid)): 682 | ids_pts = list(index.intersection( 683 | grid.geometry[id_cell].bounds, objects='raw')) 684 | if ids_pts: 685 | res = method(gdf.iloc[ids_pts][col_name]) 686 | grid.loc[id_cell, col_name] = res 687 | return grid 688 | 689 | 690 | def non_contiguous_cartogram(gdf, value, nrescales, 691 | n_iter=2, tol=100, buff_kws={}): 692 | """ 693 | Make a non-contiguous cartogram on a geopandas.GeoDataFrame collection 694 | of Polygon/MultiPolygon. 695 | 696 | Parameters 697 | ---------- 698 | gdf: :py:obj:`geopandas.GeoDataFrame` 699 | The GeoDataFrame containing the geometry and a field to use 700 | for the transformation. 701 | field_name: String 702 | The name of the column of *gdf* containing the value to use. 703 | n_rescales: Integer 704 | The number of iterations to make, each one scaling down the radius of 705 | the circle in order to avoid overlapping. 706 | n_iter: Integer 707 | The number of iterations to make, within each scale ratio. 708 | tol: Integer 709 | The tolerance to consider for overlapping (an overlapping within the 710 | tolerance will not be considered as an overlapping). 711 | buf_kws: Dict 712 | A dict of parameter for the shapely function :py:func: buffer to choose 713 | the shape of obtained geometry (like square, octogone, circle). 714 | 715 | Returns 716 | ------- 717 | cartogram: GeoDataFrame 718 | A new geodataframe with transformed geometries, ready to map. 719 | """ 720 | ratios = [1 - i/nrescales for i in range(nrescales)] 721 | gdf2 = gdf.copy() 722 | gdf2.geometry = gdf2.geometry.centroid 723 | for ratio in ratios: 724 | radius = dorling_radius(gdf, value, ratio) 725 | for n_time in range(n_iter): 726 | overlap_count = 0 727 | for idxa, fta, idxb, ftb in dbl_range(gdf2): 728 | dx = fta.geometry.coords.xy[0][0] \ 729 | - ftb.geometry.coords.xy[0][0] 730 | dy = fta.geometry.coords.xy[1][0] \ 731 | - ftb.geometry.coords.xy[1][0] 732 | l = np.sqrt(dx**2+dy**2) 733 | d = radius[idxa] + radius[idxb] 734 | prop = (l-d)/l 735 | dx = dx * prop 736 | dy = dy * prop 737 | if l < d and abs(l - d) > tol: 738 | overlap_count += 1 739 | gdf2.loc[idxa, 'geometry'] = Point( 740 | (fta.geometry.coords.xy[0][0] - dx, 741 | fta.geometry.coords.xy[1][0] - dy) 742 | ) 743 | if overlap_count == 0: 744 | break 745 | geoms = [gdf2.geometry[i].buffer(radius[i], **buff_kws) 746 | for i in range(len(gdf2))] 747 | gdf2['geometry'] = geoms 748 | return gdf2 749 | 750 | 751 | def countour_poly(gdf, field_name, levels='auto'): 752 | """ 753 | Parameters 754 | ---------- 755 | gdf: :py:obj:`geopandas.GeoDataFrame` 756 | The GeoDataFrame containing points and associated values. 757 | field_name: String 758 | The name of the column of *gdf* containing the value to use. 759 | levels: int, or list of int, default 'auto' 760 | The number of levels to use for contour polygons if levels is an 761 | integer (exemple: levels=8). 762 | Or 763 | Limits of the class to use in a list/tuple (like [0, 200, 400, 800]) 764 | Defaults is set to 15 class. 765 | 766 | Return 767 | ------ 768 | collection_polygons: matplotlib.contour.QuadContourSet 769 | The shape of the computed polygons. 770 | levels: list of integers 771 | """ 772 | import matplotlib.pyplot as plt 773 | from matplotlib.mlab import griddata 774 | if plt.isinteractive(): 775 | plt.ioff() 776 | switched = True 777 | else: 778 | switched = False 779 | 780 | # Dont take point without value : 781 | gdf = gdf.iloc[gdf[field_name].nonzero()[0]][:] 782 | # Try to avoid unvalid geom : 783 | if len(gdf.geometry.valid()) != len(gdf): 784 | # Invalid geoms have been encountered : 785 | valid_geoms = gdf.geometry.valid() 786 | valid_geoms = valid_geoms.reset_index() 787 | valid_geoms['idx'] = valid_geoms['index'] 788 | del valid_geoms['index'] 789 | valid_geoms[field_name] = \ 790 | valid_geoms.idx.apply(lambda x: gdf[field_name][x]) 791 | else: 792 | valid_geoms = gdf[['geometry', field_name]][:] 793 | 794 | # Always in order to avoid invalid value which will cause the fail 795 | # of the griddata function : 796 | try: # Normal way (fails if a non valid geom is encountered) 797 | x = np.array([geom.coords.xy[0][0] for geom in valid_geoms.geometry]) 798 | y = np.array([geom.coords.xy[1][0] for geom in valid_geoms.geometry]) 799 | z = valid_geoms[field_name].values 800 | except: # Taking the long way to load the value... : 801 | x = np.array([]) 802 | y = np.array([]) 803 | z = np.array([], dtype=float) 804 | for idx, geom, val in gdf[['geometry', field_name]].itertuples(): 805 | try: 806 | x = np.append(x, geom.coords.xy[0][0]) 807 | y = np.append(y, geom.coords.xy[1][0]) 808 | z = np.append(z, val) 809 | except Exception as err: 810 | print(err) 811 | 812 | # # compute min and max and values : 813 | minx = np.nanmin(x) 814 | miny = np.nanmin(y) 815 | maxx = np.nanmax(x) 816 | maxy = np.nanmax(y) 817 | 818 | # Assuming we want a square grid for the interpolation 819 | xi = np.linspace(minx, maxx, 200) 820 | yi = np.linspace(miny, maxy, 200) 821 | zi = griddata(x, y, z, xi, yi, interp='linear') 822 | if isinstance(levels, (str, bytes)) and 'auto' in levels: 823 | jmp = int(round((np.nanmax(z) - np.nanmin(z)) / 15)) 824 | levels = [nb for nb in range(0, int(round(np.nanmax(z))+1)+jmp, jmp)] 825 | 826 | collec_poly = plt.contourf( 827 | xi, yi, zi, levels, cmap=plt.cm.rainbow, 828 | vmax=abs(zi).max(), vmin=-abs(zi).max(), alpha=0.35 829 | ) 830 | 831 | if isinstance(levels, int): 832 | jmp = int(round((np.nanmax(z) - np.nanmin(z)) / levels)) 833 | levels = [nb for nb in range(0, int(round(np.nanmax(z))+1)+jmp, jmp)] 834 | if switched: 835 | plt.ion() 836 | return collec_poly, levels 837 | 838 | 839 | def isopoly_to_gdf(collec_poly, field_name=None, levels=None): 840 | polygons, data = [], [] 841 | 842 | for i, polygon in enumerate(collec_poly.collections): 843 | mpoly = [] 844 | for path in polygon.get_paths(): 845 | path.should_simplify = False 846 | poly = path.to_polygons() 847 | exterior, holes = [], [] 848 | if len(poly) > 0 and len(poly[0]) > 3: 849 | exterior = poly[0] 850 | if len(poly) > 1: # There's some holes 851 | holes = [h for h in poly[1:] if len(h) > 3] 852 | mpoly.append(Polygon(exterior, holes)) 853 | if len(mpoly) > 1: 854 | mpoly = MultiPolygon(mpoly) 855 | polygons.append(mpoly) 856 | if levels: 857 | data.append(levels[i]) 858 | elif len(poly) == 1: 859 | polygons.append(mpoly[0]) 860 | if levels: 861 | data.append(levels[i]) 862 | 863 | if levels and isinstance(levels, (list, tuple)) \ 864 | and len(data) == len(polygons): 865 | if not field_name: 866 | field_name = 'value' 867 | return GeoDataFrame(geometry=polygons, 868 | data=data, columns=[field_name]) 869 | else: 870 | return GeoDataFrame(geometry=polygons) 871 | 872 | 873 | def access_isocrone(point_origine=(21.7351529, 41.7147303), 874 | precision=0.022, size=0.35, 875 | host='http://localhost:5000'): 876 | """ 877 | Parameters 878 | ---------- 879 | point_origine: 2-floats tuple 880 | The coordinates of the center point to use as (x, y). 881 | precision: float 882 | The name of the column of *gdf* containing the value to use. 883 | size: float 884 | Search radius (in degree). 885 | host: string, default 'http://localhost:5000' 886 | OSRM instance URL (no final backslash) 887 | 888 | Return 889 | ------ 890 | gdf_ploy: GeoDataFrame 891 | The shape of the computed accessibility polygons. 892 | grid: GeoDataFrame 893 | The location and time of each used point. 894 | point_origine: 2-floats tuple 895 | The coord (x, y) of the origin point (could be the same as provided 896 | or have been slightly moved to be on a road). 897 | """ 898 | from osrm import light_table, locate 899 | pt = Point(point_origine) 900 | gdf = GeoDataFrame(geometry=[pt.buffer(size)]) 901 | grid = make_grid(gdf, precision, cut=False) 902 | len(grid) 903 | if len(grid) > 4000: 904 | print('Too large query requiered - Reduce precision or size') 905 | return -1 906 | point_origine = locate(point_origine, host=host)['mapped_coordinate'][::-1] 907 | liste_coords = [locate((i.coords.xy[0][0], i.coords.xy[1][0]), host=host) 908 | ['mapped_coordinate'][::-1] 909 | for i in grid.geometry.centroid] 910 | liste_coords.append(point_origine) 911 | matrix = light_table(liste_coords, host=host) 912 | times = matrix[len(matrix)-1].tolist() 913 | del matrix 914 | geoms, values = [], [] 915 | for time, coord in zip(times, liste_coords): 916 | if time != 2147483647 and time != 0: 917 | geoms.append(Point(coord)) 918 | values.append(time/3600) 919 | grid = GeoDataFrame(geometry=geoms, data=values, columns=['time']) 920 | del geoms 921 | del values 922 | collec_poly, levels = countour_poly(grid, 'time', levels=8) 923 | gdf_poly = isopoly_to_gdf(collec_poly, 'time', levels) 924 | return gdf_poly, grid, point_origine 925 | -------------------------------------------------------------------------------- /gpd_lite_toolbox/cycartogram.h: -------------------------------------------------------------------------------- 1 | /* Generated by Cython 0.28.3 */ 2 | 3 | #ifndef __PYX_HAVE__gpd_lite_toolbox__cycartogram 4 | #define __PYX_HAVE__gpd_lite_toolbox__cycartogram 5 | 6 | struct Holder; 7 | typedef struct Holder Holder; 8 | 9 | /* "gpd_lite_toolbox/cycartogram.pyx":75 10 | * 11 | * 12 | * ctypedef public struct Holder: # <<<<<<<<<<<<<< 13 | * unsigned int lFID 14 | * double ptCenter_x 15 | */ 16 | struct Holder { 17 | unsigned int lFID; 18 | double ptCenter_x; 19 | double ptCenter_y; 20 | double dValue; 21 | double dArea; 22 | double dMass; 23 | double dRadius; 24 | }; 25 | 26 | #ifndef __PYX_HAVE_API__gpd_lite_toolbox__cycartogram 27 | 28 | #ifndef __PYX_EXTERN_C 29 | #ifdef __cplusplus 30 | #define __PYX_EXTERN_C extern "C" 31 | #else 32 | #define __PYX_EXTERN_C extern 33 | #endif 34 | #endif 35 | 36 | #ifndef DL_IMPORT 37 | #define DL_IMPORT(_T) _T 38 | #endif 39 | 40 | #endif /* !__PYX_HAVE_API__gpd_lite_toolbox__cycartogram */ 41 | 42 | /* WARNING: the interface of the module init function changed in CPython 3.5. */ 43 | /* It now returns a PyModuleDef instance instead of a PyModule instance. */ 44 | 45 | #if PY_MAJOR_VERSION < 3 46 | PyMODINIT_FUNC initcycartogram(void); 47 | #else 48 | PyMODINIT_FUNC PyInit_cycartogram(void); 49 | #endif 50 | 51 | #endif /* !__PYX_HAVE__gpd_lite_toolbox__cycartogram */ 52 | -------------------------------------------------------------------------------- /gpd_lite_toolbox/cycartogram.pyx: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | #cython: boundscheck = False 3 | #cython: wraparound = False 4 | #cython: cdivision = True 5 | """ 6 | cycartogram extension: 7 | 8 | Easy construction of continuous cartogram on a Polygon/MultiPolygon 9 | GeoDataFrame (modify the geometry in place or create a new GeoDataFrame). 10 | 11 | Code adapted (to fit the geopandas.GeoDataFrame datastructure) from 12 | Carson Farmer's code (https://github.com/carsonfarmer/cartogram which was 13 | part of 'Cartogram' QGis python plugin), itself partially related to 14 | 'pyCartogram.py' from Eric Wolfs. 15 | 16 | Algorithm itself based on : 17 | ``` 18 | Dougenik, J. A, N. R. Chrisman, and D. R. Niemeyer. 1985. 19 | "An algorithm to construct continuous cartograms." 20 | Professional Geographer 37:75-81 21 | ``` 22 | 23 | No warranty concerning the result. 24 | Copyright (C) 2013 Carson Farmer, 2015 mthh 25 | """ 26 | from shapely.geometry import LineString, Polygon, MultiPolygon 27 | from libc.math cimport sqrt 28 | from cpython cimport array 29 | from libc.stdlib cimport malloc, free 30 | 31 | 32 | def make_cartogram(geodf, field_name, iterations=5, inplace=False): 33 | """ 34 | Make a continuous cartogram on a geopandas.GeoDataFrame collection 35 | of Polygon/MultiPolygon (wrapper to call the core functions 36 | written in cython). 37 | Based on the transformation of Dougenik and al.(1985). 38 | 39 | Parameters 40 | ---------- 41 | gdf: geopandas.GeoDataFrame 42 | The GeoDataFrame containing the geometry and a field to use for the 43 | transformation. 44 | field_name: String 45 | The label of the field containing the value to use. 46 | iterations: Integer, default 5 47 | The number of iteration to make. 48 | inplace, Boolean, default False 49 | Append in place if True. Otherwhise return a new :py:obj:GeoDataFrame 50 | with transformed geometry. 51 | 52 | Returns 53 | ------- 54 | GeoDataFrame: A new GeoDataFrame (or None if inplace=True) 55 | 56 | References 57 | ---------- 58 | ``Dougenik, J. A, N. R. Chrisman, and D. R. Niemeyer. 1985. 59 | "An algorithm to construct continuous cartograms." 60 | Professional Geographer 37:75-81`` 61 | """ 62 | assert isinstance(iterations, int) and iterations > 0, \ 63 | "Iteration number have to be a positive integer" 64 | try: 65 | f_idx = geodf.columns.get_loc(field_name) 66 | except KeyError: 67 | raise KeyError('Column name \'{}\' not found'.format(field_name)) 68 | 69 | if inplace: 70 | Cartogram(geodf, f_idx, iterations).make() 71 | else: 72 | return Cartogram(geodf.copy(), f_idx, iterations).make() 73 | 74 | 75 | ctypedef public struct Holder: 76 | unsigned int lFID 77 | double ptCenter_x 78 | double ptCenter_y 79 | double dValue 80 | double dArea 81 | double dMass 82 | double dRadius 83 | 84 | cdef class Cartogram(object): 85 | cdef object geodf, temp_geo_serie 86 | cdef unsigned int iterations, total_features 87 | cdef float dForceReductionFactor 88 | cdef Holder *aLocal 89 | cdef double[:] values 90 | 91 | def __init__(self, object geodf not None, int field_idx, unsigned int iterations): 92 | cdef set geom_type, allowed = {'MultiPolygon', 'Polygon'} 93 | 94 | geom_type = set(list(geodf.geom_type)) 95 | if not geom_type.issubset(allowed): 96 | raise ValueError( 97 | "Geometry type doesn't match 'Polygon'/'MultiPolygon" 98 | ) 99 | self.geodf = geodf 100 | self.temp_geo_serie = geodf.geometry[:] 101 | self.iterations = iterations 102 | self.total_features = len(self.geodf) 103 | self.dForceReductionFactor = 0 104 | self.values = geodf[[field_idx]].values.T[0] 105 | self.aLocal = malloc(self.total_features * sizeof(Holder)) 106 | if not self.aLocal: 107 | raise MemoryError() 108 | 109 | cpdef object make(self): 110 | """Fetch the result and make it available""" 111 | 112 | self.cartogram() 113 | self.geodf.set_geometry(self.temp_geo_serie, inplace=True) 114 | free(self.aLocal) 115 | return self.geodf 116 | 117 | cdef object cartogram(self): 118 | """ 119 | Compute for transformation 120 | (recursively, according to the specified iteration number) 121 | """ 122 | cdef unsigned int ite=0, nbi=0 123 | 124 | for ite in range(self.iterations): 125 | self.getinfo() 126 | for nbi in range(self.total_features): 127 | self.temp_geo_serie[nbi] = self.transform_geom( 128 | self.temp_geo_serie[nbi] 129 | ) 130 | 131 | cdef void getinfo(self): 132 | """ 133 | Gets the information required for calcualting size reduction factor 134 | """ 135 | cdef unsigned int fid=0, i, featCount = self.total_features 136 | cdef float dPolygonValue, dPolygonArea, dFraction, dDesired, dRadius 137 | cdef float dSizeError=0.0, dMean, pi=3.14159265 138 | cdef float area_total, value_total, tmp, dSizeErrorTotal = 0.0 139 | 140 | area_total = sum(self.temp_geo_serie.area) 141 | value_total = sum(self.values) 142 | for fid in range(featCount): 143 | geom = self.temp_geo_serie.iloc[fid] 144 | self.aLocal[fid].dArea = geom.area # save area of this feature 145 | self.aLocal[fid].lFID = fid # save id for this feature 146 | # save weighted 'area' value for this feature : 147 | self.aLocal[fid].dValue = self.values[fid] 148 | # save centroid coord for the feature : 149 | (self.aLocal[fid].ptCenter_x, self.aLocal[fid].ptCenter_y) = \ 150 | (geom.centroid.coords.ctypes[0], geom.centroid.coords.ctypes[1]) 151 | 152 | dFraction = area_total / value_total 153 | with nogil: 154 | for i in range(featCount): 155 | dPolygonValue = self.aLocal[i].dValue 156 | dPolygonArea = self.aLocal[i].dArea 157 | if dPolygonArea < 0: # area should never be less than zero 158 | dPolygonArea = 0 159 | # this is our 'desired' area... 160 | dDesired = dPolygonValue * dFraction 161 | # calculate radius, a zero area is zero radius 162 | dRadius = sqrt(dPolygonArea / pi) 163 | self.aLocal[i].dRadius = dRadius 164 | tmp = dDesired / pi 165 | if tmp > 0: 166 | # calculate area mass, don't think this should be negative 167 | self.aLocal[i].dMass = sqrt(dDesired / pi) - dRadius 168 | else: 169 | self.aLocal[i].dMass = 0 170 | # both radius and mass are being added to the feature list for 171 | # later on... 172 | # calculate size error... 173 | dSizeError = \ 174 | max(dPolygonArea, dDesired) / min(dPolygonArea, dDesired) 175 | # this is the total size error for all polygons 176 | dSizeErrorTotal += dSizeError 177 | # average error 178 | dMean = dSizeErrorTotal / featCount 179 | # need to read up more on why this is done 180 | self.dForceReductionFactor = 1 / (dMean + 1) 181 | 182 | cdef object transform_geom(self, object geom, 183 | Polygon=Polygon, MultiPolygon=MultiPolygon, 184 | LineString=LineString): 185 | """ 186 | Core function computing the transformation on the Polygon (or on each 187 | polygon, if multipolygon layer), using previously retieved informations 188 | about its geometry and about other feature geometries. 189 | """ 190 | cdef unsigned int i, k, it_geom=0, it_bound=0, l_coord_bound=0 191 | cdef double x, y, x0, y0, cx, cy, distance, Fij, xF 192 | cdef Py_ssize_t nb_geom, nb_bound 193 | cdef Holder *lf 194 | cdef object boundarys 195 | cdef double[:] xs, ys 196 | cdef list tmp_bound, new_geom = [] 197 | 198 | if isinstance(geom, Polygon): 199 | geom = [geom] 200 | nb_geom = 1 201 | else: 202 | nb_geom = len(geom) 203 | for it_geom in range(nb_geom): 204 | boundarys = geom[it_geom].boundary 205 | tmp_bound = [] 206 | try: 207 | nb_bound = len(boundarys) 208 | except: 209 | boundarys = [boundarys] 210 | nb_bound = 1 211 | for it_bound in range(nb_bound): 212 | line_coord = [] 213 | xs, ys = boundarys[it_bound].coords.xy 214 | l_coord_bound = len(xs) 215 | with nogil: 216 | for k in range(l_coord_bound): 217 | x = xs[k] 218 | y = ys[k] 219 | x0, y0 = x, y 220 | # Compute the influence of all shapes on this point 221 | for i in range(self.total_features): 222 | lf = &self.aLocal[i] 223 | cx = lf.ptCenter_x 224 | cy = lf.ptCenter_y 225 | # Pythagorean distance 226 | distance = sqrt((x0 - cx) ** 2 + (y0 - cy) ** 2) 227 | if distance > lf.dRadius: 228 | # Calculate the force on verteces far away 229 | # from the centroid of this feature 230 | Fij = lf.dMass * lf.dRadius / distance 231 | else: 232 | # Calculate the force on verteces far away 233 | # from the centroid of this feature 234 | xF = distance / lf.dRadius 235 | Fij = lf.dMass * (xF ** 2) * (4 - (3 * xF)) 236 | Fij = Fij * self.dForceReductionFactor / distance 237 | x = (x0 - cx) * Fij + x 238 | y = (y0 - cy) * Fij + y 239 | with gil: 240 | line_coord.append((x, y)) 241 | tmp_bound.append(line_coord) 242 | 243 | if nb_bound == 1: 244 | new_geom.append(Polygon(tmp_bound[0])) 245 | else: 246 | for it_bound in range(nb_bound): 247 | new_geom.append(Polygon(tmp_bound[it_bound])) 248 | 249 | if nb_geom > 1: 250 | return MultiPolygon(new_geom) 251 | elif nb_geom == 1: 252 | return new_geom[0] 253 | 254 | -------------------------------------------------------------------------------- /gpd_lite_toolbox/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | gpd_lite_toolboox utils 4 | @author: mthh 5 | """ 6 | 7 | import time 8 | import math 9 | import rtree 10 | import numpy as np 11 | import pandas as pd 12 | import geopandas as gpd 13 | from sklearn.preprocessing import normalize 14 | from sklearn.metrics.pairwise import pairwise_distances 15 | from shapely.errors import TopologicalError 16 | 17 | def nrepeat(iterable, n): 18 | return iter([i for i in iterable for j in range(n)]) 19 | 20 | 21 | def dbl_range(df_item): 22 | for i in df_item.iterrows(): 23 | for j in df_item.iterrows(): 24 | if i[0] != j[0]: 25 | yield i[0], i[1], j[0], j[1] 26 | 27 | 28 | def db_creation(path): 29 | import sqlite3 as db 30 | conn = db.connect(path) 31 | conn.enable_load_extension(True) 32 | conn.load_extension('/usr/local/lib/mod_spatialite.so.7.1.0') 33 | conn.executescript('PRAGMA synchronous=off; PRAGMA cache_size=1536000;') 34 | print('Initializing Spatial Metadata...') 35 | conn.execute('SELECT InitSpatialMetadata();') 36 | conn.commit() 37 | return conn 38 | 39 | 40 | def db_connect(path): 41 | import sqlite3 as db 42 | conn = db.connect(path) 43 | conn.enable_load_extension(True) 44 | conn.load_extension('/usr/local/lib/mod_spatialite.so.7.1.0') 45 | conn.executescript('PRAGMA synchronous=off; PRAGMA cache_size=1536000;') 46 | conn.commit() 47 | return conn 48 | 49 | 50 | def idx_generator_func(bounds): 51 | for i, bound in enumerate(bounds): 52 | yield (i, bound, i) 53 | 54 | 55 | def make_index(bounds): 56 | return rtree.index.Index([z for z in idx_generator_func(bounds)], 57 | Interleaved=True) 58 | 59 | 60 | def mparams(gdf1): 61 | params = [] 62 | for i in range(len(gdf1)): 63 | ftg = gdf1.geometry[i] 64 | len_v = len(ftg.coords.xy[0]) - 1 65 | first_pt_x, first_pt_y = \ 66 | ftg.coords.xy[0][0], ftg.coords.xy[1][0] 67 | last_pt_x, last_pt_y = \ 68 | ftg.coords.xy[0][len_v], ftg.coords.xy[1][len_v] 69 | orientation = 180 + math.atan2( 70 | (first_pt_x - last_pt_x), (first_pt_y - last_pt_y) 71 | ) * (180 / math.pi) 72 | params.append( 73 | (ftg.centroid.x, ftg.centroid.y, ftg.length, orientation)) 74 | return params 75 | 76 | 77 | def fh2_dist_lines2(li1, li2): 78 | c1 = np.array([i for i in zip(li1.coords.xy[0], li1.coords.xy[1])]) 79 | c2 = np.array([i for i in zip(li2.coords.xy[0], li2.coords.xy[1])]) 80 | return (pairwise_distances(c1, c2, metric='euclidean', n_jobs=1)).max() 81 | 82 | 83 | def hav_dist(locs1, locs2): 84 | # (lat, lon) (lat, lon) 85 | locs1 = locs1 * 0.0174532925 86 | locs2 = locs2 * 0.0174532925 87 | cos_lat1 = np.cos(locs1[..., 0]) 88 | cos_lat2 = np.cos(locs2[..., 0]) 89 | cos_lat_d = np.cos(locs1[..., 0] - locs2[..., 0]) 90 | cos_lon_d = np.cos(locs1[..., 1] - locs2[..., 1]) 91 | return 6367 * np.arccos(cos_lat_d - cos_lat1 * cos_lat2 * (1 - cos_lon_d)) 92 | 93 | 94 | def bearing_180(li1): 95 | len_li = len(li1.coords.xy[0]) - 1 96 | first_pt_x, first_pt_y = li1.coords.xy[0][0], li1.coords.xy[1][0] 97 | last_pt_x, last_pt_y = li1.coords.xy[0][len_li], li1.coords.xy[1][len_li] 98 | b = 180 + math.atan2( 99 | (first_pt_x - last_pt_x), (first_pt_y - last_pt_y) 100 | ) * (180 / math.pi) 101 | if b > 180: 102 | return 180 - b 103 | else: 104 | return b 105 | 106 | def dorling_radius(poly, value_field, ratio, 107 | pi=np.pi, sqrt=np.sqrt): 108 | cum_dist, cum_rad = 0, 0 109 | centroids = poly.geometry.centroid 110 | for i in range(len(centroids)): 111 | for j in range(len(centroids)): 112 | if i != j: 113 | l = centroids.geometry[i].distance(centroids.geometry[j]) 114 | d = sqrt(poly.iloc[i][value_field]/pi) \ 115 | + sqrt(poly.iloc[j][value_field]/pi) 116 | cum_dist = cum_dist + l 117 | cum_rad = cum_rad + d 118 | 119 | scale = cum_dist / cum_rad 120 | radiuses = sqrt(poly[value_field]/pi) * scale * ratio 121 | norm_areas = normalize( 122 | [poly.geometry[i].area for i in range(len(poly))] 123 | )[0] 124 | return radiuses * norm_areas 125 | 126 | 127 | def dorling_radius2(poly, value_field, ratio, mat_shared_border, 128 | pi=np.pi, sqrt=np.sqrt): 129 | cum_dist, cum_rad = 0, 0 130 | centroids = poly.geometry.centroid 131 | for i in range(len(centroids)): 132 | for j in range(len(centroids)): 133 | if i != j: 134 | fp = abs( 135 | round(mat_shared_border[i][j] / mat_shared_border[i].sum(), 2) - 1) 136 | l = centroids.geometry[i].distance(centroids.geometry[j]) 137 | d = sqrt(poly.iloc[i][value_field]/pi) \ 138 | + sqrt(poly.iloc[j][value_field]/pi) 139 | cum_dist = cum_dist + l*(fp/2) 140 | cum_rad = cum_rad + d 141 | # print(fp, cum_dist, cum_rad) 142 | scale = cum_dist / cum_rad 143 | radiuses = sqrt(poly[value_field]/pi) * scale * ratio 144 | norm_areas = normalize( 145 | [poly.geometry[i].area for i in range(len(poly))] 146 | )[0] 147 | return radiuses * norm_areas 148 | 149 | def l_shared_border(gdf, touche_table): 150 | dim = len(touche_table) 151 | mat_sh_bord = np.empty((dim,dim)) 152 | for id1 in range(dim): 153 | for id2 in touche_table.iloc[id1]: 154 | mat_sh_bord[id1][id2] = \ 155 | (gdf.geometry[id1].buffer(0.01).intersection(gdf.geometry[id2])).length 156 | mat_sh_bord[id2][id1] = mat_sh_bord[id1][id2] 157 | return mat_sh_bord 158 | 159 | def ftouches_byid(geoms1, geoms2, tolerance=0): 160 | """ 161 | Return a table with a row for each features of *geoms1*, containing the id 162 | of each *geoms2* touching features. 163 | The test is not based on the *touches* predicat but on a intersects 164 | between the two features (which are buffered with *tolerance*) 165 | 166 | Parameters 167 | ---------- 168 | geoms1: GeoSeries or GeoDataFrame 169 | Collection on which the touching table will be based. 170 | geoms2: GeoSeries or GeoDataFrame 171 | Collection to test against the first one. 172 | tolerance: Float 173 | The tolerance within two features as considered as touching. 174 | (in unit of both input collections) 175 | 176 | Returns 177 | ------- 178 | touching_table: pandas.Series 179 | A Series with the same index id as geoms1, each row containg the ids of 180 | the features of geoms2 touching it. 181 | """ 182 | return geoms1.geometry.apply( 183 | lambda x: [i for i in range(len(geoms2.geometry)) 184 | if x.intersects(geoms2.geometry[i].buffer(tolerance))] 185 | ) 186 | 187 | 188 | def intersection_part(g1, g2): 189 | """ 190 | Return the part of *g1* which is covered by *g2*. 191 | Return 0 if no intersection or invalid geom(s). 192 | 193 | Parameters 194 | ---------- 195 | g1: Shapely.geometry 196 | g2: Shapely.geometry 197 | """ 198 | try: 199 | if g1.intersects(g2): 200 | return g1.intersection(g2).area / g1.area 201 | else: 202 | return 0 203 | except TopologicalError as err: 204 | print('Warning : {}'.format(err)) 205 | return 0 206 | 207 | 208 | def intersection_part_table(geoms1, geoms2): 209 | return geoms1.geometry.apply( 210 | lambda x: [intersection_part(x, geoms2.geometry[i]) for i in range(len(geoms2.geometry))] 211 | ) 212 | 213 | 214 | def make_prop_lines(gdf, field_name, color='red', normalize_values=False, 215 | norm_min=None, norm_max=None, axes=None): 216 | """ 217 | Display a GeoDataFrame collection of (Multi)LineStrings, 218 | proportionaly to numerical field. 219 | 220 | Parameters 221 | ---------- 222 | gdf: GeoDataFrame 223 | The collection of linestring/multilinestring to be displayed 224 | field_name: String 225 | The name of the field containing values to scale the line width of 226 | each border. 227 | color: String 228 | The color to render the lines with. 229 | normalize_values: Boolean, default False 230 | Normalize the value of the 'field_name' column between norm_min and 231 | norm_max. 232 | norm_min: float, default None 233 | The linewidth for the minimum value to plot. 234 | norm_max: float, default None 235 | The linewidth for the maximum value to plot. 236 | axes: 237 | Axes on which to draw the plot. 238 | 239 | Return 240 | ------ 241 | axes: matplotlib.axes._subplots.AxesSubplot 242 | """ 243 | from geopandas.plotting import plot_linestring, plot_multilinestring 244 | from shapely.geometry import MultiLineString 245 | import matplotlib.pyplot as plt 246 | 247 | if normalize_values and norm_max and norm_min: 248 | vals = (norm_max - norm_min) * (normalize(gdf[field_name].astype(float))).T + norm_min 249 | elif normalize_values: 250 | print('Warning : values where not normalized ' 251 | '(norm_max or norm_min is missing)') 252 | vals = gdf[field_name].values 253 | else: 254 | vals = gdf[field_name].values 255 | 256 | if not axes: 257 | axes = plt.gca() 258 | for nbi, line in enumerate(gdf.geometry.values): 259 | if isinstance(line, MultiLineString): 260 | plot_multilinestring(axes, gdf.geometry.iloc[nbi], 261 | linewidth=vals[nbi], color=color) 262 | else: 263 | plot_linestring(axes, gdf.geometry.iloc[nbi], 264 | linewidth=vals[nbi], color=color) 265 | return axes 266 | 267 | 268 | class Borderiz(object): 269 | def __init__(self, gdf_polygon): 270 | self.gdf = gdf_polygon.copy() 271 | 272 | def run(self, tol, col_name): 273 | self.multi_to_singles() 274 | self._buffer(tol) 275 | self._polygon_to_lines() 276 | self._buff_line_intersection(col_name) 277 | self._grep_border() 278 | return self.border 279 | 280 | def _polygon_to_lines(self): 281 | s_t = time.time() 282 | self.gdf.geometry = self.gdf.geometry.boundary 283 | # print('{:.2f}s'.format(time.time()-s_t)) 284 | 285 | def multi_to_singles(self): 286 | """Return a new geodataframe where each feature is a single-geometry""" 287 | values = self.gdf[[i for i in self.gdf.columns if i != 'geometry']] 288 | geom = self.gdf.geometry 289 | geoms, attrs = [], [] 290 | for i in range(len(self.gdf)-1): 291 | try: 292 | for single_geom in geom.iloc[i]: 293 | geoms.append(single_geom) 294 | attrs.append(values.iloc[i]) 295 | except: 296 | geoms.append(geom.iloc[i]) 297 | attrs.append(values.iloc[i]) 298 | self.gdf = gpd.GeoDataFrame( 299 | attrs, geometry=geoms, 300 | columns=[i for i in self.gdf.columns if i != 'geometry'], 301 | index=pd.Int64Index([i for i in range(len(geoms))])) 302 | 303 | def _buffer(self, tol): 304 | s_t = time.time() 305 | self.buffered = self.gdf.copy() 306 | self.buffered.geometry = self.buffered.geometry.buffer(tol) 307 | # print('{:.2f}s'.format(time.time()-s_t)) 308 | 309 | def _buff_line_intersection(self, col_name): 310 | s_t = time.time() 311 | resgeom, resattrs = [], [] 312 | resgappd = resgeom.append 313 | resaappd = resattrs.append 314 | res = self.intersects_table() 315 | for i, _ in enumerate(res): 316 | fti = self.gdf.iloc[i] 317 | i_geom = self.gdf.geometry.iloc[i] 318 | for j in res[i]: 319 | ftj = self.buffered.iloc[j] 320 | j_geom = self.buffered.geometry.iloc[j] 321 | tmp = i_geom.intersection(j_geom) 322 | if 'Collection' not in tmp.geom_type: 323 | resgappd(i_geom.intersection(j_geom)) 324 | resaappd( 325 | (fti[col_name]+'-'+ftj[col_name], 326 | ftj[col_name]+'-'+fti[col_name])) 327 | else: 328 | pass 329 | self.result = gpd.GeoDataFrame( 330 | resattrs, geometry=resgeom, 331 | columns=['FRONT', 'FRONT_r'], 332 | index=pd.Int64Index([i for i in range(len(resattrs))]) 333 | ) 334 | # self.result = self.result.ix[[not self.result.geometry[i].is_ring 335 | # for i in range(len(self.result)-1)]] 336 | # print( 337 | # '{:.2f}s ({} features)'.format(time.time()-s_t, len(self.result)) 338 | # ) 339 | 340 | def _filt(self, ref): 341 | for ii in range(len(self.result)): 342 | if self.result.iloc[ii]['FRONT_r'] in ref['FRONT'] \ 343 | and self.result.iloc[ii]['FRONT'] != self.result.iloc[ii]['FRONT_r']: 344 | yield self.result.iloc[ii] 345 | 346 | def _grep_border(self): 347 | s_t = time.time() 348 | self.border = [] 349 | seen = {} 350 | for i in range(len(self.result)): 351 | fti = self.result.iloc[i] 352 | for j in self._filt(fti): 353 | try: 354 | puidj = str(round(j.geometry.length, -2)) 355 | if j['FRONT'] + puidj not in seen \ 356 | and j['FRONT_r'] + puidj not in seen: 357 | key1 = j['FRONT'] + puidj 358 | key2 = j['FRONT_r'] + puidj 359 | seen[key1] = 1 360 | seen[key2] = 1 361 | self.border.append(j) 362 | except TypeError as err: 363 | print(err) 364 | pass 365 | self.border = gpd.GeoDataFrame(self.border) 366 | # print('{:.2f}s'.format(time.time()-s_t)) 367 | # print('len(seen) : ', len(seen), ' | len(border) : ', len(self.border)) 368 | 369 | def intersects_table(self): 370 | """ 371 | Return a table with a row for each features of g1, each one containing 372 | the id of each g2 intersecting features 373 | """ 374 | return self.gdf.geometry.apply( 375 | lambda x: [i for i in range(len(self.buffered.geometry)) 376 | if x.intersects(self.buffered.iloc[i].geometry)] 377 | ) 378 | -------------------------------------------------------------------------------- /gpd_lite_toolbox/utils_carto.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | @author: mthh 4 | 5 | 6 | Copy-paste from geopandas.plotting module with only minor modif. in order to 7 | take linewidth and edgecolor on polygon plotting into account. 8 | """ 9 | from geopandas.plotting import ( 10 | _mapclassify_choro, plot_series, plot_point_collection, plot_linestring_collection 11 | ) 12 | from geopandas import GeoSeries 13 | import numpy as np 14 | 15 | 16 | def m_plot_multipolygon(ax, geom, linewidth, facecolor='red', edgecolor='grey', alpha=0.5): 17 | """ Can safely call with either Polygon or Multipolygon geometry 18 | """ 19 | if geom.type == 'Polygon': 20 | m_plot_polygon(ax, geom, facecolor=facecolor, edgecolor=edgecolor, 21 | linewidth=linewidth, alpha=alpha) 22 | elif geom.type == 'MultiPolygon': 23 | for poly in geom.geoms: 24 | m_plot_polygon(ax, poly, facecolor=facecolor, edgecolor=edgecolor, 25 | linewidth=linewidth, alpha=alpha) 26 | 27 | 28 | def m_plot_polygon(ax, poly, facecolor='red', edgecolor='black', linewidth=0.5, alpha=0.5): 29 | """ Plot a single Polygon geometry """ 30 | from descartes.patch import PolygonPatch 31 | a = np.asarray(poly.exterior) 32 | # without Descartes, we could make a Patch of exterior 33 | ax.add_patch(PolygonPatch(poly, facecolor=facecolor, 34 | linewidth=linewidth, alpha=alpha)) 35 | ax.plot(a[:, 0], a[:, 1], color=edgecolor, linewidth=linewidth) 36 | for p in poly.interiors: 37 | x, y = zip(*p.coords) 38 | ax.plot(x, y, color=edgecolor, linewidth=linewidth) 39 | 40 | 41 | def m_plot_dataframe(s, column=None, colormap=None, alpha=0.5, edgecolor=None, 42 | categorical=False, legend=False, axes=None, scheme=None, 43 | contour_poly_width=0.5, 44 | k=5): 45 | """ Plot a GeoDataFrame 46 | 47 | Generate a plot of a GeoDataFrame with matplotlib. If a 48 | column is specified, the plot coloring will be based on values 49 | in that column. Otherwise, a categorical plot of the 50 | geometries in the `geometry` column will be generated. 51 | 52 | Parameters 53 | ---------- 54 | 55 | GeoDataFrame 56 | The GeoDataFrame to be plotted. Currently Polygon, 57 | MultiPolygon, LineString, MultiLineString and Point 58 | geometries can be plotted. 59 | 60 | column : str (default None) 61 | The name of the column to be plotted. 62 | 63 | categorical : bool (default False) 64 | If False, colormap will reflect numerical values of the 65 | column being plotted. For non-numerical columns (or if 66 | column=None), this will be set to True. 67 | 68 | colormap : str (default 'Set1') 69 | The name of a colormap recognized by matplotlib. 70 | 71 | alpha : float (default 0.5) 72 | Alpha value for polygon fill regions. Has no effect for 73 | lines or points. 74 | 75 | legend : bool (default False) 76 | Plot a legend (Experimental; currently for categorical 77 | plots only) 78 | 79 | axes : matplotlib.pyplot.Artist (default None) 80 | axes on which to draw the plot 81 | 82 | scheme : pysal.esda.mapclassify.Map_Classifier 83 | Choropleth classification schemes 84 | 85 | k : int (default 5) 86 | Number of classes (ignored if scheme is None) 87 | 88 | 89 | Returns 90 | ------- 91 | 92 | matplotlib axes instance 93 | """ 94 | import matplotlib.pyplot as plt 95 | from matplotlib.lines import Line2D 96 | from matplotlib.colors import Normalize 97 | from matplotlib import cm 98 | 99 | if column is None: 100 | return plot_series(s.geometry, colormap=colormap, alpha=alpha, axes=axes) 101 | else: 102 | if s[column].dtype is np.dtype('O'): 103 | categorical = True 104 | if categorical: 105 | if colormap is None: 106 | colormap = 'Set1' 107 | categories = list(set(s[column].values)) 108 | categories.sort() 109 | valuemap = dict([(key, v) for (v, key) in enumerate(categories)]) 110 | values = [valuemap[key] for key in s[column]] 111 | else: 112 | values = s[column] 113 | if scheme is not None: 114 | values = _mapclassify_choro(values, scheme, k=k) 115 | 116 | norm = Normalize(vmin=values.min(), vmax=values.max()) 117 | cmap = cm.ScalarMappable(norm=norm, cmap=colormap) 118 | if not axes: 119 | fig = plt.gcf() 120 | fig.add_subplot(111, aspect='equal') 121 | ax = plt.gca() 122 | else: 123 | ax = axes 124 | for geom, value in zip(s.geometry, values): 125 | if geom.type == 'Polygon' or geom.type == 'MultiPolygon': 126 | m_plot_multipolygon(ax, geom, facecolor=cmap.to_rgba(value), 127 | edgecolor=edgecolor, 128 | linewidth=contour_poly_width, alpha=alpha) 129 | elif geom.type == 'LineString' or geom.type == 'MultiLineString': 130 | plot_linestring_collection(ax, GeoSeries([geom]), colors=[cmap.to_rgba(value)]) 131 | # TODO: color point geometries 132 | elif geom.type == 'Point': 133 | plot_point_collection(ax, GeoSeries([geom])) 134 | if legend: 135 | if categorical: 136 | patches = [] 137 | for value, cat in enumerate(categories): 138 | patches.append(Line2D([0], [0], linestyle="none", 139 | marker="o", alpha=alpha, 140 | markersize=10, 141 | markerfacecolor=cmap.to_rgba(value))) 142 | ax.legend(patches, categories, numpoints=1, loc='best') 143 | else: 144 | # TODO: show a colorbar 145 | raise NotImplementedError 146 | plt.draw() 147 | return ax -------------------------------------------------------------------------------- /misc/output_11_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mthh/gpd_lite_toolbox/28f901ccdd815ca49d07aae11002be27ee627219/misc/output_11_1.png -------------------------------------------------------------------------------- /misc/output_13_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mthh/gpd_lite_toolbox/28f901ccdd815ca49d07aae11002be27ee627219/misc/output_13_1.png -------------------------------------------------------------------------------- /misc/output_16_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mthh/gpd_lite_toolbox/28f901ccdd815ca49d07aae11002be27ee627219/misc/output_16_1.png -------------------------------------------------------------------------------- /misc/output_19_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mthh/gpd_lite_toolbox/28f901ccdd815ca49d07aae11002be27ee627219/misc/output_19_1.png -------------------------------------------------------------------------------- /misc/output_21_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mthh/gpd_lite_toolbox/28f901ccdd815ca49d07aae11002be27ee627219/misc/output_21_1.png -------------------------------------------------------------------------------- /misc/output_22_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mthh/gpd_lite_toolbox/28f901ccdd815ca49d07aae11002be27ee627219/misc/output_22_1.png -------------------------------------------------------------------------------- /misc/output_26_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mthh/gpd_lite_toolbox/28f901ccdd815ca49d07aae11002be27ee627219/misc/output_26_1.png -------------------------------------------------------------------------------- /misc/output_6_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mthh/gpd_lite_toolbox/28f901ccdd815ca49d07aae11002be27ee627219/misc/output_6_1.png -------------------------------------------------------------------------------- /misc/output_9_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mthh/gpd_lite_toolbox/28f901ccdd815ca49d07aae11002be27ee627219/misc/output_9_1.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | gpd_lite_toolboox setup file 4 | """ 5 | 6 | from distutils.core import setup 7 | from distutils.extension import Extension 8 | from Cython.Distutils import build_ext 9 | from Cython.Build import cythonize 10 | import gpd_lite_toolbox 11 | 12 | 13 | ext = Extension("gpd_lite_toolbox.cycartogram", 14 | ["gpd_lite_toolbox/cycartogram.pyx"], ["."]) 15 | 16 | setup( 17 | name='gpd_lite_toolbox', 18 | version=gpd_lite_toolbox.__version__, 19 | description='Convenience functions acting on GeoDataFrames', 20 | author='mthh', 21 | ext_modules=cythonize(ext), 22 | cmdclass = {'build_ext': build_ext}, 23 | packages=['gpd_lite_toolbox'], 24 | license='MIT', 25 | ) 26 | --------------------------------------------------------------------------------