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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,2 @@
1
+ from .parser import SigwxParser
2
+ __all__ = ["SigwxParser"]
@@ -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,3 @@
1
+ lxml
2
+ shapely
3
+ antimeridian
@@ -0,0 +1 @@
1
+ sigwx_parser