-
Notifications
You must be signed in to change notification settings - Fork 0
/
kml_to_shp.py
303 lines (250 loc) · 10.3 KB
/
kml_to_shp.py
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
import pandas as pd
from io import BytesIO,StringIO
from zipfile import ZipFile
import re,os
import numpy as np
import xml.sax, xml.sax.handler
from html.parser import HTMLParser
import pandas as pd
from html.parser import HTMLParser
class MyHTMLParser(HTMLParser):
def __init__(self):
# initialize the base class
HTMLParser.__init__(self)
self.inTable=False
self.mapping = {}
self.buffer = ""
self.name_tag = ""
self.series = pd.Series()
def handle_starttag(self, tag, attrs):
if tag == 'table':
self.inTable = True
def handle_data(self, data):
if self.inTable:
self.buffer = data.strip(' \n\t').split(':')
if len(self.buffer)==2:
self.mapping[self.buffer[0]]=self.buffer[1]
self.series = pd.Series(self.mapping)
class PlacemarkHandler(xml.sax.handler.ContentHandler):
def __init__(self):
self.inName = False # handle XML parser events
self.inPlacemark = False
self.mapping = {}
self.buffer = ""
self.name_tag = ""
def startElement(self, name, attributes):
if name == "Placemark": # on start Placemark tag
self.inPlacemark = True
self.buffer = ""
if self.inPlacemark:
if name == "name": # on start title tag
self.inName = True # save name text to follow
def characters(self, data):
if self.inPlacemark: # on text within tag
self.buffer += data # save text if in title
def endElement(self, name):
self.buffer = self.buffer.strip('\n\t')
if name == "Placemark":
self.inPlacemark = False
self.name_tag = "" #clear current name
elif name == "name" and self.inPlacemark:
self.inName = False # on end title tag
self.name_tag = self.buffer.strip()
self.mapping[self.name_tag] = {}
elif self.inPlacemark:
if name in self.mapping[self.name_tag]:
self.mapping[self.name_tag][name] += self.buffer
else:
self.mapping[self.name_tag][name] = self.buffer
self.buffer = ""
def spatializer(row):
"""
Function to convert string objects to Python spatial objects
"""
#############################
# coordinates field
#############################
try:
# look for the coordinates column
data = row['coordinates'].strip(' \t\n\r')
except:
pass
try:
import shapely
from shapely.geometry import Polygon,LineString,Point
except ImportError as e:
raise ImportError('This operation requires shapely. {0}'.format(e))
import ast
lsp = data.strip().split(' ')
linestring = map(lambda x: ast.literal_eval(x),lsp)
try:
spatial = Polygon(LineString(linestring))
convertedpoly = pd.Series({'geometry':spatial})
return convertedpoly
except:
try:
g = ast.literal_eval(data)
points = pd.Series({'geometry':Point(g[:2]),
'altitude':g[-1]})
return points
except:
pass
try:
# Test for latitude and longitude columns
lat=float(row['latitude'])
lon=float(row['longitude'])
point = Point(lon,lat)
convertedpoly = pd.Series({'geometry':point})
return convertedpoly
except:
pass
def htmlizer(row):
htmlparser = MyHTMLParser()
htmlparser.feed(row['description'])
return htmlparser.series
def keyholemarkup2x(file,output='df'):
"""
Takes Keyhole Markup Language Zipped (KMZ) or KML file as input. The
output is a pandas dataframe, geopandas geodataframe, csv, geojson, or
shapefile.
All core functionality from:
http://programmingadvent.blogspot.com/2013/06/kmzkml-file-parsing-with-python.html
Parameters
----------
file : {string}
The string path to your KMZ or .
output : {string}
Defines the type of output. Valid selections include:
- shapefile - 'shp', 'shapefile', or 'ESRI Shapefile'
Returns
-------
self : object
"""
r = re.compile(r'(?<=\.)km+[lz]?',re.I)
try:
extension = r.search(file).group(0) #(re.findall(r'(?<=\.)[\w]+',file))[-1]
except IOError as e:
logging.error("I/O error {0}".format(e))
if (extension.lower()=='kml') is True:
buffer = file
elif (extension.lower()=='kmz') is True:
kmz = ZipFile(file, 'r')
vmatch = np.vectorize(lambda x:bool(r.search(x)))
A = np.array(kmz.namelist())
sel = vmatch(A)
buffer = kmz.open(A[sel][0],'r')
else:
raise ValueError('Incorrect file format entered. Please provide the '
'path to a valid KML or KMZ file.')
parser = xml.sax.make_parser()
handler = PlacemarkHandler()
parser.setContentHandler(handler)
parser.parse(buffer)
try:
kmz.close()
except:
pass
df = pd.DataFrame(handler.mapping).T
names = list(map(lambda x: x.lower(),df.columns))
if 'description' in names:
extradata = df.apply(PlacemarkHandler.htmlizer,axis=1)
df = df.join(extradata)
output = output.lower()
if output=='df' or output=='dataframe' or output == None:
result = df
elif output=='csv':
out_filename = file[:-3] + "csv"
df.to_csv(out_filename,encoding='utf-8',sep="\t")
result = ("Successfully converted {0} to CSV and output to"
" disk at {1}".format(file,out_filename))
elif output=='gpd' or output == 'gdf' or output=='geoframe' or output == 'geodataframe':
try:
import shapely
from shapely.geometry import Polygon,LineString,Point
except ImportError as e:
raise ImportError('This operation requires shapely. {0}'.format(e))
try:
import fiona
except ImportError as e:
raise ImportError('This operation requires fiona. {0}'.format(e))
try:
import geopandas as gpd
except ImportError as e:
raise ImportError('This operation requires geopandas. {0}'.format(e))
geos = gpd.GeoDataFrame(df.apply(PlacemarkHandler.spatializer,axis=1))
result = gpd.GeoDataFrame(pd.concat([df,geos],axis=1))
elif output=='geojson' or output=='json':
try:
import shapely
from shapely.geometry import Polygon,LineString,Point
except ImportError as e:
raise ImportError('This operation requires shapely. {0}'.format(e))
try:
import fiona
except ImportError as e:
raise ImportError('This operation requires fiona. {0}'.format(e))
try:
import geopandas as gpd
except ImportError as e:
raise ImportError('This operation requires geopandas. {0}'.format(e))
try:
import geojson
except ImportError as e:
raise ImportError('This operation requires geojson. {0}'.format(e))
geos = gpd.GeoDataFrame(df.apply(PlacemarkHandler.spatializer,axis=1))
gdf = gpd.GeoDataFrame(pd.concat([df,geos],axis=1))
out_filename = file[:-3] + "geojson"
gdf.to_file(out_filename,driver='GeoJSON')
validation = geojson.is_valid(geojson.load(open(out_filename)))['valid']
if validation == 'yes':
result = ("Successfully converted {0} to GeoJSON and output to"
" disk at {1}".format(file,out_filename))
else:
raise ValueError('The geojson conversion did not create a '
'valid geojson object. Try to clean your '
'data or try another file.')
elif output=='shapefile' or output=='shp' or output =='esri shapefile':
try:
import shapely
from shapely.geometry import Polygon,LineString,Point
except ImportError as e:
raise ImportError('This operation requires shapely. {0}'.format(e))
try:
import fiona
except ImportError as e:
raise ImportError('This operation requires fiona. {0}'.format(e))
try:
import geopandas as gpd
except ImportError as e:
raise ImportError('This operation requires geopandas. {0}'.format(e))
try:
import shapefile
except ImportError as e:
raise ImportError('This operation requires pyshp. {0}'.format(e))
geos = gpd.GeoDataFrame(df.apply(PlacemarkHandler.spatializer,axis=1))
gdf = gpd.GeoDataFrame(pd.concat([df,geos],axis=1))
out_filename = file[:-3] + "shp"
gdf.to_file(out_filename,driver='ESRI Shapefile')
sf = shapefile.Reader(out_filename)
import shapefile
sf = shapefile.Reader(out_filename)
if len(sf.shapes())>0:
validation = "yes"
else:
validation = "no"
if validation == 'yes':
result = ("Successfully converted {0} to Shapefile and output to"
" disk at {1}".format(file,out_filename))
else:
raise ValueError('The Shapefile conversion did not create a '
'valid shapefile object. Try to clean your '
'data or try another file.')
else:
raise ValueError('The conversion returned no data; check if'
' you entered a correct output file type. '
'Valid output types are geojson, shapefile,'
' csv, geodataframe, and/or pandas dataframe.')
return result
# convert to shapefile
a = keyholemarkup2x('shapefiles/midc.kml',output='shp')
print("Successfully Done")