-
Notifications
You must be signed in to change notification settings - Fork 12
Expand file tree
/
Copy pathvector_gpd.py
More file actions
1986 lines (1636 loc) · 76.2 KB
/
Copy pathvector_gpd.py
File metadata and controls
1986 lines (1636 loc) · 76.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python
# Filename: vector_gpd
"""
introduction: similar to vector_features.py, by use geopandas to read and write shapefile
authors: Huang Lingcao
email:huanglingcao@gmail.com
add time: 08 December, 2019
"""
import os,sys
from optparse import OptionParser
# import these two to make sure load GEOS dll before using shapely
import shapely
from shapely.geometry import mapping # transform to GeJSON format
from shapely.geometry import MultiPolygon
from shapely.geometry import Polygon
from shapely.geometry import box
from shapely.geometry import LineString
from shapely.geometry import MultiLineString
from shapely import ops
from shapely.geometry import GeometryCollection
from shapely.ops import unary_union
from shapely.strtree import STRtree
import geopandas as gpd
from shapely.geometry import Point
import pandas as pd
import math
import numpy as np
import time
import random
import basic_src.basic as basic
import basic_src.map_projection as map_projection
from datetime import datetime
from multiprocessing import Pool
from packaging import version
import networkx as nx
import fiona
def check_remove_None_geometries(geometries, gpd_dataframe, file_path=None):
# Missing and empty geometries, find None geometry, then remove them
# https://geopandas.org/en/stable/docs/user_guide/missing_empty.html
# check None in geometries:
# gpd_dataframe = gpd.read_file(polygon_shp)
# geometries = shapefile.geometry.values
idx_list = [ idx for idx, polygon in enumerate(geometries) if polygon is None]
if len(idx_list) > 0:
message = 'Warning, %d None geometries, will be removed'%len(idx_list)
if file_path is not None:
message += ', file path: %s'%file_path
for idx in idx_list:
gpd_dataframe.drop(idx, inplace=True)
# geometries.drop(idx,inplace=True) # not working
basic.outputlogMessage(message)
# return geometries again after droping some rows
return gpd_dataframe.geometry.values
def check_remove_None_geometries_file(input_file, output_file):
"""
Reads a geospatial file, removes rows with None geometries, and saves the cleaned file.
:param input_file: Path to the input file (GeoJSON, Shapefile, etc.).
:param output_file: Path to save the output file with cleaned geometries.
:return: None
"""
# Read the input file into a GeoDataFrame
gpd_dataframe = gpd.read_file(input_file)
# Find rows with None geometries
none_geometry_indices = gpd_dataframe[gpd_dataframe.geometry.isna()].index
# Log and remove None geometries
if len(none_geometry_indices) > 0:
print(f"Warning: Found {len(none_geometry_indices)} None geometries. Removing them...")
# Drop rows with None geometries
gpd_dataframe.drop(none_geometry_indices, inplace=True)
else:
print("No None geometries found. No changes made.")
# Save the cleaned GeoDataFrame to the output file
gpd_dataframe.to_file(output_file)
print(f"Cleaned file saved to: {output_file}")
def guess_file_format_extension(file_path):
_, extension = os.path.splitext(file_path)
if extension.lower() == '.gpkg': # GPKG
return 'GPKG'
elif extension.lower() == '.shp': # GPKG
return 'ESRI Shapefile'
else:
raise ValueError('unknown file format from extension: %s'%extension)
def read_polygons_json(polygon_shp, no_json=False):
'''
read polyogns and convert to json format
:param polygon_shp: polygon in projection of EPSG:4326
:param no_json: True indicate not json format
:return:
'''
# check projection
shp_args_list = ['gdalsrsinfo', '-o', 'EPSG', polygon_shp]
epsg_str = basic.exec_command_args_list_one_string(shp_args_list)
epsg_str = epsg_str.decode().strip() # byte to str, remove '\n'
if epsg_str != 'EPSG:4326':
raise ValueError('Current support shape file in projection of EPSG:4326, but the input has projection of %s'%epsg_str)
shapefile = gpd.read_file(polygon_shp)
polygons = shapefile.geometry.values
# # check invalidity of polygons
invalid_polygon_idx = []
# for idx, geom in enumerate(polygons):
# if geom.is_valid is False:
# invalid_polygon_idx.append(idx + 1)
# if len(invalid_polygon_idx) > 0:
# raise ValueError('error, polygons %s (index start from 1) in %s are invalid, please fix them first '%(str(invalid_polygon_idx),polygon_shp))
# fix invalid polygons
polygons = fix_invalid_polygons(polygons)
if no_json:
return polygons
else:
# convert to json format
polygons_json = [ mapping(item) for item in polygons]
return polygons_json
def fix_invalid_polygons(polygons, buffer_size = 0.000001):
'''
fix invalid polygon by using buffer operation.
:param polygons: polygons in shapely format
:param buffer_size: buffer size
:return: polygons after checking invalidity
'''
invalid_polygon_idx = []
for idx in range(0,len(polygons)):
if polygons[idx].is_valid is False:
invalid_polygon_idx.append(idx + 1)
polygons[idx] = polygons[idx].buffer(buffer_size) # trying to solve self-intersection
if len(invalid_polygon_idx) > 0:
basic.outputlogMessage('Warning, polygons %s (index start from 1) in are invalid, fix them by the buffer operation '%(str(invalid_polygon_idx)))
return polygons
def read_lines_gpd(lines_shp):
shapefile = gpd.read_file(lines_shp)
lines = shapefile.geometry.values
# check are lines
return lines
def read_lines_attributes_list(polygon_shp, field_nameS):
return read_polygons_attributes_list(polygon_shp, field_nameS, b_fix_invalid_polygon=False)
def find_one_line_intersect_Polygon(polygon, line_list, line_check_list,b_line_only_one_poly):
for idx, (line, b_checked) in enumerate(zip(line_list,line_check_list)):
if b_checked and b_line_only_one_poly:
continue
if polygon.intersection(line).is_empty is False:
line_check_list[idx] = True
return line
return None
def find_polygon_intersec_polygons(shp_path):
basic.outputlogMessage('Checking duplicated polygons in %s'%shp_path)
polygons = read_polygons_gpd(shp_path)
count = len(polygons)
for idx, poly in enumerate(polygons):
for kk in range(idx+1,count):
inter = poly.intersection(polygons[kk])
if inter.is_empty is False:
basic.outputlogMessage('warning, %d th polygon has intersection with %d th polygon'%(idx+1, kk+1))
# break
basic.outputlogMessage('finished checking of polygons intersect other polygons')
def read_shape_gpd_to_NewPrj(shp_path, prj_str):
'''
read polyogns using geopandas, and reproejct to a projection.
:param polygon_shp:
:param prj_str: project string, like EPSG:4326
:return:
'''
shapefile = gpd.read_file(shp_path)
# print(shapefile.crs)
# shapefile = shapefile.to_crs(prj_str)
if version.parse(gpd.__version__) >= version.parse('0.7.0'):
shapefile = shapefile.to_crs(prj_str)
else:
shapefile = shapefile.to_crs({'init':prj_str})
# print(shapefile.crs)
polygons = shapefile.geometry.values
# fix invalid polygons
polygons = fix_invalid_polygons(polygons)
return polygons
def reproject_shapefile(shp_path, prj_str,save_path):
'''
reprject a shapefile and save to another path
:param shp_path: EPSG:4326
:param prj_str: e.g., EPSG:4326
:param save_path: save path
:return:
'''
shapefile = gpd.read_file(shp_path)
# print(shapefile.crs)
# shapefile = shapefile.to_crs(prj_str)
if version.parse(gpd.__version__) >= version.parse('0.7.0'):
shapefile = shapefile.to_crs(prj_str)
else:
shapefile = shapefile.to_crs({'init': prj_str})
return shapefile.to_file(save_path, driver = 'ESRI Shapefile')
def read_polygons_gpd(polygon_shp, b_fix_invalid_polygon = True):
'''
read polyogns using geopandas
:param polygon_shp: polygon in projection of EPSG:4326
:param no_json: True indicate not json format
:return:
'''
shapefile = gpd.read_file(polygon_shp)
polygons = shapefile.geometry.values
# print('before removing None, %d records'%len(shapefile))
polygons = check_remove_None_geometries(polygons,shapefile,polygon_shp)
# print('after removing None, %d records' % len(shapefile))
# # check invalidity of polygons
invalid_polygon_idx = []
# for idx, geom in enumerate(polygons):
# if geom.is_valid is False:
# invalid_polygon_idx.append(idx + 1)
# if len(invalid_polygon_idx) > 0:
# raise ValueError('error, polygons %s (index start from 1) in %s are invalid, please fix them first '%(str(invalid_polygon_idx),polygon_shp))
# fix invalid polygons
if b_fix_invalid_polygon:
polygons = fix_invalid_polygons(polygons)
return polygons
def add_attributes_to_shp(shp_path, add_attributes,save_as=None,format='ESRI Shapefile'):
'''
add attbibutes to a shapefile
:param shp_path: the path of shapefile
:param add_attributes: attributes (dict)
:return: True if successful, False otherwise
'''
shapefile = gpd.read_file(shp_path)
# print(shapefile.loc[0]) # output the first row
# get attributes_names
org_attribute_names = [ key for key in shapefile.loc[0].keys()]
# print(org_attribute_names)
for key in add_attributes.keys():
if key in org_attribute_names:
basic.outputlogMessage('warning, field name: %s already in table '
'this will replace the original value'%(key))
shapefile[key] = add_attributes[key]
# print(shapefile)
# save the original file
if save_as is not None:
return shapefile.to_file(save_as, driver=format)
else:
return shapefile.to_file(shp_path, driver=format)
def read_attribute_values_list(polygon_shp, field_name, out_dtype=None):
'''
Read the attribute values from a shapefile column into a list.
:param polygon_shp: Path to the shapefile.
:param field_name: The column name to read.
:param out_dtype: Target data type for the output values (e.g., int, float, str).
:return: A list containing the attribute values (converted if out_dtype is specified), or None if field is missing.
'''
shapefile = gpd.read_file(polygon_shp)
if field_name in shapefile.keys():
attribute_values = shapefile[field_name]
# Convert to target dtype if specified
if out_dtype is not None:
converted_values = attribute_values.astype(out_dtype)
# Optionally, convert NaN to None
result_list = [v if pd.notnull(v) else None for v in converted_values]
return result_list
else:
return attribute_values.tolist()
else:
basic.outputlogMessage('Warning: %s not in the shape file, will return None'%field_name)
return None
def read_attribute_values_list_2d(polygon_shp, field_nameS):
'''
read attribute value (list)
:param polygon_shp:
:param field_nameS: a string file name or a list of field_name
:return: attributes
'''
shapefile = gpd.read_file(polygon_shp)
# read attributes
if isinstance(field_nameS, str): # only one field name
if field_nameS in shapefile.keys():
attribute_values = shapefile[field_nameS]
return attribute_values.tolist()
else:
basic.outputlogMessage('Warning: %s not in the shape file, get None' % field_nameS)
return None
elif isinstance(field_nameS, list): # a list of field name
attribute_2d = []
for field_name in field_nameS:
if field_name in shapefile.keys():
attribute_values = shapefile[field_name]
attribute_2d.append(attribute_values.tolist())
else:
basic.outputlogMessage('Warning: %s not in the shape file, get None' % field_nameS)
attribute_2d.append(None)
return attribute_2d
def is_field_name_in_shp(polygon_shp, field_name):
'''
check a attribute name is in the shapefile
:param polygon_shp:
:param field_name:
:return:
'''
# using finoa is much faster than geopanda when the file is large
with fiona.open(polygon_shp) as src:
# print("Column names:", list(src.schema['properties'].keys()))
# To check if 'name' exists:
return field_name in src.schema['properties']
# shapefile = gpd.read_file(polygon_shp, rows=0) # Only reads schema, not data!
# print(field_name in shapefile.columns)
# if field_name in shapefile.keys():
# return True
# else:
# return False
def read_attribute_name_list(polygon_shp):
'''
read all attribute names from a shapefile
:param polygon_shp:
:return: a list of attribute names
'''
# using finoa is much faster than geopanda when the file is large
with fiona.open(polygon_shp) as src:
return list(src.schema['properties'].keys())
def read_polygons_attributes_list(polygon_shp, field_nameS, b_fix_invalid_polygon = True):
'''
read polygons and attribute value (list)
:param polygon_shp:
:param field_nameS: a string file name or a list of field_name
:return: Polygons and attributes
'''
shapefile = gpd.read_file(polygon_shp)
polygons = shapefile.geometry.values
# check None
polygons = check_remove_None_geometries(polygons,shapefile,polygon_shp)
# fix invalid polygons
if b_fix_invalid_polygon:
polygons = fix_invalid_polygons(polygons)
# read attributes
if isinstance(field_nameS,str): # only one field name
if field_nameS in shapefile.keys():
attribute_values = shapefile[field_nameS]
return polygons, attribute_values.tolist()
else:
basic.outputlogMessage('Warning: %s not in the shape file, get None' % field_nameS)
return polygons, None
elif isinstance(field_nameS,list): # a list of field name
attribute_2d = []
for field_name in field_nameS:
if field_name in shapefile.keys():
attribute_values = shapefile[field_name]
attribute_2d.append(attribute_values.tolist())
else:
basic.outputlogMessage('Warning: %s not in the shape file, get None' % field_nameS)
attribute_2d.append(None)
return polygons, attribute_2d
else:
raise ValueError('unknown type of %s'%str(field_nameS))
def is_two_bound_disjoint(box1, box2):
# same to the one in raster_io by calling rasterio.coords.disjoint_bounds(box1,box2)
# but just do not want to import rater_io
# box: (minx, miny, maxx, maxy)
# left 1 > right 2 or right 1 < left 2 or bottom 1 > top 2 or top 1 < bottom 2
if box1[0] > box2[2] or box1[2] < box2[0] or box1[1] > box2[3] or box1[3] < box2[1]:
return True
return False
def get_projection(file_path, format=None):
# convert the different type, to epsg, proj4, and wkt
gdf = gpd.read_file(file_path)
if format is not None:
if format == 'proj4':
return gdf.crs.to_proj4() # string like '+proj=stere +lat_0=90 +lat_ts=70 +lon_0=-45 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +type=crs',
elif format == 'wkt':
return gdf.crs.to_wkt() # string, # its OGC WKT representation
elif format == 'epsg':
return gdf.crs.to_epsg() # to epsg code, int, such as 3413
else:
raise ValueError('Unknown format: %s' % str(format))
return gdf.crs
def get_vector_file_bounding_box(file_path):
# return bounding box of all geometryies ((minx, miny, maxx, maxy))
shapefile = gpd.read_file(file_path)
return shapefile.total_bounds
def get_polygon_bounding_box(polygon):
# return the bounding box of a shapely polygon (minx, miny, maxx, maxy)
return polygon.bounds
def get_polygon_centroid_lat_lon(in_shp):
geometries = read_shape_gpd_to_NewPrj(in_shp,'EPSG:4326')
# Get centroids and extract (lat, lon)
centroids = geometries.centroid
lat_lon_list = [(pt.y, pt.x) for pt in centroids]
return lat_lon_list
def get_polygon_centroid(polygon):
# return the geometric center of a polygon
return polygon.centroid
def get_polygon_representative_point(polygon):
# centroid not always inside a polygon,
# use representative_point to get a point alway inside the polygon, not in general the same as the centroid
return polygon.representative_point()
def get_polygon_envelope_xy(polygon):
# get polygon envelope x,y coordinates
# polygon, shapely polygon
# output: x: a list of x0 to x4 y: a list of y0 to y4 # the last one is the same as the first one.
polygon_env = polygon.envelope
x, y = polygon_env.exterior.coords.xy
return x,y
def get_box_polygon_leftUp_rightDown(box_polygon):
# get box left-up and right-botton x,y coordinates
# box_polygon, shapely polygon
# output: (x1, y1, x2, y2) # the last one is the same as the first one.
x, y = box_polygon.exterior.coords.xy
return (x[0], y[0], x[2], y[2])
def remove_polygon_equal(shapefile,field_name, expect_value, b_equal, output):
'''
remove polygons the the attribute value is not equal to a specific value
:param shapefile:
:param field_name:
:param threshold:
:param b_equal: if True, remove records not equal to expect_value, otherwise, remove the one equal to expect_value
:param output:
:return:
'''
shapefile = gpd.read_file(shapefile)
remove_count = 0
for idx,row in shapefile.iterrows():
# polygon = row['geometry']
# go through post-processing to decide to keep or remove it
if b_equal:
if row[field_name] != expect_value:
shapefile.drop(idx, inplace=True)
remove_count += 1
else:
if row[field_name] == expect_value:
shapefile.drop(idx, inplace=True)
remove_count += 1
basic.outputlogMessage('remove %d polygons based on %s, remain %d ones saving to %s' %
(remove_count, field_name, len(shapefile.geometry.values), output))
# save results
shapefile.to_file(output, driver='ESRI Shapefile')
def remove_polygon_time_index(shapefile,field_name, time_count, output):
'''
remove polygons if the time index is not monotonically increasing and not follow the pattern
:param shapefile:
:param field_name:
:param time_count:
:param output:
:return:
'''
remove_count = 0
shapefile = gpd.read_file(shapefile)
for idx, row in shapefile.iterrows():
idx_string = row[field_name]
num_list = [int(item) for item in idx_string.split('_')]
# the number list should be one of the pattern: 0, 1, 2...n or 1, 2,...n or n, not only monotonically increasing
pattern_int = [str(item) for item in range(num_list[0],time_count)]
pattern_str = '_'.join(pattern_int)
# if np.all(np.diff(num_list) >= 1):
if idx_string == pattern_str:
pass
else:
shapefile.drop(idx, inplace=True)
remove_count += 1
basic.outputlogMessage('remove %d polygons based on %s, remain %d ones saving to %s' %
(remove_count, field_name, len(shapefile.geometry.values), output))
# save results
return shapefile.to_file(output, driver='ESRI Shapefile')
def remove_polygon_index_string(shapefile,field_name, index_list, output):
'''
remove polygons the the attribute value is not equal to a specific value
:param shapefile:
:param field_name:
:param threshold:
:param b_equal: if True, remove records not equal to expect_value, otherwise, remove the one equal to expect_value
:param output:
:return:
'''
if len(index_list) < 1:
raise ValueError('Wrong input index_list, it size is zero')
shapefile = gpd.read_file(shapefile)
remove_count = 0
for idx,row in shapefile.iterrows():
# polygon = row['geometry']
# go through post-processing to decide to keep or remove it
idx_string = row[field_name]
num_list = [ int(item) for item in idx_string.split('_')]
# if all the index in index_list found in num_list, then keep it, otherwise, remove it
for index in index_list:
if index not in num_list:
shapefile.drop(idx, inplace=True)
remove_count += 1
break
basic.outputlogMessage('remove %d polygons based on %s, remain %d ones saving to %s' %
(remove_count, field_name, len(shapefile.geometry.values), output))
# save results
shapefile.to_file(output, driver='ESRI Shapefile')
def remove_polygons_not_in_range(shapefile,field_name, min_value, max_value,output):
'''
remove polygon not in range (min, max]
:param shapefile:
:param field_name:
:param min_value:
:param max_value:
:param output:
:return:
'''
# read polygons as shapely objects
shapefile = gpd.read_file(shapefile)
remove_count = 0
for idx, row in shapefile.iterrows():
# polygon = row['geometry']
# go through post-processing to decide to keep or remove it
if row[field_name] < min_value or row[field_name] >= max_value:
shapefile.drop(idx, inplace=True)
remove_count += 1
if len(shapefile.geometry.values) < 1:
basic.outputlogMessage('remove %d polygons based on %s, remain %d ones, no saved files' %
(remove_count, field_name, len(shapefile.geometry.values)))
return False
else:
basic.outputlogMessage('remove %d polygons based on %s, remain %d ones saving to %s' %
(remove_count, field_name, len(shapefile.geometry.values), output))
# save results
shapefile.to_file(output, driver='ESRI Shapefile')
def remove_polygons_based_values(shapefile,value_list, threshold, bsmaller,output):
'''
remove polygons based on attribute values
:param shapefile:
:param value_list: values for removing polygons, its number should be the same polygon numbers in shapefile
:param threshold:
:param bsmaller:
:param output:
:return:
'''
# read polygons as shapely objects
shapefile = gpd.read_file(shapefile)
org_count = len(shapefile)
# for (idx,row), value in zip(shapefile.iterrows(),value_list):
# if bsmaller:
# if value < threshold:
# shapefile.drop(idx, inplace=True)
# remove_count += 1
# else:
# if value >= threshold:
# shapefile.drop(idx, inplace=True)
# remove_count += 1
value_array = np.array(value_list)
if bsmaller:
# remove small values, so keep the bigger ones
shapefile = shapefile[value_array >= threshold]
else:
# remove small values, so keep the small ones
shapefile = shapefile[value_array < threshold]
remove_count = org_count - len(shapefile)
if len(shapefile.geometry.values) < 1:
basic.outputlogMessage('remove %d polygons based on a list of values, remain %d ones, no saved files' %
(remove_count, len(shapefile.geometry.values)))
return False
else:
basic.outputlogMessage('remove %d polygons, remain %d ones saving to %s' %
(remove_count, len(shapefile.geometry.values), output))
# save results
shapefile.to_file(output, driver='ESRI Shapefile')
def remove_polygons(shapefile,field_name, threshold, bsmaller,output):
'''
remove polygons based on attribute values
:param shapefile:
:param field_name:
:param threshold:
:param bsmaller:
:param output:
:return:
'''
# another version
# operation_obj = shape_opeation()
# if operation_obj.remove_shape_baseon_field_value(shapefile, output, field_name, threshold, smaller=bsmaller) is False:
# return False
# read polygons as shapely objects
shapefile = gpd.read_file(shapefile)
remove_count = 0
for idx,row in shapefile.iterrows():
# polygon = row['geometry']
# go through post-processing to decide to keep or remove it
if bsmaller:
if row[field_name] < threshold:
shapefile.drop(idx, inplace=True)
remove_count += 1
else:
if row[field_name] >= threshold:
shapefile.drop(idx, inplace=True)
remove_count += 1
if len(shapefile.geometry.values) < 1:
basic.outputlogMessage('remove %d polygons based on %s, remain %d ones, no saved files' %
(remove_count, field_name, len(shapefile.geometry.values)))
return False
else:
basic.outputlogMessage('remove %d polygons based on %s, remain %d ones saving to %s' %
(remove_count, field_name, len(shapefile.geometry.values), output))
# save results
shapefile.to_file(output, driver='ESRI Shapefile')
def calculate_polygon_shape_info(polygon_shapely):
'''
calculate the shape information of a polygon, including area, perimeter, circularity,
WIDTH and HEIGHT based on minimum_rotated_rectangle,
:param polygon_shapely: a polygon (shapely object)
:return:
'''
shape_info = {}
shape_info['INarea'] = polygon_shapely.area
shape_info['INperimete'] = polygon_shapely.length
if polygon_shapely.is_empty:
shape_info['circularit'] = 0
else:
# circularity
circularity = (4 * math.pi * polygon_shapely.area / polygon_shapely.length** 2)
shape_info['circularit'] = circularity
if polygon_shapely.is_empty:
shape_info['WIDTH'] = 0
shape_info['HEIGHT'] = 0
shape_info['ratio_w_h'] = 0
else:
minimum_rotated_rectangle = polygon_shapely.minimum_rotated_rectangle
points = list(minimum_rotated_rectangle.boundary.coords)
point1 = Point(points[0])
point2 = Point(points[1])
point3 = Point(points[2])
width = point1.distance(point2)
height = point2.distance(point3)
shape_info['WIDTH'] = width
shape_info['HEIGHT'] = height
if width > height:
shape_info['ratio_w_h'] = height / width
else:
shape_info['ratio_w_h'] = width / height
#added number of holes
if polygon_shapely.geom_type == 'Polygon':
shape_info['hole_count'] = len(list(polygon_shapely.interiors))
else:
polygons = MultiPolygon_to_polygons(0, polygon_shapely)
hole_count = 0
for poly in polygons:
hole_count += len(list(poly.interiors))
shape_info['hole_count'] = hole_count
return shape_info
# convert the list from calculate_polygon_shape_info to a dict for saving to shapefile.
def list_to_dict(list_dict):
out_dict = {}
for dict_obj in list_dict:
for key in dict_obj.keys():
if key in out_dict.keys():
out_dict[key].append(dict_obj[key])
else:
out_dict[key] = [dict_obj[key]]
return out_dict
def save_shapefile_subset_as_valueInlist(org_shp, save_path, field_name, value_list, format='ESRI Shapefile'):
'''
save a subset of vector file based on values in a column (field)
:param org_shp: original shapefile
:param save_path: save path
:param field_name: the file name
:param value_list: a list of values, if the value of the column is in this list, then will save the record
:param format:
:return:
'''
#
shapefile = gpd.read_file(org_shp)
filtered_shapefile = shapefile[shapefile[field_name].isin(value_list)]
# change format
guess_format = guess_file_format_extension(save_path)
if guess_format != format:
basic.outputlogMessage('warning, the format (%s) associated with file extension is different with the input one (%s)'%
(guess_format,format))
format = guess_format
filtered_shapefile.to_file(save_path, driver=format)
basic.outputlogMessage('save subset (%d geometry) of shapefile to %s'%(len(filtered_shapefile),save_path))
def save_shapefile_subset_as(data_poly_indices, org_shp, save_path,format='ESRI Shapefile'):
'''
save subset of shapefile
:param data_poly_indices: polygon index
:param org_shp: orignal shapefile
:param save_path: save path
:return: True if successful
'''
if len(data_poly_indices) < 1:
raise ValueError('no input index')
save_count = len(data_poly_indices)
shapefile = gpd.read_file(org_shp)
# nrow, ncol = shapefile.shape
# selected_list = [False]*nrow
# for idx in data_poly_indices:
# selected_list[idx] = True
# shapefile_sub = shapefile[selected_list]
shapefile_sub = shapefile.iloc[data_poly_indices]
# change format
guess_format = guess_file_format_extension(save_path)
if guess_format != format:
basic.outputlogMessage('warning, the format (%s) associated with file extension is different with the input one (%s)'%
(guess_format,format))
format = guess_format
shapefile_sub.to_file(save_path, driver=format)
basic.outputlogMessage('save subset (%d geometry) of shapefile to %s'%(save_count,save_path))
return True
def save_polygons_to_files(data_frame, geometry_name, wkt_string, save_path,format='ESRI Shapefile'):
'''
:param data_frame: include polygon list and the corresponding attributes
:param geometry_name: dict key for the polgyon in the DataFrame
:param wkt_string: wkt string (projection)
:param save_path: save path
:param format: use ESRI Shapefile or "GPKG" (GeoPackage)
:return:
'''
# data_frame[geometry_name] = data_frame[geometry_name].apply(wkt.loads)
poly_df = gpd.GeoDataFrame(data_frame, geometry=geometry_name)
poly_df.crs = wkt_string # or poly_df.crs = {'init' :'epsg:4326'}
if format=='ESRI Shapefile' and save_path.endswith('.shp') is False:
basic.outputlogMessage('Warning, the extension of the save file is not shp, adding .shp')
save_path = os.path.splitext(save_path)[0] + ".shp"
poly_df.to_file(save_path, driver=format)
return True
def save_lines_to_files(data_frame, geometry_name, wkt_string, save_path,format='ESRI Shapefile'):
return save_polygons_to_files(data_frame, geometry_name, wkt_string, save_path,format=format)
def save_points_to_file(data_frame, geometry_name, wkt_string, save_path,format='ESRI Shapefile'):
return save_polygons_to_files(data_frame, geometry_name, wkt_string, save_path,format=format)
def remove_narrow_parts_of_a_polygon(shapely_polygon, rm_narrow_thr):
'''
try to remove the narrow (or thin) parts of a polygon by using buffer opeartion
:param shapely_polygon: a shapely object, Polygon or MultiPolygon
:param rm_narrow_thr: how narrow
:return: the shapely polygon, multipolygons or polygons
'''
# A positive distance has an effect of dilation; a negative distance, erosion.
# object.buffer(distance, resolution=16, cap_style=1, join_style=1, mitre_limit=5.0)
enlarge_factor = 1.6
# can return multiple polygons
# remain_polygon_parts = shapely_polygon.buffer(-rm_narrow_thr)
# remain_polygon_parts = shapely_polygon.buffer(-rm_narrow_thr).buffer(rm_narrow_thr * enlarge_factor)
remain_polygon_parts = shapely_polygon.buffer(-rm_narrow_thr).buffer(rm_narrow_thr * enlarge_factor).intersection(shapely_polygon)
return remain_polygon_parts
def remove_narrow_parts_of_polygons_shp_NOmultiPolygon(input_shp,out_shp,rm_narrow_thr):
# read polygons as shapely objects
shapefile = gpd.read_file(input_shp)
attribute_names = None
new_polygon_list = []
polygon_attributes_list = [] # 2d list
for idx, row in shapefile.iterrows():
if idx==0:
attribute_names = row.keys().to_list()[:-1] # the last one is 'geometry'
print('removing narrow parts of %dth polygon (total: %d)'%(idx+1,len(shapefile.geometry.values)))
shapely_polygon = row['geometry']
if shapely_polygon.is_valid is False:
shapely_polygon = shapely_polygon.buffer(0.000001)
basic.outputlogMessage('warning, %d th polygon is is_valid, fix it by the buffer operation'%idx)
out_geometry = remove_narrow_parts_of_a_polygon(shapely_polygon, rm_narrow_thr)
# if out_polygon.is_empty is True:
# print(idx, out_polygon)
if out_geometry.is_empty is True:
basic.outputlogMessage('Warning, remove %dth (0 index) polygon in %s because it is empty after removing narrow parts'%
(idx, os.path.basename(input_shp)))
# continue, don't save
# shapefile.drop(idx, inplace=True),
else:
out_polygon_list = MultiPolygon_to_polygons(idx, out_geometry)
if len(out_polygon_list) < 1:
continue
new_polygon_list.extend(out_polygon_list)
attributes = [row[key] for key in attribute_names]
for idx in range(len(out_polygon_list)):
# copy the attributes (Not area and perimeter, etc)
polygon_attributes_list.append(attributes) # last one is 'geometry'
# copy attributes
if len(new_polygon_list) < 1:
basic.outputlogMessage('Warning, no polygons in %s'%input_shp)
return False
save_polyons_attributes = {}
for idx, attribute in enumerate(attribute_names):
# print(idx, attribute)
values = [item[idx] for item in polygon_attributes_list]
save_polyons_attributes[attribute] = values
save_polyons_attributes["Polygons"] = new_polygon_list
polygon_df = pd.DataFrame(save_polyons_attributes)
basic.outputlogMessage('After removing the narrow parts, obtaining %d polygons'%len(new_polygon_list))
print(out_shp, isinstance(out_shp,list))
basic.outputlogMessage('will be saved to %s'%out_shp)
wkt_string = map_projection.get_raster_or_vector_srs_info_wkt(input_shp)
return save_polygons_to_files(polygon_df, 'Polygons', wkt_string, out_shp)
def remove_narrow_parts_of_polygons_shp(input_shp,out_shp,rm_narrow_thr):
# read polygons as shapely objects
shapefile = gpd.read_file(input_shp)
attribute_names = None
new_polygon_list = []
polygon_attributes_list = [] # 2d list
for idx, row in shapefile.iterrows():
if idx==0:
attribute_names = row.keys().to_list()[:-1] # the last one is 'geometry'
print('removing narrow parts of %dth polygon (total: %d)'%(idx+1,len(shapefile.geometry.values)))
shapely_polygon = row['geometry']
out_polygon = remove_narrow_parts_of_a_polygon(shapely_polygon, rm_narrow_thr)
# if out_polygon.is_empty is True:
# print(idx, out_polygon)
if out_polygon.is_empty is True:
basic.outputlogMessage('Warning, remove %dth (0 index) polygon in %s because it is empty after removing narrow parts'%
(idx, os.path.basename(input_shp)))
# continue, don't save
# shapefile.drop(idx, inplace=True),
else:
new_polygon_list.append(out_polygon)
attributes = [row[key] for key in attribute_names]
polygon_attributes_list.append(attributes) # last one is 'geometry'
# copy attributes
save_polyons_attributes = {}
for idx, attribute in enumerate(attribute_names):
# print(idx, attribute)
values = [item[idx] for item in polygon_attributes_list]
save_polyons_attributes[attribute] = values
save_polyons_attributes["Polygons"] = new_polygon_list
polygon_df = pd.DataFrame(save_polyons_attributes)
basic.outputlogMessage('After removing the narrow parts, obtaining %d polygons'%len(new_polygon_list))
print(out_shp, isinstance(out_shp,list))
basic.outputlogMessage('will be saved to %s'%out_shp)
wkt_string = map_projection.get_raster_or_vector_srs_info_wkt(input_shp)
return save_polygons_to_files(polygon_df, 'Polygons', wkt_string, out_shp)
def polygons_to_a_MultiPolygon(polygon_list):
if isinstance(polygon_list,list) is False:
raise ValueError('the input is a not list')
if len(polygon_list) < 1:
raise ValueError('There is no polygon in the input')
return MultiPolygon(polygon_list)
def MultiPolygon_to_polygons(idx, multiPolygon, attributes=None):
''''''
if version.parse(shapely.__version__) >= version.parse("2.0.0"):
geometry_values = multiPolygon.geoms
else:
geometry_values = multiPolygon
if multiPolygon.geom_type == 'GeometryCollection':
polygons = []
# print(multiPolygon)
# geometries = list(multiPolygon)
# print(geometries)
for geometry in geometry_values:
# print(geometry)
if geometry.geom_type == 'Polygon':
polygons.append(geometry)
elif geometry.geom_type == 'MultiPolygon':
polygons.extend(list(geometry))
else:
basic.outputlogMessage("Warning, abandon a %s derived from the %d th polygon "%(geometry.geom_type,idx))
elif multiPolygon.geom_type == 'MultiPolygon':
polygons = list(geometry_values)
elif multiPolygon.geom_type == 'Polygon':
polygons = [multiPolygon]
else:
raise ValueError('Currently, only support Polygon and MultiPolygon, but input is %s' % multiPolygon.geom_type)
# # TODO: calculate new information each polygon
# polygon_attributes_list = [] # 2D list for polygons
# for p_idx, polygon in enumerate(polygons):