Skip to content

Commit

Permalink
Merge pull request #3 from MHenderson1988/alpha2
Browse files Browse the repository at this point in the history
Alpha2
  • Loading branch information
MHenderson1988 authored Jun 21, 2023
2 parents 18c1075 + d87e937 commit 4e7151e
Show file tree
Hide file tree
Showing 8 changed files with 73 additions and 64 deletions.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ Project status - Current work in progress at Alpha stage.

# AIXMGeo

AIXMGeo is an AIXM wrapper for [KMLPlus](https://github.com/MHenderson1988/kmlplus) which supports the creation of a curved and 'floating' objects in KML.
AIXMGeo is an AIXM wrapper for [KMLPlus](https://github.com/MHenderson1988/kmlplus) which supports the creation of a
curved and 'floating' objects in KML.

AIXMGeo parses a valid, well-formed, AIXM file and outputs geographic information to a .KML
file. The .KML file can be opened in Google Earth or other mapping software.
AIXMGeo parses a valid, well-formed, AIXM file and outputs geographic information to a .KML file. The .KML file can be
opened in Google Earth or other mapping software.

AIXMGeo is currently only tested with AIXM 5.1 however it should also work with AIXM 5.1.1.

Expand Down
16 changes: 8 additions & 8 deletions aixm_geo/aixm_geo.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,7 @@ def draw_airspace(self, aixm_feature_dict, kml_obj):
lower_layer_uom=aixm_feature_dict['lower_layer_uom'],
upper_layer_uom=aixm_feature_dict['upper_layer_uom'],
uom=aixm_feature_dict['lower_layer_uom'], fol=aixm_feature_dict['name'],
altitude_mode=self.altitude_mode(aixm_feature_dict))

def altitude_mode(self, aixm_dict):
altitude_mode = 'absolute'
if aixm_dict['upper_layer_reference'] == 'SFC':
altitude_mode = 'relativetoground'
return altitude_mode
altitude_mode=util.altitude_mode(aixm_feature_dict))

def draw_cylinder(self, aixm_feature_dict, kml_obj):
aixm_feature_dict = util.convert_elevation(aixm_feature_dict)
Expand All @@ -68,4 +62,10 @@ def draw_cylinder(self, aixm_feature_dict, kml_obj):
upper_layer=float(upper_layer), uom=uom,
fol=aixm_feature_dict['name'], lower_layer_uom=aixm_feature_dict['lower_layer_uom'],
upper_layer_uom=aixm_feature_dict['upper_layer_uom'],
altitude_mode=self.altitude_mode(aixm_feature_dict))
altitude_mode=util.altitude_mode(aixm_feature_dict))


if __name__ == '__main__':
file_loc = Path().absolute().joinpath('..', Path('test_data/donlon.xml'))
output = Path().absolute()
AixmGeo(file_loc, output, 'test_kml.kml').build_kml()
53 changes: 25 additions & 28 deletions aixm_geo/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,28 +41,6 @@ def get_first_value(self, xpath: str, **kwargs: etree.Element) -> str:
value = "Unknown"
return value

# XPath with namespace example = self.root.xpath('//aixm:name', namespaces=self.namespace)
# Uses findall as opposed to find
def get_all_values(self, xpath: str, **kwargs: etree.Element):
"""Returns a list of all values within the subtree which match the Xpath provided
Args:
xpath (str): Valid Xpath string for the tag to try and find.
**kwargs:
subtree(etree.Element): The subtree to search. Defaults to self.root if no value provided.
Returns:
values(list): List of string values found.
"""
subtree = kwargs.pop('subtree', self._root)
try:
values = subtree.findall(xpath, namespaces=NAMESPACES)
if values is None:
raise AttributeError
else:
values = [x.text for x in values]
except AttributeError:
values = "Unknown"
return values

def get_first_value_attribute(self, xpath: str, **kwargs: Union[etree.Element, str]) -> Union[str, dict]:
"""
Args:
Expand Down Expand Up @@ -104,13 +82,14 @@ def get_elevation(self):

def get_crs(self):
"""
Parses the CRS from the AirspaceGeometryComponent parent tag and returns the CRS as a string.
Args:
self
Returns:
crs(str): A string of 'Anticlockwise' or 'Clockwise' depending upon the CRS
applied and the start and end angles
"""
crs = self._timeslice[-1].xpath(".//aixm:AirspaceGeometryComponent//*[@srsName]", namespaces=NAMESPACES)[0]
crs = self._timeslice[-1].xpath(".//*[@srsName]", namespaces=NAMESPACES)[0]

split = crs.get("srsName").split(':')[-1]
if split == '4326':
Expand Down Expand Up @@ -138,6 +117,15 @@ def __init__(self, root):
super().__init__(root)

def get_coordinate_list(self, subroot):
"""
Parses the LXML etree._Element object and returns a list of coordinate strings.
Args:
subroot: LXML etree._Element object
Returns:
unpacked_gml(list[str]): A list of coordinate strings
"""
unpacked_gml = None
for location in subroot:
try:
Expand Down Expand Up @@ -188,9 +176,9 @@ def unpack_geodesic_string(self, location):
def unpack_pos_list(self, string_to_manipulate):
split = string_to_manipulate.split(' ')
if len(split) > 1:
for i in range(len(split)-1):
for i in range(len(split) - 1):
if i < len(split) and i % 2 == 0:
new_string = f'{split[i]} {split[i+1]}'
new_string = f'{split[i]} {split[i + 1]}'
yield new_string
else:
yield string_to_manipulate
Expand All @@ -203,7 +191,7 @@ def unpack_arc(self, location: etree.Element) -> str:
Returns:
coordinate_string(str): A coordinate string
"""
centre = self.get_centre(location).strip()
centre = self.get_arc_centre_point(location).strip()
start_angle = self.get_first_value('.//gml:startAngle', subtree=location)
end_angle = self.get_first_value('.//gml:endAngle', subtree=location)
# Pyproj uses metres, we will have to convert for distance
Expand All @@ -227,7 +215,16 @@ def unpack_arc(self, location: etree.Element) -> str:

return coordinate_string

def get_centre(self, location):
def get_arc_centre_point(self, location):
centre = self.get_first_value('.//gml:pos', subtree=location)

# If none, check for gml:posList instead
if centre == 'Unknown':
centre = self.get_first_value('.//gml:posList', subtree=location)

return centre

def get_circle_centre_point(self, location):
centre = self.get_first_value('.//gml:pos', subtree=location)

# If none, check for gml:posList instead
Expand Down Expand Up @@ -282,7 +279,7 @@ def unpack_circle(self, location: etree.Element) -> str:
Returns:
coordinate_string(str): A coordinate string
"""
centre = self.get_centre(location)
centre = self.get_circle_centre_point(location)
radius = self.get_first_value('.//gml:radius', subtree=location)
radius_uom = self.get_first_value_attribute('.//gml:radius', subtree=location, attribute_string='uom')

Expand Down
13 changes: 13 additions & 0 deletions aixm_geo/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ def errors(self, value):
self._errors.append(value)

def get_feature_details(self):
"""
Iterates through the root of the AIXM file and returns a generator of AIXMFeature objects
Returns:
"""
aixm_features = self._root.iterfind('.//message:hasMember', NAMESPACES)
for feature in aixm_features:
aixm_feature = self.produce(feature)
Expand All @@ -48,6 +53,14 @@ def get_feature_details(self):
pass

def produce(self, subroot):
"""
Produces an individual AIXMFeature object from the subroot
Args:
subroot:
Returns:
"""
feature_type = util.get_feature_type(subroot)
try:
aixm_feature = self._feature_classes[feature_type](subroot)
Expand Down
9 changes: 8 additions & 1 deletion aixm_geo/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,18 @@ def convert_elevation(aixm_feature_dict):

if aixm_feature_dict['upper_layer'] == 'UNL':
aixm_feature_dict['upper_layer_uom'] = 'M'
aixm_feature_dict['upper_layer'] = 18288.00 # 60,000 ft in metres
aixm_feature_dict['upper_layer'] = 18288.00 # 60,000 ft in metres

return aixm_feature_dict


def altitude_mode(aixm_dict):
altitude_mode = 'absolute'
if aixm_dict['upper_layer_reference'] == 'SFC':
altitude_mode = 'relativetoground'
return altitude_mode


def switch_radius_uom(radius_uom):
if radius_uom == '[nmi_i]':
radius_uom = 'NM'
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@
"Operating System :: OS Independent",
],
python_requires='>=3.8',
)
)
19 changes: 14 additions & 5 deletions tests/test_base.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from pathlib import Path
from unittest import TestCase

from aixm_geo.base import MultiPointAixm, SinglePointAixm
from aixm_geo.factory import AixmFeatureFactory
from aixm_geo.settings import NAMESPACES
Expand All @@ -8,12 +9,13 @@
class TestSinglePointAixm(TestCase):
def setUp(self) -> None:
file_loc = Path().absolute().joinpath('..', Path('test_data/donlon.xml'))
features = AixmFeatureFactory(file_loc).root.findall('.//message:hasMember', NAMESPACES)
self.ah = SinglePointAixm(features[0])
ah_feature = AixmFeatureFactory(file_loc).root.xpath("/message:AIXMBasicMessage/message:hasMember/aixm"
":AirportHeliport", namespaces=NAMESPACES)
self.ah = SinglePointAixm(ah_feature[0])

def test_get_first_value(self):
name = self.ah.get_first_value('.//aixm:name')
self.assertEqual('REPUBLIC OF DONLON', name)
self.assertEqual('DONLON/DOWNTOWN HELIPORT', name)
self.assertNotEqual('RANDOM AIRPORT NAME', name)
self.assertTrue(isinstance(name, str))

Expand All @@ -26,9 +28,16 @@ def test_get_first_value_attribute(self):
self.assertTrue(isinstance(attribute, dict))
self.assertEqual('unknown', attribute['indeterminatePosition'])

def test_get_elevation(self):
elevation = self.ah.get_elevation()
self.assertEqual(elevation, (0.0, 'FT'))
self.assertNotEqual(elevation, (0.0, 'M'))
self.assertTrue(isinstance(elevation, tuple))


class TestMultiPointAixm(TestCase):
def setUp(self) -> None:
file_loc = Path().absolute().joinpath('..', Path('test_data/donlon.xml'))
features = AixmFeatureFactory(file_loc).root.findall('.//message:hasMember', NAMESPACES)
self.airspace = MultiPointAixm(features[0])
airspace_feature = AixmFeatureFactory(file_loc).root.xpath("/message:AIXMBasicMessage/message:hasMember/"
"aixm:Airspace", namespaces=NAMESPACES)
self.airspace = MultiPointAixm(airspace_feature[0])
18 changes: 0 additions & 18 deletions tests/test_factory.py

This file was deleted.

0 comments on commit 4e7151e

Please sign in to comment.