cht_utils 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cht_utils/__init__.py +28 -0
- cht_utils/cog/__init__.py +6 -0
- cht_utils/cog/geotiff_to_cog.py +79 -0
- cht_utils/cog/netcdf_to_cog.py +85 -0
- cht_utils/cog/xyz_to_cog.py +86 -0
- cht_utils/colors/__init__.py +6 -0
- cht_utils/colors/colors.py +117 -0
- cht_utils/fileio/__init__.py +21 -0
- cht_utils/fileio/deltares_ini.py +326 -0
- cht_utils/fileio/json_js.py +72 -0
- cht_utils/fileio/pli_file.py +233 -0
- cht_utils/fileio/tekal.py +234 -0
- cht_utils/fileio/xml.py +184 -0
- cht_utils/fileio/yaml.py +39 -0
- cht_utils/fileops/__init__.py +25 -0
- cht_utils/fileops/fileops.py +344 -0
- cht_utils/interpolation/__init__.py +5 -0
- cht_utils/interpolation/interpolation.py +152 -0
- cht_utils/maps/__init__.py +2 -0
- cht_utils/maps/fileops.py +191 -0
- cht_utils/maps/flood_map.py +1231 -0
- cht_utils/maps/topobathy_map.py +463 -0
- cht_utils/maps/utils.py +700 -0
- cht_utils/physics/__init__.py +8 -0
- cht_utils/physics/deshoal.py +63 -0
- cht_utils/physics/disper.py +91 -0
- cht_utils/physics/runup_vo21.py +229 -0
- cht_utils/physics/waves.py +59 -0
- cht_utils/probabilistic/__init__.py +5 -0
- cht_utils/probabilistic/prob_maps.py +263 -0
- cht_utils/remote/__init__.py +4 -0
- cht_utils/remote/s3.py +380 -0
- cht_utils/remote/sftp.py +192 -0
- cht_utils-2.0.0.dist-info/METADATA +30 -0
- cht_utils-2.0.0.dist-info/RECORD +39 -0
- cht_utils-2.0.0.dist-info/WHEEL +5 -0
- cht_utils-2.0.0.dist-info/licenses/LICENSE +21 -0
- cht_utils-2.0.0.dist-info/top_level.txt +1 -0
- cht_utils-2.0.0.dist-info/zip-safe +1 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"""Reader and writer for Deltares-style INI configuration files.
|
|
2
|
+
|
|
3
|
+
These files consist of ``[Section]`` blocks containing keyword-value pairs
|
|
4
|
+
and optional data tables.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import datetime
|
|
8
|
+
import re
|
|
9
|
+
from typing import Any, List, Optional
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
import pandas as pd
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Section:
|
|
16
|
+
"""A single section within an INI file.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
name : str or None
|
|
21
|
+
Section name (text between square brackets).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, name: Optional[str] = None) -> None:
|
|
25
|
+
self.name: Optional[str] = name
|
|
26
|
+
self.keyword: List["Keyword"] = []
|
|
27
|
+
self.data: Optional[pd.DataFrame] = None
|
|
28
|
+
|
|
29
|
+
def get_value(self, keyword: str) -> Optional[Any]:
|
|
30
|
+
"""Return the value of a keyword in this section.
|
|
31
|
+
|
|
32
|
+
Parameters
|
|
33
|
+
----------
|
|
34
|
+
keyword : str
|
|
35
|
+
Keyword name (case-insensitive).
|
|
36
|
+
|
|
37
|
+
Returns
|
|
38
|
+
-------
|
|
39
|
+
Any or None
|
|
40
|
+
The keyword value, or ``None`` if not found.
|
|
41
|
+
"""
|
|
42
|
+
for kw in self.keyword:
|
|
43
|
+
if kw.name.lower() == keyword.lower():
|
|
44
|
+
return kw.value
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Keyword:
|
|
49
|
+
"""A key-value pair within an INI section.
|
|
50
|
+
|
|
51
|
+
Parameters
|
|
52
|
+
----------
|
|
53
|
+
name : str or None
|
|
54
|
+
Keyword name.
|
|
55
|
+
value : Any
|
|
56
|
+
Keyword value.
|
|
57
|
+
comment : str or None
|
|
58
|
+
Inline comment.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
name: Optional[str] = None,
|
|
64
|
+
value: Any = None,
|
|
65
|
+
comment: Optional[str] = None,
|
|
66
|
+
) -> None:
|
|
67
|
+
self.name = name
|
|
68
|
+
self.value = value
|
|
69
|
+
self.comment = comment
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class IniStruct:
|
|
73
|
+
"""Deltares INI file parser and writer.
|
|
74
|
+
|
|
75
|
+
Parameters
|
|
76
|
+
----------
|
|
77
|
+
filename : str or None
|
|
78
|
+
If provided, the file is read immediately on construction.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(self, filename: Optional[str] = None) -> None:
|
|
82
|
+
self.section: List[Section] = []
|
|
83
|
+
if filename:
|
|
84
|
+
self.read(filename)
|
|
85
|
+
|
|
86
|
+
def read(self, filename: str) -> None:
|
|
87
|
+
"""Parse an INI file into sections, keywords, and data tables.
|
|
88
|
+
|
|
89
|
+
Parameters
|
|
90
|
+
----------
|
|
91
|
+
filename : str
|
|
92
|
+
Path to the INI file.
|
|
93
|
+
"""
|
|
94
|
+
self.section = []
|
|
95
|
+
istart: List[int] = []
|
|
96
|
+
|
|
97
|
+
with open(filename, "r") as fid:
|
|
98
|
+
lines = fid.readlines()
|
|
99
|
+
|
|
100
|
+
# Find section starts
|
|
101
|
+
for i, line in enumerate(lines):
|
|
102
|
+
ll = line.strip()
|
|
103
|
+
if len(ll) == 0:
|
|
104
|
+
continue
|
|
105
|
+
if ll[0] == "[" and ll[-1] == "]":
|
|
106
|
+
sec = Section(name=ll[1:-1])
|
|
107
|
+
istart.append(i)
|
|
108
|
+
self.section.append(sec)
|
|
109
|
+
|
|
110
|
+
# Parse each section
|
|
111
|
+
for isec in range(len(self.section)):
|
|
112
|
+
i1 = istart[isec] + 1
|
|
113
|
+
i2 = istart[isec + 1] - 1 if isec < len(self.section) - 1 else len(lines)
|
|
114
|
+
|
|
115
|
+
df = pd.DataFrame()
|
|
116
|
+
|
|
117
|
+
for iline in range(i1, i2):
|
|
118
|
+
ll = lines[iline].strip()
|
|
119
|
+
if len(ll) == 0 or ll[0] == "#":
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
if "=" in ll:
|
|
123
|
+
key = Keyword()
|
|
124
|
+
|
|
125
|
+
# Handle embedded # in values (e.g. hex colours)
|
|
126
|
+
if "#" in ll:
|
|
127
|
+
ipos = [m.start() for m in re.finditer("#", ll)]
|
|
128
|
+
if len(ipos) > 1:
|
|
129
|
+
ll = (
|
|
130
|
+
ll[: ipos[0]]
|
|
131
|
+
+ ll[ipos[0] + 1 : ipos[1]]
|
|
132
|
+
+ ll[ipos[1] + 1 :]
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
if "#" in ll:
|
|
136
|
+
j = ll.index("#")
|
|
137
|
+
key.comment = ll[j + 1 :].strip()
|
|
138
|
+
ll = ll[:j].strip()
|
|
139
|
+
|
|
140
|
+
tx = ll.split("=")
|
|
141
|
+
key.name = tx[0].strip()
|
|
142
|
+
key.value = tx[1].strip()
|
|
143
|
+
self.section[isec].keyword.append(key)
|
|
144
|
+
else:
|
|
145
|
+
a_list = ll.split()
|
|
146
|
+
values = []
|
|
147
|
+
for item in a_list:
|
|
148
|
+
try:
|
|
149
|
+
values.append(float(item))
|
|
150
|
+
except ValueError:
|
|
151
|
+
values.append(item)
|
|
152
|
+
df = pd.concat([df, pd.Series(values)], axis=1)
|
|
153
|
+
|
|
154
|
+
if not df.empty:
|
|
155
|
+
df = df.transpose().set_index([0])
|
|
156
|
+
self.section[isec].data = df
|
|
157
|
+
|
|
158
|
+
def write(self, file_name: str) -> None:
|
|
159
|
+
"""Write the INI structure back to a file.
|
|
160
|
+
|
|
161
|
+
Parameters
|
|
162
|
+
----------
|
|
163
|
+
file_name : str
|
|
164
|
+
Output file path.
|
|
165
|
+
"""
|
|
166
|
+
try:
|
|
167
|
+
fid = open(file_name, "w")
|
|
168
|
+
except OSError:
|
|
169
|
+
print(f"Warning! Could not create file: {file_name}")
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
for section in self.section:
|
|
174
|
+
fid.write(f"[{section.name}]\n")
|
|
175
|
+
|
|
176
|
+
for kw in section.keyword:
|
|
177
|
+
valstr = ""
|
|
178
|
+
if kw.value is not None:
|
|
179
|
+
if isinstance(kw.value, (float, int)):
|
|
180
|
+
valstr = str(kw.value)
|
|
181
|
+
elif isinstance(kw.value, datetime.date):
|
|
182
|
+
valstr = kw.value.strftime("%Y%m%d")
|
|
183
|
+
else:
|
|
184
|
+
valstr = kw.value
|
|
185
|
+
|
|
186
|
+
kwstr = f" {kw.name:<20s} = "
|
|
187
|
+
valstr = f"{valstr:<30s}"
|
|
188
|
+
comstr = f" # {kw.comment}" if kw.comment else ""
|
|
189
|
+
fid.write(f"{kwstr}{valstr}{comstr}\n")
|
|
190
|
+
|
|
191
|
+
if section.data is not None:
|
|
192
|
+
tt = section.data.index.to_numpy()
|
|
193
|
+
vv = section.data.array.to_numpy()
|
|
194
|
+
for it, v in enumerate(vv):
|
|
195
|
+
if np.isnan(v):
|
|
196
|
+
v = -999.0
|
|
197
|
+
fid.write(f"{tt[it]:12.1f}{v:12.3f}\n")
|
|
198
|
+
|
|
199
|
+
fid.write("\n")
|
|
200
|
+
finally:
|
|
201
|
+
fid.close()
|
|
202
|
+
|
|
203
|
+
def get_value(self, section_name: str, keyword_name: str) -> Optional[Any]:
|
|
204
|
+
"""Get a keyword value from a named section.
|
|
205
|
+
|
|
206
|
+
Parameters
|
|
207
|
+
----------
|
|
208
|
+
section_name : str
|
|
209
|
+
Section name.
|
|
210
|
+
keyword_name : str
|
|
211
|
+
Keyword name.
|
|
212
|
+
|
|
213
|
+
Returns
|
|
214
|
+
-------
|
|
215
|
+
Any or None
|
|
216
|
+
The keyword value, or ``None`` if not found.
|
|
217
|
+
"""
|
|
218
|
+
for section in self.section:
|
|
219
|
+
if section.name == section_name:
|
|
220
|
+
for kw in section.keyword:
|
|
221
|
+
if kw.name == keyword_name:
|
|
222
|
+
return kw.value
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
def set_value(
|
|
226
|
+
self,
|
|
227
|
+
section_name: str,
|
|
228
|
+
keyword_name: str,
|
|
229
|
+
keyword_value: Any,
|
|
230
|
+
keyword_comment: Optional[str] = None,
|
|
231
|
+
) -> None:
|
|
232
|
+
"""Set a keyword value, creating the section or keyword if needed.
|
|
233
|
+
|
|
234
|
+
Parameters
|
|
235
|
+
----------
|
|
236
|
+
section_name : str
|
|
237
|
+
Section name.
|
|
238
|
+
keyword_name : str
|
|
239
|
+
Keyword name.
|
|
240
|
+
keyword_value : Any
|
|
241
|
+
New value.
|
|
242
|
+
keyword_comment : str or None
|
|
243
|
+
Optional inline comment.
|
|
244
|
+
"""
|
|
245
|
+
for section in self.section:
|
|
246
|
+
if section.name == section_name:
|
|
247
|
+
for kw in section.keyword:
|
|
248
|
+
if kw.name == keyword_name:
|
|
249
|
+
kw.value = keyword_value
|
|
250
|
+
return
|
|
251
|
+
kw = Keyword(keyword_name, keyword_value, keyword_comment)
|
|
252
|
+
section.keyword.append(kw)
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
kw = Keyword(keyword_name, keyword_value, keyword_comment)
|
|
256
|
+
section = Section(section_name)
|
|
257
|
+
section.keyword.append(kw)
|
|
258
|
+
self.section.append(section)
|
|
259
|
+
|
|
260
|
+
def get_data(
|
|
261
|
+
self,
|
|
262
|
+
section_name: str,
|
|
263
|
+
keyword_list: Optional[List[str]] = None,
|
|
264
|
+
value_list: Optional[List[str]] = None,
|
|
265
|
+
) -> Optional[pd.DataFrame]:
|
|
266
|
+
"""Return the data table from a matching section.
|
|
267
|
+
|
|
268
|
+
Parameters
|
|
269
|
+
----------
|
|
270
|
+
section_name : str
|
|
271
|
+
Section name.
|
|
272
|
+
keyword_list : List[str] or None
|
|
273
|
+
Keywords to match for disambiguation.
|
|
274
|
+
value_list : List[str] or None
|
|
275
|
+
Values that the keywords must have.
|
|
276
|
+
|
|
277
|
+
Returns
|
|
278
|
+
-------
|
|
279
|
+
pd.DataFrame or None
|
|
280
|
+
"""
|
|
281
|
+
for section in self.section:
|
|
282
|
+
if section.name == section_name:
|
|
283
|
+
if keyword_list and value_list:
|
|
284
|
+
ok = True
|
|
285
|
+
for key, val in zip(keyword_list, value_list):
|
|
286
|
+
for kw in section.keyword:
|
|
287
|
+
if kw.name == key and kw.value != val:
|
|
288
|
+
ok = False
|
|
289
|
+
if not ok:
|
|
290
|
+
continue
|
|
291
|
+
return section.data
|
|
292
|
+
return None
|
|
293
|
+
|
|
294
|
+
def get_section(
|
|
295
|
+
self,
|
|
296
|
+
section_name: str,
|
|
297
|
+
keyword_list: Optional[List[str]] = None,
|
|
298
|
+
value_list: Optional[List[str]] = None,
|
|
299
|
+
) -> Optional[Section]:
|
|
300
|
+
"""Return the first section matching the given name and filters.
|
|
301
|
+
|
|
302
|
+
Parameters
|
|
303
|
+
----------
|
|
304
|
+
section_name : str
|
|
305
|
+
Section name.
|
|
306
|
+
keyword_list : List[str] or None
|
|
307
|
+
Keywords to match for disambiguation.
|
|
308
|
+
value_list : List[str] or None
|
|
309
|
+
Values that the keywords must have.
|
|
310
|
+
|
|
311
|
+
Returns
|
|
312
|
+
-------
|
|
313
|
+
Section or None
|
|
314
|
+
"""
|
|
315
|
+
for section in self.section:
|
|
316
|
+
if section.name == section_name:
|
|
317
|
+
if keyword_list and value_list:
|
|
318
|
+
ok = True
|
|
319
|
+
for key, val in zip(keyword_list, value_list):
|
|
320
|
+
for kw in section.keyword:
|
|
321
|
+
if kw.name == key and kw.value != val:
|
|
322
|
+
ok = False
|
|
323
|
+
if not ok:
|
|
324
|
+
continue
|
|
325
|
+
return section
|
|
326
|
+
return None
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""I/O for JavaScript-wrapped JSON and CSV files.
|
|
2
|
+
|
|
3
|
+
These formats prepend a JavaScript variable declaration line before the actual
|
|
4
|
+
JSON or CSV payload.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
from typing import Any, Union
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def read_json_js(file_name: str) -> Any:
|
|
13
|
+
"""Read a JavaScript-wrapped JSON file (skips the first line).
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
file_name : str
|
|
18
|
+
Input file path.
|
|
19
|
+
|
|
20
|
+
Returns
|
|
21
|
+
-------
|
|
22
|
+
Any
|
|
23
|
+
Parsed JSON object.
|
|
24
|
+
"""
|
|
25
|
+
with open(file_name, "r") as f:
|
|
26
|
+
lines = f.readlines()
|
|
27
|
+
jsn_string = "".join(line.strip() for line in lines[1:])
|
|
28
|
+
return json.loads(jsn_string)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def write_json_js(file_name: str, jsn: Union[list, dict], first_line: str) -> None:
|
|
32
|
+
"""Write a JSON object to a JavaScript-wrapped file.
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
file_name : str
|
|
37
|
+
Output file path.
|
|
38
|
+
jsn : list or dict
|
|
39
|
+
JSON-serializable data.
|
|
40
|
+
first_line : str
|
|
41
|
+
JavaScript variable declaration line (e.g. ``"var data ="``)
|
|
42
|
+
"""
|
|
43
|
+
with open(file_name, "w") as f:
|
|
44
|
+
f.write(first_line + "\n")
|
|
45
|
+
if isinstance(jsn, list):
|
|
46
|
+
f.write("[\n")
|
|
47
|
+
for ix, item in enumerate(jsn):
|
|
48
|
+
sep = "," if ix < len(jsn) - 1 else ""
|
|
49
|
+
f.write(json.dumps(item) + sep + "\n")
|
|
50
|
+
f.write("]\n")
|
|
51
|
+
else:
|
|
52
|
+
f.write(json.dumps(jsn) + "\n")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def write_csv_js(file_name: str, csv_string: str, first_line: str) -> None:
|
|
56
|
+
"""Write a CSV string to a JavaScript-wrapped file.
|
|
57
|
+
|
|
58
|
+
Parameters
|
|
59
|
+
----------
|
|
60
|
+
file_name : str
|
|
61
|
+
Output file path.
|
|
62
|
+
csv_string : str
|
|
63
|
+
CSV content.
|
|
64
|
+
first_line : str
|
|
65
|
+
JavaScript variable declaration line.
|
|
66
|
+
"""
|
|
67
|
+
os.makedirs(os.path.dirname(file_name), exist_ok=True)
|
|
68
|
+
csv_string = csv_string.replace(chr(13), "")
|
|
69
|
+
with open(file_name, "w") as f:
|
|
70
|
+
f.write(first_line + "\n")
|
|
71
|
+
f.write(csv_string)
|
|
72
|
+
f.write("`;")
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""Read and write Deltares PLI/POL files (polyline and polygon data).
|
|
2
|
+
|
|
3
|
+
PLI files store polylines; POL files store polygons. Both use the Tekal
|
|
4
|
+
block format internally.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import geopandas as gpd
|
|
10
|
+
import pandas as pd
|
|
11
|
+
import shapely.geometry
|
|
12
|
+
|
|
13
|
+
import cht_utils.fileio.tekal as tek
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Polyline:
|
|
17
|
+
"""Simple polyline wrapper with x/y coordinate arrays.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
x : array-like
|
|
22
|
+
X-coordinates.
|
|
23
|
+
y : array-like
|
|
24
|
+
Y-coordinates.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, x, y) -> None:
|
|
28
|
+
self.x = x
|
|
29
|
+
self.y = y
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def read_pli_file(file_name: str) -> list:
|
|
33
|
+
"""Read all polylines from a PLI file.
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
file_name : str
|
|
38
|
+
Path to the PLI file.
|
|
39
|
+
|
|
40
|
+
Returns
|
|
41
|
+
-------
|
|
42
|
+
list of Polyline
|
|
43
|
+
List of polyline objects.
|
|
44
|
+
"""
|
|
45
|
+
polylines = []
|
|
46
|
+
D = tek.tekal(file_name)
|
|
47
|
+
D.info()
|
|
48
|
+
for j in range(len(D.blocks)):
|
|
49
|
+
m = D.read(j)
|
|
50
|
+
polylines.append(Polyline(m[0, :, 0], m[1, :, 0]))
|
|
51
|
+
return polylines
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def pli2gdf(
|
|
55
|
+
file_name: str,
|
|
56
|
+
crs: Optional[int] = None,
|
|
57
|
+
name_string: str = "name",
|
|
58
|
+
) -> gpd.GeoDataFrame:
|
|
59
|
+
"""Convert a PLI file to a GeoDataFrame of LineStrings.
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
file_name : str
|
|
64
|
+
Path to the PLI file.
|
|
65
|
+
crs : int or None
|
|
66
|
+
Coordinate reference system (EPSG code).
|
|
67
|
+
name_string : str
|
|
68
|
+
Column name for the polyline name attribute.
|
|
69
|
+
|
|
70
|
+
Returns
|
|
71
|
+
-------
|
|
72
|
+
gpd.GeoDataFrame
|
|
73
|
+
"""
|
|
74
|
+
D = tek.tekal(file_name)
|
|
75
|
+
D.info()
|
|
76
|
+
gdf_list = []
|
|
77
|
+
for j in range(len(D.blocks)):
|
|
78
|
+
name = D.blocks[j].name
|
|
79
|
+
if isinstance(name, bytes):
|
|
80
|
+
name = name.decode("utf-8")
|
|
81
|
+
m = D.read(j)
|
|
82
|
+
x, y = m[0, :, 0], m[1, :, 0]
|
|
83
|
+
line = shapely.geometry.LineString(list(zip(x, y)))
|
|
84
|
+
gdf_list.append({name_string: name, "geometry": line})
|
|
85
|
+
return gpd.GeoDataFrame(gdf_list, crs=crs)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def pli2geojson(
|
|
89
|
+
file_name_in: str,
|
|
90
|
+
file_name_out: str,
|
|
91
|
+
crs: Optional[int] = None,
|
|
92
|
+
) -> None:
|
|
93
|
+
"""Convert a PLI file to GeoJSON.
|
|
94
|
+
|
|
95
|
+
Parameters
|
|
96
|
+
----------
|
|
97
|
+
file_name_in : str
|
|
98
|
+
Input PLI file path.
|
|
99
|
+
file_name_out : str
|
|
100
|
+
Output GeoJSON file path.
|
|
101
|
+
crs : int or None
|
|
102
|
+
Coordinate reference system (EPSG code).
|
|
103
|
+
"""
|
|
104
|
+
gdf = pli2gdf(file_name_in, crs=crs)
|
|
105
|
+
gdf.to_file(file_name_out, driver="GeoJSON")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def pol2gdf(
|
|
109
|
+
file_name: str,
|
|
110
|
+
crs: Optional[int] = None,
|
|
111
|
+
header: bool = True,
|
|
112
|
+
) -> gpd.GeoDataFrame:
|
|
113
|
+
"""Convert a POL file to a GeoDataFrame of Polygons.
|
|
114
|
+
|
|
115
|
+
Parameters
|
|
116
|
+
----------
|
|
117
|
+
file_name : str
|
|
118
|
+
Path to the POL file.
|
|
119
|
+
crs : int or None
|
|
120
|
+
Coordinate reference system (EPSG code).
|
|
121
|
+
header : bool
|
|
122
|
+
If ``False``, read as plain whitespace-separated x/y columns.
|
|
123
|
+
|
|
124
|
+
Returns
|
|
125
|
+
-------
|
|
126
|
+
gpd.GeoDataFrame
|
|
127
|
+
"""
|
|
128
|
+
gdf_list = []
|
|
129
|
+
if not header:
|
|
130
|
+
df = pd.read_csv(
|
|
131
|
+
file_name,
|
|
132
|
+
index_col=False,
|
|
133
|
+
header=None,
|
|
134
|
+
delim_whitespace=True,
|
|
135
|
+
names=["x", "y"],
|
|
136
|
+
)
|
|
137
|
+
poly = shapely.geometry.Polygon(list(zip(df.x.values, df.y.values)))
|
|
138
|
+
gdf_list.append({"geometry": poly})
|
|
139
|
+
else:
|
|
140
|
+
D = tek.tekal(file_name)
|
|
141
|
+
D.info()
|
|
142
|
+
for j in range(len(D.blocks)):
|
|
143
|
+
m = D.read(j)
|
|
144
|
+
x, y = m[0, :, 0], m[1, :, 0]
|
|
145
|
+
poly = shapely.geometry.Polygon(list(zip(x, y)))
|
|
146
|
+
gdf_list.append({"geometry": poly})
|
|
147
|
+
return gpd.GeoDataFrame(gdf_list, crs=crs)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def pol2geojson(
|
|
151
|
+
file_name_in: str,
|
|
152
|
+
file_name_out: str,
|
|
153
|
+
crs: Optional[int] = None,
|
|
154
|
+
) -> None:
|
|
155
|
+
"""Convert a POL file to GeoJSON.
|
|
156
|
+
|
|
157
|
+
Parameters
|
|
158
|
+
----------
|
|
159
|
+
file_name_in : str
|
|
160
|
+
Input POL file path.
|
|
161
|
+
file_name_out : str
|
|
162
|
+
Output GeoJSON file path.
|
|
163
|
+
crs : int or None
|
|
164
|
+
Coordinate reference system (EPSG code).
|
|
165
|
+
"""
|
|
166
|
+
gdf = pol2gdf(file_name_in, crs=crs)
|
|
167
|
+
gdf.to_file(file_name_out, driver="GeoJSON")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def gdf2pli(
|
|
171
|
+
gdf: gpd.GeoDataFrame,
|
|
172
|
+
file_name: str,
|
|
173
|
+
header: bool = True,
|
|
174
|
+
name_string: str = "name",
|
|
175
|
+
add_point_name: bool = False,
|
|
176
|
+
) -> None:
|
|
177
|
+
"""Write a GeoDataFrame of LineStrings to a PLI file.
|
|
178
|
+
|
|
179
|
+
Parameters
|
|
180
|
+
----------
|
|
181
|
+
gdf : gpd.GeoDataFrame
|
|
182
|
+
GeoDataFrame with LineString geometries.
|
|
183
|
+
file_name : str
|
|
184
|
+
Output PLI file path.
|
|
185
|
+
header : bool
|
|
186
|
+
Write Tekal block headers.
|
|
187
|
+
name_string : str
|
|
188
|
+
Column containing the polyline name.
|
|
189
|
+
add_point_name : bool
|
|
190
|
+
Append vertex names (``NAME_0001``, etc.) to each coordinate line.
|
|
191
|
+
"""
|
|
192
|
+
fmt = "{:12.6f}" if gdf.crs.is_geographic else "{:12.1f}"
|
|
193
|
+
|
|
194
|
+
with open(file_name, "w") as fid:
|
|
195
|
+
for index, row in gdf.iterrows():
|
|
196
|
+
coords = list(row["geometry"].coords)
|
|
197
|
+
nrp = len(coords)
|
|
198
|
+
if header:
|
|
199
|
+
hdr = row.get(name_string, f"BL{index + 1:04d}")
|
|
200
|
+
fid.write(f"{hdr}\n{nrp} 2\n")
|
|
201
|
+
for ip, (x, y) in enumerate(coords):
|
|
202
|
+
line = fmt.format(x) + fmt.format(y)
|
|
203
|
+
if add_point_name:
|
|
204
|
+
line += f" {hdr}_{ip + 1:04d}"
|
|
205
|
+
fid.write(line + "\n")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def gdf2pol(
|
|
209
|
+
gdf: gpd.GeoDataFrame,
|
|
210
|
+
file_name: str,
|
|
211
|
+
header: bool = True,
|
|
212
|
+
) -> None:
|
|
213
|
+
"""Write a GeoDataFrame of Polygons to a POL file.
|
|
214
|
+
|
|
215
|
+
Parameters
|
|
216
|
+
----------
|
|
217
|
+
gdf : gpd.GeoDataFrame
|
|
218
|
+
GeoDataFrame with Polygon geometries.
|
|
219
|
+
file_name : str
|
|
220
|
+
Output POL file path.
|
|
221
|
+
header : bool
|
|
222
|
+
Write Tekal block headers.
|
|
223
|
+
"""
|
|
224
|
+
fmt = "{:12.6f}" if gdf.crs.is_geographic else "{:12.1f}"
|
|
225
|
+
|
|
226
|
+
with open(file_name, "w") as fid:
|
|
227
|
+
for index, row in gdf.iterrows():
|
|
228
|
+
coords = list(row["geometry"].exterior.coords)
|
|
229
|
+
nrp = len(coords)
|
|
230
|
+
if header:
|
|
231
|
+
fid.write(f"BL{index + 1:04d}\n{nrp} 2\n")
|
|
232
|
+
for x, y in coords:
|
|
233
|
+
fid.write(fmt.format(x) + fmt.format(y) + "\n")
|