-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathobject_data_parser.py
More file actions
239 lines (184 loc) · 7.33 KB
/
Copy pathobject_data_parser.py
File metadata and controls
239 lines (184 loc) · 7.33 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
import webbrowser
import folium
def parse_dof_line(line: str) -> dict:
"""
Parses a single DOF data line (after the header lines) and returns a dictionary
of all relevant fields. This follows the fixed-column specifications given by
the FAA DOF documentation.
"""
# Note: Python strings are zero-indexed, but the FAA specification is one-indexed.
# We must carefully map the specification's columns [1..N] to Python's [0..N-1].
# Columns (inclusive) -> Python slicing [start_index:end_index]
# For example, columns 1-2 -> line[0:2], columns 36-37 -> line[35:37], etc.
# If a field has whitespace, we call .strip() to clean it up.
return {
# Columns 1-2: OAS Code (State or area code, e.g., 06 for California)
"oas_code": line[0:2].strip(),
# Columns 3: a dash ("-"), we can ignore or store to verify
# Columns 4-9: Obstacle Number
"obstacle_number": line[3:9].strip(),
# Column 10: blank
# Column 11: Verification status 'O' = verified, 'U' = unverified
"verification_status": line[10].strip(),
# Column 12: blank
# Columns 13-14: Country ID
"country_id": line[12:14].strip(),
# Column 15: blank
# Columns 16-17: State ID
"state_id": line[15:17].strip(),
# Column 18: blank
# Columns 19-34: City (16 characters)
"city_name": line[18:34].strip(),
# Column 35: blank
# Columns 36-37: Latitude Degrees
"lat_deg": line[35:37].strip(),
# Column 38: blank
# Columns 39-40: Latitude Minutes
"lat_min": line[38:40].strip(),
# Column 41: blank
# Columns 42-46: Latitude Seconds
"lat_sec": line[41:46].strip(),
# Column 47: Latitude Hemisphere (N/S)
"lat_hem": line[46].strip(),
# Column 48: blank
# Columns 49-51: Longitude Degrees
"lon_deg": line[48:51].strip(),
# Column 52: blank
# Columns 53-54: Longitude Minutes
"lon_min": line[52:54].strip(),
# Column 55: blank
# Columns 56-60: Longitude Seconds
"lon_sec": line[55:60].strip(),
# Column 61: Longitude Hemisphere (E/W)
"lon_hem": line[60].strip(),
# Column 62: blank
# Columns 63-80: Obstacle Type
"obstacle_type": line[62:80].strip(),
# Column 81: blank
# Column 82: Quantity
"quantity": line[81].strip(),
# Column 83: blank
# Columns 84-88: AGL Height
"agl_height": line[83:88].strip(),
# Column 89: blank
# Columns 90-94: AMSL Height
"amsl_height": line[89:94].strip(),
# Column 95: blank
# Column 96: Lighting
"lighting": line[95].strip(),
# Column 97: blank
# Column 98: Horizontal Accuracy
"horizontal_accuracy": line[97].strip(),
# Column 99: blank
# Column 100: Vertical Accuracy
"vertical_accuracy": line[99].strip(),
# Column 101: blank
# Column 102: Mark Indicator
"mark_indicator": line[101].strip(),
# Column 103: blank
# Columns 104-117: FAA Study Number
"faa_study_number": line[103:117].strip(),
# Column 118: blank
# Column 119: Action ('A' = Add, 'C' = Change, etc.)
"action": line[118].strip(),
# Column 120: blank
# Columns 121-127: Julian date
"julian_date": line[120:127].strip(),
}
def parse_dof_file(filename: str):
"""
Reads a DOF file and returns a list of dictionaries (one per obstacle data line).
Skips header lines (the first four lines in the main DOF file) and any lines
that are just dashes.
"""
records = []
with open(filename, "r", encoding="utf-8") as f:
# Typically, the first four lines are headers in the DOF:
# 1) CURRENCY DATE line
# 2) Column titles for LAT/LONG
# 3) Column titles for OAS# etc.
# 4) A line of dashes
# This can vary slightly for the full file vs. state files, but generally
# we skip until we find lines that look like actual data lines.
for line in f:
# Trim trailing newline
line = line.rstrip("\n")
# Skip empty lines or lines that are too short to be data
if not line.strip() or len(line) < 80:
continue
# Skip lines made up only of dashes
if set(line.strip()) == {"-"}:
continue
# Now parse the line
record = parse_dof_line(line)
records.append(record)
return records
def dms_to_decimal_degrees(deg_str, min_str, sec_str, hemisphere):
"""
Converts 'degrees minutes seconds + hemisphere' (e.g. 117 05 49.19 W)
into decimal degrees, returning a float. West/South are negative.
"""
try:
degrees = float(deg_str)
except:
degrees = 0.0
try:
minutes = float(min_str)
except:
minutes = 0.0
try:
seconds = float(sec_str)
except:
seconds = 0.0
decimal = degrees + minutes / 60.0 + seconds / 3600.0
if hemisphere.upper() in ("S", "W"):
decimal = -decimal
return decimal
def create_obstacle_map(obstacle_data, map_filename="obstacles_map.html"):
"""
Takes a list of DOF obstacle records (dicts) and plots them on a folium map.
Saves the interactive map to 'obstacles_map.html' by default.
Filters obstacles to include only those within specified latitude and longitude bounds.
"""
# You can choose a default start location (e.g., continental US)
obstacle_map = folium.Map(location=[37.8283, -121.5795], zoom_start=5)
# Define latitude and longitude bounds
lat_min, lat_max = 36, 40
lon_min, lon_max = -124, -120
for obs in obstacle_data:
# Convert lat/lon from DMS -> decimal
lat = dms_to_decimal_degrees(
obs["lat_deg"], obs["lat_min"], obs["lat_sec"], obs["lat_hem"]
)
lon = dms_to_decimal_degrees(
obs["lon_deg"], obs["lon_min"], obs["lon_sec"], obs["lon_hem"]
)
# Filter objects outside the specified bounds
if not (lat_min <= lat <= lat_max and lon_min <= lon <= lon_max):
continue
# Create a marker. Use obstacle_number + obstacle_type for label/tooltip
popup_text = (
f"OAS#{obs['oas_code']}-{obs['obstacle_number']}<br>"
f"Type: {obs['obstacle_type']}<br>"
f"City: {obs['city_name']}<br>"
f"AGL Height: {obs['agl_height']} ft<br>"
f"AMSL Height: {obs['amsl_height']} ft<br>"
f"Lighting: {obs['lighting']}<br>"
f"Action: {obs['action']}<br>"
f"Julian Date: {obs['julian_date']}"
)
folium.Marker(
location=[lat, lon],
tooltip=f"{obs['obstacle_type']}",
popup=popup_text,
).add_to(obstacle_map)
# Save to an HTML file so you can open it locally in a browser
obstacle_map.save(map_filename)
print(f"Map saved to {map_filename}")
if __name__ == "__main__":
# Example usage
# Replace 'DOF.DAT' with the actual path to your DOF file
dof_filename = "DOF_250119/06-CA.Dat"
obstacles = parse_dof_file(dof_filename)
create_obstacle_map(obstacles, "obstacles_map.html")
webbrowser.open("obstacles_map.html", new=2) # open in new tab