sigwx-parser 0.1.0__tar.gz
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.
- sigwx_parser-0.1.0/PKG-INFO +11 -0
- sigwx_parser-0.1.0/README.md +0 -0
- sigwx_parser-0.1.0/pyproject.toml +21 -0
- sigwx_parser-0.1.0/setup.cfg +4 -0
- sigwx_parser-0.1.0/src/sigwx_parser/__init__.py +2 -0
- sigwx_parser-0.1.0/src/sigwx_parser/geometry.py +67 -0
- sigwx_parser-0.1.0/src/sigwx_parser/parser.py +95 -0
- sigwx_parser-0.1.0/src/sigwx_parser.egg-info/PKG-INFO +11 -0
- sigwx_parser-0.1.0/src/sigwx_parser.egg-info/SOURCES.txt +10 -0
- sigwx_parser-0.1.0/src/sigwx_parser.egg-info/dependency_links.txt +1 -0
- sigwx_parser-0.1.0/src/sigwx_parser.egg-info/requires.txt +3 -0
- sigwx_parser-0.1.0/src/sigwx_parser.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sigwx-parser
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A parser to convert WAFS IWXXM SIGWX XML files into GeoJSON FeatureCollections.
|
|
5
|
+
Author-email: Miguel Ebersbach <ebersbachmiguel@gmail.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/miguel-pub/sigwx-parser
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: lxml
|
|
10
|
+
Requires-Dist: shapely
|
|
11
|
+
Requires-Dist: antimeridian
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sigwx-parser"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A parser to convert WAFS IWXXM SIGWX XML files into GeoJSON FeatureCollections."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{ name="Miguel Ebersbach", email="ebersbachmiguel@gmail.com" },
|
|
12
|
+
]
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
|
+
dependencies = [
|
|
15
|
+
"lxml",
|
|
16
|
+
"shapely",
|
|
17
|
+
"antimeridian"
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
"Homepage" = "https://github.com/miguel-pub/sigwx-parser"
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import antimeridian
|
|
2
|
+
from shapely.geometry import shape, mapping
|
|
3
|
+
from shapely.geometry.polygon import orient
|
|
4
|
+
|
|
5
|
+
def process_polygons(feature_type, polygon_patch, feature_id, upper_fl=None, lower_fl=None):
|
|
6
|
+
exterior = polygon_patch.find(".//exterior//posList").text.strip().split()
|
|
7
|
+
interiors = [int.text.strip().split() for int in polygon_patch.findall(".//interior//posList")]
|
|
8
|
+
exterior = [[float(y), float(x)] for x, y in zip(exterior[::2], exterior[1::2])]
|
|
9
|
+
interiors = [[[float(y), float(x)] for x, y in zip(int[::2], int[1::2])] for int in interiors]
|
|
10
|
+
|
|
11
|
+
feature = {
|
|
12
|
+
"type": "Feature",
|
|
13
|
+
"geometry": {
|
|
14
|
+
"type": "Polygon",
|
|
15
|
+
"coordinates": [exterior, *interiors],
|
|
16
|
+
},
|
|
17
|
+
"properties": {
|
|
18
|
+
"feature_id": feature_id,
|
|
19
|
+
"phenomenon": feature_type,
|
|
20
|
+
"upper_fl": upper_fl,
|
|
21
|
+
"lower_fl": lower_fl,
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
raw_shape = shape(feature["geometry"])
|
|
25
|
+
properly_wound_shape = orient(raw_shape, sign=1.0)
|
|
26
|
+
feature["geometry"] = mapping(properly_wound_shape)
|
|
27
|
+
|
|
28
|
+
fixed_feature = antimeridian.fix_geojson(feature)
|
|
29
|
+
return fixed_feature
|
|
30
|
+
|
|
31
|
+
def process_points(feature_type, point, feature_id, upper_fl=None, lower_fl=None):
|
|
32
|
+
coords = point.find(".//pos").text.strip().split()
|
|
33
|
+
coords[1], coords[0] = float(coords[0]), float(coords[1]) # Swap to [lat, lon]
|
|
34
|
+
feature = {
|
|
35
|
+
"type": "Feature",
|
|
36
|
+
"geometry": {
|
|
37
|
+
"type": "Point",
|
|
38
|
+
"coordinates": coords,
|
|
39
|
+
},
|
|
40
|
+
"properties": {
|
|
41
|
+
"feature_id": feature_id,
|
|
42
|
+
"phenomenon": feature_type,
|
|
43
|
+
"upper_fl": upper_fl,
|
|
44
|
+
"lower_fl": lower_fl,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return feature
|
|
48
|
+
|
|
49
|
+
def process_lines(feature_type, line, feature_id, upper_fl=None, lower_fl=None):
|
|
50
|
+
coords = line.find(".//posList").text.strip().split()
|
|
51
|
+
coords = [[float(y), float(x)] for x, y in zip(coords[::2], coords[1::2])]
|
|
52
|
+
feature = {
|
|
53
|
+
"type": "Feature",
|
|
54
|
+
"geometry": {
|
|
55
|
+
"type": "LineString",
|
|
56
|
+
"coordinates": coords,
|
|
57
|
+
},
|
|
58
|
+
"properties": {
|
|
59
|
+
"feature_id": feature_id,
|
|
60
|
+
"phenomenon": feature_type,
|
|
61
|
+
"upper_fl": upper_fl,
|
|
62
|
+
"lower_fl": lower_fl,
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
fixed_feature = antimeridian.fix_geojson(feature)
|
|
66
|
+
return fixed_feature
|
|
67
|
+
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from geometry import process_points, process_lines, process_polygons
|
|
4
|
+
from lxml import etree
|
|
5
|
+
|
|
6
|
+
class SigwxParser:
|
|
7
|
+
def __init__(self, file_path: str, xml_content: str = None):
|
|
8
|
+
self.file_path = file_path
|
|
9
|
+
self.xml_content = xml_content
|
|
10
|
+
self.root = None
|
|
11
|
+
|
|
12
|
+
if file_path:
|
|
13
|
+
path = Path(file_path)
|
|
14
|
+
if not path.is_file():
|
|
15
|
+
raise FileNotFoundError(f"Could not find a file at: {path.absolute()}")
|
|
16
|
+
raw_text = path.read_text(encoding='utf-8')
|
|
17
|
+
elif xml_content:
|
|
18
|
+
raw_text = xml_content
|
|
19
|
+
else:
|
|
20
|
+
raise ValueError("Either file_path or xml_content must be provided.")
|
|
21
|
+
self.root = self._clean_xml_string(raw_text)
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def _clean_xml_string(raw_text: str):
|
|
25
|
+
clean_text = raw_text.replace('\uFEFF', '').strip()
|
|
26
|
+
parser = etree.XMLParser(recover=True)
|
|
27
|
+
root = etree.fromstring(clean_text.encode('utf-8'), parser=parser)
|
|
28
|
+
|
|
29
|
+
for elem in root.iter():
|
|
30
|
+
elem.tag = etree.QName(elem).localname
|
|
31
|
+
|
|
32
|
+
clean_attribs = {etree.QName(k).localname: v for k, v in elem.attrib.items()}
|
|
33
|
+
elem.attrib.clear()
|
|
34
|
+
elem.attrib.update(clean_attribs)
|
|
35
|
+
|
|
36
|
+
etree.cleanup_namespaces(root)
|
|
37
|
+
|
|
38
|
+
return root
|
|
39
|
+
@staticmethod
|
|
40
|
+
def _safe_text(element, default="N/A"):
|
|
41
|
+
if element is not None and element.text is not None:
|
|
42
|
+
return element.text.strip()
|
|
43
|
+
return default
|
|
44
|
+
def parse(self):
|
|
45
|
+
forecasts = self.root.xpath("//WAFSSignificantWeatherForecast")
|
|
46
|
+
collections = []
|
|
47
|
+
for f in forecasts:
|
|
48
|
+
valid_time = f.find(".//beginPosition").text.strip()
|
|
49
|
+
issue_time = f.find(".//timePosition").text.strip()
|
|
50
|
+
feature_collection = {
|
|
51
|
+
"type": "FeatureCollection",
|
|
52
|
+
"features": []
|
|
53
|
+
}
|
|
54
|
+
met_features = f.xpath(".//MeteorologicalFeature")
|
|
55
|
+
|
|
56
|
+
for feature in met_features:
|
|
57
|
+
feature_type = feature.find(".//phenomenon").get("href").strip().split("/")[-1] or "N/A"
|
|
58
|
+
feature_id = feature.get("id").strip() if feature.get("id") else "N/A"
|
|
59
|
+
upper_fl = self._safe_text(feature.find(".//upperElevation"))
|
|
60
|
+
lower_fl = self._safe_text(feature.find(".//lowerElevation"))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if feature.find(".//PolygonPatch") is not None:
|
|
64
|
+
processed_polygon = process_polygons(feature_type=feature_type,
|
|
65
|
+
polygon_patch=feature.find(".//PolygonPatch"),
|
|
66
|
+
feature_id=feature_id,
|
|
67
|
+
upper_fl=upper_fl,
|
|
68
|
+
lower_fl=lower_fl)
|
|
69
|
+
|
|
70
|
+
feature_collection.get("features").append(processed_polygon)
|
|
71
|
+
|
|
72
|
+
elif feature.find(".//Point") is not None:
|
|
73
|
+
processed_point = process_points(feature_type=feature_type,
|
|
74
|
+
point=feature.find(".//Point"),
|
|
75
|
+
feature_id=feature_id,
|
|
76
|
+
upper_fl=upper_fl,
|
|
77
|
+
lower_fl=lower_fl)
|
|
78
|
+
feature_collection.get("features").append(processed_point)
|
|
79
|
+
else:
|
|
80
|
+
processed_line = process_lines(feature_type=feature_type,
|
|
81
|
+
line=feature.find(".//CubicSpline"),
|
|
82
|
+
feature_id=feature_id,
|
|
83
|
+
upper_fl=upper_fl,
|
|
84
|
+
lower_fl=lower_fl)
|
|
85
|
+
feature_collection.get("features").append(processed_line)
|
|
86
|
+
|
|
87
|
+
collections.append({
|
|
88
|
+
"valid_time": valid_time,
|
|
89
|
+
"issue_time": issue_time,
|
|
90
|
+
"feature_collection": feature_collection})
|
|
91
|
+
return collections
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sigwx-parser
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A parser to convert WAFS IWXXM SIGWX XML files into GeoJSON FeatureCollections.
|
|
5
|
+
Author-email: Miguel Ebersbach <ebersbachmiguel@gmail.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/miguel-pub/sigwx-parser
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: lxml
|
|
10
|
+
Requires-Dist: shapely
|
|
11
|
+
Requires-Dist: antimeridian
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/sigwx_parser/__init__.py
|
|
4
|
+
src/sigwx_parser/geometry.py
|
|
5
|
+
src/sigwx_parser/parser.py
|
|
6
|
+
src/sigwx_parser.egg-info/PKG-INFO
|
|
7
|
+
src/sigwx_parser.egg-info/SOURCES.txt
|
|
8
|
+
src/sigwx_parser.egg-info/dependency_links.txt
|
|
9
|
+
src/sigwx_parser.egg-info/requires.txt
|
|
10
|
+
src/sigwx_parser.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sigwx_parser
|