diff --git a/scenery/aptdat2airportsxml.py b/scenery/aptdat2airportsxml.py
new file mode 100644
index 0000000..b5d6a76
--- /dev/null
+++ b/scenery/aptdat2airportsxml.py
@@ -0,0 +1,502 @@
+#!/usr/bin/env python
+#-*- coding:utf-8 -*-
+
+import os
+import sys
+import argparse
+import statistics
+
+from fgtools.utils.files import find_input_files
+from fgtools import utils
+from fgtools.utils import geo
+from fgtools.utils import unit_convert
+
+def format_coord(coord, lonlat):
+ prefix = {"lon": ["E", "W"], "lat": ["N", "S"]}[lonlat][coord < 0]
+
+ i = abs(int(coord))
+ f = abs(coord) - i
+ return f"{prefix}{i} {f * 60:.8f}"
+
+def get_icao_xml_path(icao, what):
+ if len(icao) == 3:
+ return f"{icao[0]}/{icao[1]}/{icao}.{what}.xml"
+ else:
+ return f"{icao[0]}/{icao[1]}/{icao[2]}/{icao}.{what}.xml"
+
+class Parking:
+ def __init__(self, index, type, name, lon, lat, hdg, radius=7.5, pushback_route=False, airline_codes=[]):
+ self.index = index
+ self.type = type
+ self.name = name
+ self.lon = lon
+ self.lat = lat
+ self.hdg = hdg
+ self.radius = radius
+ self.pushback_route = pushback_route
+ self.airline_codes = airline_codes
+
+ @staticmethod
+ def get_radius(code):
+ return {"A": 7.5, "B": 14, "C": 18, "D": 26, "E": 33, "F": 40}[code]
+
+ def __repr__(self):
+ return (f' \n')
+
+class TaxiNode:
+ def __init__(self, lon, lat, index):
+ self.index = index
+ self.lon = lon
+ self.lat = lat
+ self.on_runway = None
+ self.holdPointType = "none"
+
+ def __bool__(self):
+ return self.on_runway != None
+
+ def __repr__(self):
+ return f' '
+
+class TaxiEdge:
+ def __init__(self, begin, end, is_on_runway, name):
+ self.begin = begin
+ self.end = end
+ self.name = name
+ self.is_on_runway = is_on_runway
+ self.is_pushback_route = None
+
+ def __bool__(self):
+ return self.is_pushback_route != None
+
+ def __repr__(self):
+ return f'
+
+class Runway:
+ def __init__(self, id1, lon1, lat1, displ1, stopway1, id2, lon2, lat2, displ2, stopway2):
+ self.lon1 = lon1
+ self.lat1 = lat1
+ self.id1 = id1
+ self.displ1 = displ1
+ self.stopway1 = stopway1
+ self.lon2 = lon2
+ self.lat2 = lat2
+ self.id2 = id2
+ self.displ2 = displ2
+ self.stopway2 = stopway2
+
+ def get_length_m(self):
+ return geo.great_circle_distance_m(self.lon1, self.lat1, self.lon2, self.lat2)
+
+ def get_length_ft(self):
+ return unit_convert.m2ft(self.get_length_m())
+
+ def get_heading1_deg(self):
+ return geo.get_bearing_deg(self.lon1, self.lat1, self.lon2, self.lat2)
+
+ def get_heading2_deg(self):
+ brg = self.get_heading1_deg() + 180
+ while brg >= 360:
+ brg -= 360
+ return brg
+
+ def __repr__(self):
+ return f"""
+
+ {self.lon1}
+ {self.lat1}
+ {self.id1}
+ {self.get_heading1_deg():.2f}
+ {self.displ1}
+ {self.stopway1}
+
+
+ {self.lon2}
+ {self.lat2}
+ {self.id2}
+ {self.get_heading2_deg():.2f}
+ {self.displ2}
+ {self.stopway2}
+
+
+"""
+
+class WaterRunway(Runway):
+ def __init__(self, id1, lon1, lat1, id2, lon2, lat2):
+ Runway.__init__(self, id1, lon1, lat1, 0, 0, id2, lon2, lat2, 0, 0)
+
+class Tower:
+ def __init__(self, lon, lat, agl):
+ self.lon = lon
+ self.lat = lat
+ self.agl = agl
+
+ def __repr__(self):
+ return f"""
+
+ {self.lon}
+ {self.lat}
+ {self.agl}
+
+
+"""
+
+class ILS:
+ def __init__(self):
+ self.lon1 = None
+ self.lat1 = None
+ self.rwy1 = None
+ self.hdg1 = None
+ self.elev1 = None
+ self.ident1 = None
+ self.lon2 = None
+ self.lat2 = None
+ self.rwy2 = None
+ self.hdg2 = None
+ self.elev2 = None
+ self.ident2 = None
+
+ def set_data1(self, lon, lat, rwy, hdg, ident):
+ self.lon1 = lon
+ self.lat1 = lat
+ self.rwy1 = rwy
+ self.hdg1 = hdg
+ self.ident1 = ident
+
+ def set_data2(self, lon, lat, rwy, hdg, ident):
+ self.lon2 = lon
+ self.lat2 = lat
+ self.rwy2 = rwy
+ self.hdg2 = hdg
+ self.ident2 = ident
+
+ def __repr__(self):
+ s = ""
+
+ if None not in (self.lon1, self.lat1, self.rwy1, self.hdg1, self.elev1, self.ident1):
+ s += f"""
+ {self.lon1}
+ {self.lat1}
+ {self.rwy1}
+ {self.hdg1:.2f}
+ {self.elev1}
+ {self.ident1}
+
+"""
+ if None not in (self.lon2, self.lat2, self.rwy2, self.hdg2, self.elev2, self.ident2):
+ s += """
+ {self.lon2}
+ {self.lat2}
+ {self.rwy2}
+ {self.hdg2:.2f}
+ {self.elev2}
+ {self.ident2}
+
+"""
+
+ if s:
+ s = f" \n{s} \n"
+
+ return s
+
+ def __bool__(self):
+ return (None not in (self.lon1, self.lat1, self.rwy1, self.hdg1, self.ident1)) \
+ or (None not in (self.lon2, self.lat2, self.rwy2, self.hdg2, self.ident2))
+
+
+def parse_aptdat_files(files, nav_dat, print_runway_lengths):
+ parkings = {}
+ taxi_nodes = {}
+ taxi_edges = {}
+ runways = {}
+ towers = {}
+ ils_d = {}
+
+ print("Parsing nav.dat … ", end="")
+ with open(nav_dat, "r", encoding="ISO-8859-1") as f:
+ nav_ils = list(map(str.split, filter(lambda s: s.startswith("4"), f.readlines())))
+ for i in range(0, len(nav_ils)):
+ nav_ils[i] = list(filter(None, nav_ils[i]))
+ print("done")
+
+ runway_lengths = []
+
+ i = 1
+ total = len(files)
+ for path in files:
+ print(f"\rParsing apt.dat files … {i / total * 100:.1f}% ({i} of {total})", end="")
+ with open(path, "r") as f:
+ aptdat = list(map(str.split, filter(None, map(str.strip, f.readlines()))))
+
+ icao = os.path.splitext(os.path.split(path)[-1])[0]
+ parkings[icao] = []
+ taxi_nodes[icao] = []
+ taxi_edges[icao] = []
+ runways[icao] = []
+ ils_d[icao] = []
+
+ for line in aptdat:
+ try:
+ line[0] = int(line[0])
+ except ValueError:
+ continue
+
+ if line[0] == 1300: # parking
+ parkings[icao].append(Parking(len(parkings[icao]), line[4], " ".join(line[6:]), float(line[2]), float(line[1]), int(float(line[3]))))
+ elif line[0] == 1301: # parking metadata
+ parkings[icao][-1].radius = Parking.get_radius(line[1])
+ if len(line) == 4:
+ parkings[icao][-1].airline_codes = line[3].split(",")
+ elif line[0] == 100: # land runway
+ runway = Runway(line[8], float(line[10]), float(line[9]), float(line[11]), float(line[12]),
+ line[17], float(line[19]), float(line[18]), float(line[20]), float(line[21]))
+
+ if print_runway_lengths:
+ runway_lengths.append({"icao": icao, "length-ft": runway.get_length_ft(),
+ "lon": (runway.lon1 + runway.lon2) / 2, "lat": (runway.lat1 + runway.lat2) / 2})
+
+ runways[icao].append(runway)
+
+ ils = ILS()
+ for il in nav_ils:
+ if il[8] == icao:
+ if il[9] == line[8]:
+ ils.set_data1(float(line[10]), float(line[9]), line[8], runway.get_heading1_deg(), il[7])
+ elif il[9] == line[17]:
+ ils.set_data2(float(line[19]), float(line[18]), line[17], runway.get_heading2_deg(), il[7])
+
+ if ils:
+ ils_d[icao].append(ils)
+
+ elif line[0] == 101: # water runway
+ runways[icao].append(WaterRunway(line[3], float(line[5]), float(line[4]), line[6], float(line[8]), float(line[7])))
+ elif line[0] == 14:
+ towers[icao] = Tower(float(line[2]), float(line[1]), float(line[3]))
+ elif line[0] == 1201: # taxi node
+ taxi_nodes[icao].append(TaxiNode(float(line[2], float(line[1]), len(taxi_nodes[icao]))))
+ elif line[0] == 1202: # taxi edge
+ taxi_edges[icao].append(TaxiEdge(int(line[1], int(line[2]), line[4] == "runway", line[5])))
+
+ if not icao in towers and len(runways[icao]) > 0:
+ runway_lons = []
+ runway_lats = []
+ runway_hdgs = []
+ for runway in runways[icao]:
+ runway_lons += [runway.lon1, runway.lon2]
+ runway_lats += [runway.lat1, runway.lat2]
+ runway_hdgs.append(runway.get_heading1_deg())
+
+
+ tower_lon, tower_lat = geo.apply_heading_distance(statistics.median(runway_lons),
+ statistics.median(runway_lats),
+ statistics.median(runway_hdgs) + 90, 50)
+ towers[icao] = Tower(tower_lon, tower_lat, 15)
+
+ if not parkings[icao]:
+ del parkings[icao]
+ if not runways[icao]:
+ del runways[icao]
+ if not ils_d[icao]:
+ del ils_d[icao]
+
+ i += 1
+ print()
+
+ if print_runway_lengths:
+ print("ICAO Length Lon Lat Tile index")
+ for i in sorted(runway_lengths, key=lambda d: d["length-ft"])[:10]:
+ print(i["icao"], int(i["length-ft"]), i["lon"], i["lat"], utils.get_fg_tile_index(i["lon"], i["lat"]), sep="\t")
+
+ return parkings, taxi_nodes, taxi_edges, towers, runways, ils_d
+
+def find_or_create_pushback_node(taxi_nodes, p):
+ pass
+
+def write_groundnet_files(parkings, taxi_nodes, taxi_edges, output, overwrite):
+ i = 1
+ total = len(parkings)
+ for icao in parkings:
+ print(f"\rWriting groundnet files … {i / total * 100:.1f}% ({i} of {total})", end="")
+ sys.stdout.flush()
+ path = os.path.join(output, "Airports", get_icao_xml_path(icao, "groundnet"))
+ os.makedirs(os.path.join(*os.path.split(path)[:-1]), exist_ok=True)
+
+ if os.path.isfile(path) and not overwrite:
+ print(f"\rGroundnet file {path} already exists - skipping, use --overwrite ")
+ with open(path, "w") as f:
+ utils.files.write_xml_header(f)
+ f.write("\n")
+ f.write(" 1\n")
+ f.write(" \n")
+
+ for p in parkings[icao]:
+ pushback_node = find_or_create_pushback_node(taxi_nodes, p)
+ f.write(repr(p))
+
+ f.write(" \n")
+ f.write(" ")
+
+ for edge in taxi_edges[icao]:
+ taxi_nodes[icao][edge.begin].is_on_runway = edge.is_on_runway
+ taxi_nodes[icao][edge.end].is_on_runway = edge.is_on_runway
+
+ f.write("\n")
+
+ i += 1
+ print()
+
+def write_tower_files(towers, output, elevpipe, overwrite):
+ i = 1
+ total = len(towers)
+ for icao in towers:
+ print(f"\rWriting tower files … {i / total * 100:.1f}% ({i} of {total})", end="")
+ path = os.path.join(output, "Airports", get_icao_xml_path(icao, "twr"))
+ os.makedirs(os.path.join(*os.path.split(path)[:-1]), exist_ok=True)
+
+ if os.path.isfile(path) and not overwrite:
+ print(f"\rTower file {path} already exists - skipping, use --overwrite ")
+ with open(path, "w") as f:
+ utils.files.write_xml_header(f)
+ f.write("\n")
+ f.write(repr(towers[icao]))
+ f.write("\n")
+
+ i += 1
+ print()
+
+def write_threshold_files(runways, output, overwrite):
+ i = 1
+ total = len(runways)
+ for icao in runways:
+ print(f"\rWriting threshold files … {i / total * 100:.1f}% ({i} of {total})", end="")
+ sys.stdout.flush()
+ path = os.path.join(output, "Airports", get_icao_xml_path(icao, "threshold"))
+ os.makedirs(os.path.join(*os.path.split(path)[:-1]), exist_ok=True)
+
+ if os.path.isfile(path) and not overwrite:
+ print(f"\rThreshold file {path} already exists - skipping, use --overwrite ")
+ with open(path, "w") as f:
+ utils.files.write_xml_header(f)
+ f.write("\n")
+ for runway in runways[icao]:
+ f.write(repr(runway))
+ f.write("")
+
+ i += 1
+ print()
+
+def write_ils_files(ils_d, output, elevpipe, overwrite):
+ i = 1
+ total = len(ils_d)
+ for icao in ils_d:
+ print(f"\rWriting ILS files … {i / total * 100:.1f}% ({i} of {total})", end="")
+ sys.stdout.flush()
+ for ils in ils_d[icao]:
+ elevout1, elevout2 = [], []
+ if ils.lon1 and ils.lat1:
+ elevpipe.stdin.write(f"{icao} {ils.lon1} {ils.lat1}\n".encode("utf-8"))
+ elevpipe.stdin.flush()
+ elevout1 = elevpipe.stdout.readline().split()
+ if len(elevout1) == 2:
+ ils.elev1 = float(elevout1[1])
+
+ if ils.lon2 and ils.lat2:
+ elevpipe.stdin.write(f"{icao} {ils.lon2} {ils.lat2}\n".encode("utf-8"))
+ elevpipe.stdin.flush()
+ elevout2 = elevpipe.stdout.readline().split()
+ if len(elevout2) == 2:
+ ils.elev2 = float(elevout2[1])
+
+ if not len(elevout1) == 2 and len(elevout2) == 2:
+ continue
+
+ if not list(filter(None, ils_d[icao])):
+ continue
+
+ path = os.path.join(output, "Airports", get_icao_xml_path(icao, "ils"))
+ os.makedirs(os.path.join(*os.path.split(path)[:-1]), exist_ok=True)
+
+ if os.path.isfile(path) and not overwrite:
+ print(f"\rILS file {path} already exists - skipping, use --overwrite ")
+ with open(path, "w") as f:
+ utils.files.write_xml_header(f)
+ f.write("\n")
+ for ils in ils_d[icao]:
+ if ils:
+ f.write(repr(ils))
+ f.write("\n")
+
+ i += 1
+ print()
+ print(i)
+
+if __name__ == "__main__":
+ argp = argparse.ArgumentParser(description="Convert apt.dat files to groundnet.xml files")
+
+ argp.add_argument(
+ "-i", "--input",
+ help="Input apt.dat file(s) or folder(s) containing such files",
+ required=True,
+ nargs="+"
+ )
+
+ argp.add_argument(
+ "-o", "--output",
+ help="Folder to put Airports/I/C/A/ICAO.groundnet.xml into",
+ required=True
+ )
+
+ argp.add_argument(
+ "--overwrite",
+ help="Whether to overwrite already existing files (default: False)",
+ action="store_true"
+ )
+
+ argp.add_argument(
+ "-n", "--nav-dat",
+ help="Path to nav.dat file",
+ required=True
+ )
+
+ argp.add_argument(
+ "-s", "--fgscenery",
+ help="Path to FlightGear scenery directories containing Terrain, more than one directory can be passed.",
+ nargs="+",
+ default=["~/TerraSync", "~/TerraSync/TerraSync", "TerraSync", "TerraSync/TerraSync"]
+ )
+
+ argp.add_argument(
+ "-d", "--fgdata",
+ help="Path to FlightGear data directory.",
+ default="~/fgdata"
+ )
+
+ argp.add_argument(
+ "-e", "--fgelev",
+ help="Path to FGelev",
+ default="fgelev",
+ )
+
+ argp.add_argument(
+ "-p", "--print-runway-lengths",
+ help="Only print runway lengths, do not write any files",
+ action="store_true"
+ )
+
+ args = argp.parse_args()
+
+ print("Searching apt.dat files … ", end="")
+ files = find_input_files(args.input, suffix=".dat")
+ print(f"done, found {len(files)} files")
+ parkings, taxi_nodes, taxi_edges, towers, runways, ils_d = parse_aptdat_files(files, args.nav_dat, args.print_runway_lengths)
+
+ if not args.print_runway_lengths:
+ elevpipe = utils.make_fgelev_pipe(args.fgelev, args.fgscenery, args.fgdata)
+ write_groundnet_files(parkings, taxi_nodes, taxi_edges, args.output, args.overwrite)
+ write_tower_files(towers, args.output, elevpipe, args.overwrite)
+ write_threshold_files(runways, args.output, args.overwrite)
+ write_ils_files(ils_d, args.output, elevpipe, args.overwrite)
+
diff --git a/scenery/create-day-night-xml.py b/scenery/create-day-night-xml.py
old mode 100755
new mode 100644
diff --git a/scenery/dsftxt2stg.py b/scenery/dsftxt2stg.py
old mode 100755
new mode 100644
index 21ae44f..2034a5c
--- a/scenery/dsftxt2stg.py
+++ b/scenery/dsftxt2stg.py
@@ -32,15 +32,6 @@ cessnas = [
"Cessna150_no_reg.ac"
]
-
-def find_txt_files(path):
- found = []
- for root, folders, files in os.walk(path):
- for f in files:
- if f.endswith(".txt"):
- found.append(os.path.join(root, f))
- return found
-
def parse_txt_file(path):
with open(path) as f:
content = list(map(str.strip, f.readlines()))
@@ -167,14 +158,7 @@ if __name__ == "__main__":
print("Searching for DSF/TXT files … ", end="")
sys.stdout.flush()
- txt_files = []
- for i in args.input:
- if os.path.isdir(i):
- txt_files += find_txt_files(i)
- elif os.path.isfile(i):
- txt_files.append(i)
- else:
- print(f"\rInput file / directory {i} does not exist - skipping\nSearching for DSF/TXT files … ", end="")
+ txt_files = utils.files.find_input_files(args.input)
print(f"done, found {len(txt_files)} files")
objects = parse_txt_files(txt_files)
diff --git a/scenery/edit-stg.py b/scenery/edit-stg.py
old mode 100755
new mode 100644
index 0956d5e..7847702
--- a/scenery/edit-stg.py
+++ b/scenery/edit-stg.py
@@ -8,6 +8,7 @@ import subprocess
import time
from fgtools.utils import make_fgelev_pipe
+from fgtools import utils.files
class SkipReason:
NotFound = 0
@@ -167,7 +168,7 @@ def main():
argp.add_argument(
"-i", "--input",
- help="Input STG file. Mandatory, more than one file / directory can be passed",
+ help="Input STG file / folder containing such files. Mandatory, more than one file / directory can be passed",
nargs="+",
required=True
)
@@ -199,7 +200,7 @@ def main():
)
args = argp.parse_args()
- infiles = args.input
+ infiles = utils.files.find_input_files(args.input)
outfiles = args.output
fgdata = args.fgdata
fgscenery = args.fgscenery
diff --git a/scenery/process-elevations.py b/scenery/process-elevations.py
old mode 100755
new mode 100644
diff --git a/scenery/process-shapefiles.py b/scenery/process-shapefiles.py
old mode 100755
new mode 100644
diff --git a/scenery/pull_xplane_aptdat.py b/scenery/pull_xplane_aptdat.py
old mode 100755
new mode 100644
diff --git a/scenery/stg2ufo.py b/scenery/stg2ufo.py
old mode 100755
new mode 100644
diff --git a/utils/__init__.py b/utils/__init__.py
old mode 100755
new mode 100644
index f32c4ec..5f9127f
--- a/utils/__init__.py
+++ b/utils/__init__.py
@@ -3,6 +3,7 @@
import math
import os
+import sys
import subprocess
def get_fg_tile_span(lat):
@@ -92,6 +93,8 @@ def get_fg_tile_path(lon, lat):
return f"{hem}{int(top_lon):03d}{pole}{int(top_lat):02d}/{hem}{int(main_lon):03d}{pole}{int(main_lat):02d}/{get_fg_tile_index(lon, lat)}"
def make_fgelev_pipe(fgelev, fgscenery, fgdata):
+ print("Creating pipe to fgelev … ", end="")
+ sys.stdout.flush()
env = os.environ.copy()
env["FG_SCENERY"] = os.pathsep.join(fgscenery)
env["FG_ROOT"] = fgdata
@@ -100,5 +103,25 @@ def make_fgelev_pipe(fgelev, fgscenery, fgdata):
pipe.stdout.readline()
pipe.stdin.flush()
pipe.stdin.flush()
+ print("done")
return pipe
+
+def isiterable(o, striterable=False):
+ if isinstance(o, str):
+ return striterable
+ else:
+ try:
+ iter(o)
+ return True
+ except TypeError:
+ return False
+
+def wrap_period(n, min, max):
+ while n > max:
+ n -= max - min
+ while n < min:
+ n += max - min
+
+ return n
+
diff --git a/utils/constants.py b/utils/constants.py
old mode 100755
new mode 100644
diff --git a/utils/files.py b/utils/files.py
new file mode 100644
index 0000000..180588c
--- /dev/null
+++ b/utils/files.py
@@ -0,0 +1,28 @@
+#!/usr/bin/env python
+#-*- coding:utf-8 -*-
+
+import os
+
+from fgtools.utils import isiterable
+
+def find_input_files(paths, prefix="", suffix=""):
+ if not isiterable(paths):
+ if isinstance(paths, str):
+ paths = [paths]
+ else:
+ raise TypeError("paths is not iterable")
+
+ files = []
+ for path in paths:
+ if os.path.isfile(path) and os.path.split(path)[-1].startswith(prefix) and path.endswith(suffix):
+ files.append(path)
+ elif os.path.isdir(path):
+ files += find_input_files([os.path.join(path, s) for s in os.listdir(path)])
+ else:
+ print(f"Input file / directory {path} does not exist - skipping")
+
+ return files
+
+def write_xml_header(f):
+ f.write('\n')
+
diff --git a/utils/geo.py b/utils/geo.py
new file mode 100644
index 0000000..a595451
--- /dev/null
+++ b/utils/geo.py
@@ -0,0 +1,42 @@
+#!/usr/bin/env python
+#-*- coding:utf-8 -*-
+
+import math
+
+from fgtools.utils import wrap_period
+
+EARTH_RADIUS = 6378138.12
+
+def great_circle_distance_m(lon1, lat1, lon2, lat2):
+ lon1, lat1, lon2, lat2 = map(math.radians, (lon1, lat1, lon2, lat2))
+ return abs(EARTH_RADIUS * math.acos(math.sin(lat1) * math.sin(lat2) + math.cos(lat1) * math.cos(lat2) * math.cos(lon1 - lon2)))
+
+def great_circle_distance_km(lon1, lat1, lon2, lat2):
+ return great_circle_distance_m(lon1, lat1, lon2, lat2) / 1000
+
+def get_bearing_deg(lon1, lat1, lon2, lat2):
+ dlon = (lon2 - lon1)
+ x = math.cos(math.radians(lat2)) * math.sin(math.radians(dlon))
+ y = math.cos(math.radians(lat1)) * math.sin(math.radians(lat2)) - math.sin(math.radians(lat1)) * math.cos(math.radians(lat2)) * math.cos(math.radians(dlon))
+ brg = math.atan2(x, y)
+ brg = math.degrees(brg)
+
+ return wrap_period(brg, 0, 360)
+
+def apply_heading_distance(lon, lat, heading, distance):
+ lon = math.radians(lon)
+ lat = math.radians(lat)
+ heading = math.radians(heading)
+ distance /= EARTH_RADIUS
+
+ if distance < 0:
+ distance = abs(distance)
+ heading -= math.pi
+
+ lat = math.asin(math.sin(lat) * math.cos(distance) + math.cos(lat) * math.sin(distance) * math.cos(heading))
+
+ if math.cos(lat) > 1e-15:
+ lon = math.pi - (math.pi - lon - math.asin(math.sin(heading) * math.sin(distance) / math.cos(lat)) % (2 * math.pi))
+
+ return wrap_period(math.degrees(lon), -180, 180), wrap_period(math.degrees(lat), -90, 90)
+
diff --git a/utils/interpolator.py b/utils/interpolator.py
old mode 100755
new mode 100644
diff --git a/utils/unit_convert.py b/utils/unit_convert.py
new file mode 100644
index 0000000..8d8d1ca
--- /dev/null
+++ b/utils/unit_convert.py
@@ -0,0 +1,12 @@
+#!/usr/bin/env python
+#-*- coding:utf-8 -*-
+
+import math
+
+def m2ft(m):
+ return m * 3.2808399
+
+def ft2m(ft):
+ return ft / 3.2808399
+
+